Flux-aio, mTLS в Kubernetes и проблема «курицы и яйца»

Здесь, в Cozystack, мы вновь решаем проблему «курицы и яйца»: как развернуть CNI и kube-proxy через Flux, обеспечив при этом работу самого Flux без CNI и kube-proxy.

Flux можно запустить без CNI и kube-proxy с помощью проекта flux-aio (от создателя Flux), который запускает одно развёртывание со всеми контроллерами, настроенными на взаимодействие друг с другом через localhost.

Особая сложность для Cozystack в том, что мы разворачиваем в каждом кластере небольшой HTTP-сервер с Helm-чартами и другими ресурсами, используемыми в платформе. Flux читает эти чарты и устанавливает их в систему.

Но как организовать доступ Flux к внутреннему HTTP-серверу, работающему как под в том же кластере?

Очевидно, что без CNI и kube-proxy он не сможет обратиться к этому поду по его постоянному имени (CoreDNS тоже зависит от CNI и kube-proxy).

Было несколько вариантов: например, добавить наш HTTP-сервер как sidecar к Flux или закрепить его через nodeAffinity на том же узле и заставить Flux обращаться к нему по localhost. Но @lllamnyp предложил более элегантное решение — маршрутизацию Flux через Kubernetes API.

Идея сразу показалась мне удачной, так как она также решает вопрос с необходимостью открытого порта на узле (хотя позже выяснилось, что это не так).

Итак, мы запускаем под cozystack-assets-0 и можем получить доступ к его содержимому по адресу:

https://example.org:6443/api/v1/namespaces/cozy-system/pods/cozystack-assets-0/proxy

Но вот в чём проблема: нам нужно как-то пройти аутентификацию, иначе Kubernetes API-сервер нас не пропустит.

Теоретически мы могли бы выделить для этого отдельный ServiceAccount и токен, но Flux не умеет добавлять заголовки и в целом не поддерживает ничего, кроме базовой HTTP-аутентификации или mTLS.

Это натолкнуло меня на мысль: почему бы не получить клиентский сертификат для Flux? К счастью, для этого нам не нужен ни cert-manager, ни какой-либо доступ к Kubernetes CA.

И здесь мы знакомимся с тем, как работает механизм получения клиентских сертификатов в Kubernetes — как оказывается, всё уже продумано за нас:

# Создаём приватный ключ и CSR
openssl genrsa -out tls.key 2048
openssl req -new -key tls.key -subj "/CN=cozystack-assets-reader" -out tls.csr

# Регистрируем CSR в Kubernetes
kubectl apply -f - <<EOF
apiVersion: certificates.k8s.io/v1
kind: CertificateSigningRequest
metadata:
  name: cozystack-assets-reader
spec:
  signerName: kubernetes.io/kube-apiserver-client
  request: $(base64 < tls.csr | tr -d '\n')
  usages:
    - client auth
EOF

# Одобряем его
kubectl certificate approve cozystack-assets-reader

# Получаем готовый сертификат, подписанный CA нашего Kubernetes-кластера
kubectl get csr cozystack-assets-reader \
  -o jsonpath='{.status.certificate}' | base64 -d > tls.crt

# Получаем CA-сертификат
kubectl get -n kube-public configmap kube-root-ca.crt \
  -o jsonpath='{.data.ca\.crt}' > ca.crt

# Создаём secret для Flux
kubectl create secret generic "cozystack-assets-tls" \
  --namespace='cozy-system' \
  --type='kubernetes.io/tls' \
  --from-file=tls.crt \
  --from-file=tls.key \
  --from-file=ca.crt

Добавляем роль:

---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: cozystack-assets-reader
  namespace: cozy-system
rules:
  - apiGroups: [""]
    resources:
      - pods/proxy
    resourceNames:
      - cozystack-assets-0
    verbs:
      - get
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: cozystack-assets-reader
  namespace: cozy-system
subjects:
  - kind: User
    name: cozystack-assets-reader
    apiGroup: rbac.authorization.k8s.io
roleRef:
  kind: Role
  name: cozystack-assets-reader
  apiGroup: rbac.authorization.k8s.io

Теперь этот secret можно использовать для доступа к нашему серверу напрямую через Kubernetes API. В спецификации HelmRepository указываем:

apiVersion: source.toolkit.fluxcd.io/v1
kind: HelmRepository
metadata:
  name: cozystack-apps
spec:
  url: https://example.org:6443/api/v1/namespaces/cozy-system/pods/cozystack-assets-0/proxy/repos/extra
  certSecretRef:
    name: cozystack-assets-tls

И теперь Flux может загрузить все необходимые ресурсы.

На мой взгляд, это красивый хак, которым стоит поделиться, поскольку он учит нас чему-то новому. Однако я бы не рекомендовал считать эту идею лучшей практикой.

Думаю, в будущем мы от этого избавимся. Мы постепенно переходим на source-watcher и возможность хранить артефакты прямо в OCIRepository. Таким образом, Flux будет загружать и собирать все необходимые артефакты напрямую из указанного OCI-образа или Git-репозитория.

Посмотреть (и провести ревью) полный код PR можно здесь: