Ротация сертификатов в ingress-tls#

Описание#

IngressTLS — это пользовательский ресурс Kubernetes, который автоматизирует ротацию TLS-сертификатов для ingress-платформы DropApp и обновление пакета сертификатов CA для Kube-apiserver, используемого при OIDC-интеграции с Dex.

В текущей схеме в разных пространствах имен создаются TLS-секреты ingress-tls, в которых хранятся tls.crt и tls.key. Эти секреты используются ingress-ресурсами для HTTPS-терминации. При истечении срока действия сертификата или при смене цепочки доверия такие секреты обновляйте во всех пространствах имен, где они используются.

Дополнительно Kube-apiserver интегрирован с Dex по OIDC. При проверке Bearer-токен Kube-apiserver обращается к OIDC issuer/JWKS эндпоинт Dex по HTTPS и валидирует TLS-сертификат с использованием параметра:

--oidc-ca-file=/etc/kubernetes/pki/ingress-ca.crt

Файл /etc/kubernetes/pki/ingress-ca.crt расположен на control-plane узлах. После обновления этого файла перезапустите Kube-apiserver, так как --oidc-ca-file не перечитывается динамически.

Ресурс IngressTLS решает задачи:

  • Копирует TLS-материал из единого исходного секрета dapp-system/ingress-tls в целевые секреты ingress-tls в пространствах имен.

  • По SSH обновляет /etc/kubernetes/pki/ingress-ca.crt на всех control-plane узлах.

  • Выполняет плавный перезапуск Kube-apiserver на control-plane узлах, по одному узлу за раз.

  • Отражает результат выполнения в status и conditions.

Источник TLS-данных - единый секрет в пространстве имен dapp-system:

apiVersion: v1
kind: Secret
metadata:
  name: ingress-tls
  namespace: dapp-ingress
type: kubernetes.io/tls
data:
  tls.crt: <base64 fullchain>
  tls.key: <base64 private key>
  ca.crt: <base64 CA bundle>

Содержимое Secret:

Ключ

Назначение

tls.crt

Полная цепочка сертификатов для ingress: leaf-сертификат (конечный) и intermediate-сертификаты (промежуточные)

tls.key

Приватный ключ для ingress-сертификата

ca.crt

Пакет сертификатов CA для параметра --oidc-ca-file. Обычно содержит корневой сертификат (root CA) или корневой вместе с промежуточным (root + intermediate)

Важно

tls.crt и ca.crt - разные сущности:

  • tls.crt используется ingress и должен содержать полную цепочку сертификатов.

  • ca.crt используется Kube-apiserver как доверенный пакет сертификатов для проверки TLS-сертификата Dex/OIDC issuer.

Приватный ключ CA (ingress-ca.key, ca.key) для работы Kube-apiserver не требуется. По умолчанию он не должен разворачиваться на control-plane узлы. Возможность записи ключа на узлы может быть оставлена как опциональная настройка только для совместимости с уже существующими инсталляциями.

Контекст использования#

Ручная ротация ingress TLS-сертификатов и OIDC-пакетов сертификатов CA создает несколько эксплуатационных рисков.

  1. Неполное обновление TLS-секретов. Если сертификат обновлен только в части пространств имен, часть ingress продолжит отдавать старый сертификат. Это приведет к ошибкам TLS у пользователей и внешних клиентов.

  2. Рассинхронизация между ingress TLS и Kube-apiserver. Dex может успешно подхватить новый сертификат, ingress начнет отдавать новую цепочку, но Kube-apiserver продолжит использовать старый пакет сертификатов CA из памяти. В результате OIDC-аутентификация начнет прерываться с ошибками вида:

    oidc: verify token: failed to verify signature:
    fetching keys oidc: get keys failed
    tls: failed to verify certificate:
    x509: certificate signed by unknown authority
    
  3. Обновление только части control-plane узлов. В HA-кластере запросы к API server могут попадать на разные control-plane узлы. Если пакет сертификатов CA обновлен и Kube-apiserver перезапущен только на одном узле, а на остальных осталась старая конфигурация, аутентификация будет работать нестабильно: часть запросов будет успешной, часть будет завершаться ошибкой x509.

  4. Неправильное содержимое сертификатов. Для ingress-секретов в tls.crt должен храниться fullchain: leaf-сертификат вместе с intermediate-сертификатами. Если ingress отдает только leaf-сертификат, Go/TLS-клиенты, включая Kube-apiserver, могут не собрать цепочку доверия и получат ошибки: unable to get local issuer certificate или certificate signed by unknown authority.

  5. Хранение приватного ключа прямо в CRD. Если поместить tls.crt и tls.key непосредственно в spec ресурса, приватный ключ попадет в объект Kubernetes. Это ухудшает модель безопасности: любой субъект с правом чтения IngressTLS сможет получить приватный ключ. Поэтому IngressTLS должен ссылаться на секрет Kubernetes, а не хранить TLS-материал в себе.

Введение IngressTLS делает процесс ротации декларативным и воспроизводимым:

  • источник данных хранится в одном секрете dapp-system/ingress-tls;

  • контроллер сам находит целевые пространства имен и секреты;

  • обновление выполняется идемпотентно;

  • Kube-apiserver перезапускается rolling-режимом на всех control-plane узлах;

  • состояние операции видно пользователю через status и conditions.

Создание IngressTLS#

Для запуска ротации необходимо создать ресурс IngressTLS.

Важно

IngressTLS - одноразовый ресурс. Предполагается только создание, изменение - нет. Если нужен другой сертификат, создайте новый CR IngressTLS.

Пример ресурса
apiVersion: tls.dropapp.ru/v1alpha1
kind: IngressTLS
metadata:
  name: common-tls
spec:
  sourceSecretRef:
    namespace: dapp-ingress
    name: ingress-tls
  secretName: ingress-tls
  namespaces:
  - "*"
  createIfMissing: false

Поля спецификации#

Поле

Описание

spec.sourceSecretRef.namespace

Пространство имен, в котором расположен исходный Secret с TLS-данными

spec.sourceSecretRef.name

Имя исходного Secret с ключами tls.crt, tls.key и ca.crt

spec.secretName

Имя целевого TLS-секрета в пространствах имен. Если не указано, используется ingress-tls

spec.namespaces

Список пространств имен, в которых нужно обновить целевой секрет

spec.createIfMissing

Определяет поведение, если целевой секрет отсутствует

spec.apiServerOIDC.enabled

Включает обновление OIDC-пакета сертификатов CA на control-plane узлах

spec.apiServerOIDC.caKey

Ключ в исходном Secret, из которого берется пакет сертификатов CA

spec.apiServerOIDC.destCAPath

Путь на control-plane узлах, куда записывается пакет сертификатов CA

spec.apiServerOIDC.writeKey.enabled

Включает опциональную запись key-файла на control-plane узлы

spec.apiServerOIDC.restart.strategy

Стратегия рестарта Kube-apiserver

spec.apiServerOIDC.restart.pauseSeconds

Пауза между удалением и возвратом static pod manifest

spec.apiServerOIDC.restart.rolling

Рестарт control-plane узлов по очереди

Логика выбора пространств имен#

  • Если в spec.namespaces указано значение "*", контроллер обрабатывает все пространства имен кластера:

    spec:
      namespaces:
        - "*"
    
  • Если вместе со "*" указаны другие пространства имен, они игнорируются:

    spec:
      namespaces:
        - "*"
        - kube-system
    

    Такой пример трактуется как «обработать все пространства имен».

  • Если "*" не указан, контроллер обрабатывает только явно перечисленные пространства имен:

    spec:
      namespaces:
        - dapp-system
        - dapp-monitoring
        - dapp-security
    

Логика обновления ingress TLS-секретов#

В каждом целевом пространстве имен контроллер ищет секрет с именем из spec.secretName.

  • Если секрет найден, контроллер обновляет в нем:

    data["tls.crt"]
    data["tls.key"]
    

    Значения берутся из spec.sourceSecretRef.

  • Если секрет не найден и createIfMissing=false, контроллер не завершает reconcile-ошибкой. Отсутствующий секрет логируется на уровне v=3 и учитывается в статусе как missing.

  • Если секрет не найден и createIfMissing=true, контроллер создает секрет типа kubernetes.io/tls и записывает в него tls.crt и tls.key.

Логика обновления OIDC CA на control-plane узлах#

Контроллер выполняет обновление пакета сертификатов CA на control-plane узлах, если включен apiServerOIDC:

spec:
  apiServerOIDC:
    enabled: true

Алгоритм:

  1. Забрать пакет сертификатов CA из исходного Secret по ключу spec.apiServerOIDC.caKey.

  2. Для каждого control-plane узла выполнить SSH-подключение.

  3. Записать пакет сертификатов CA во временный файл.

  4. Атомарно переместить временный файл в spec.apiServerOIDC.destCAPath.

  5. При необходимости записать key-файл, если writeKey.enabled=true.

  6. Выполнить restart kube-apiserver.

  7. Перейти к следующему control-plane узлу только после завершения операции на текущем.

Рестарт Kube-apiserver выполняется через временное удаление static pod manifest:

sudo mv /etc/kubernetes/manifests/kube-apiserver.yaml /tmp/
sleep 20
sudo mv /tmp/kube-apiserver.yaml /etc/kubernetes/manifests/

Рестарт должен выполняться rolling-режимом: по одному control-plane узлу за раз. Это необходимо, чтобы не потерять доступность Kubernetes API и не получить ситуацию, когда часть Kube-apiserver использует старый пакет сертификатов CA, а часть - новый.