Управление секретами при деплое в k8s

Привет, хабр!

В определенный момент мы приходим к понимаю, что процесс, который работает «хорошо», должен начать работать «правильно», особенно, если речь зашла про секреты приложений.

Дано: gitlab (onprem), облако (в моем случае Yandex Cloud), 10+ сервисов, которые нужно активно и часто деплоить (с возможностью быстрого наращивания кол-ва сервисов)

Требования к решению: деплои должны происходить без участия вмешательства инженера непосредственно в процессе деплоя, процесс прозрачен для разработчика и отвечает принятым в компании требованиям к качеству и безопасности.
Решение: Оговорюсь сразу — мое решение построено на Yandex Cloud, но справедливо для любого облачного или onprem решения.
Пропускаю путь от деплоя файлом локально через kubectl до Helm (можно в отдельную тему)

Итак есть репозитории с кодом, в них размещаем файлы .{environment}.values.yaml
При деплое указываем --values=/path/to/file

Возникает главный вопрос — если configmap хранить в репозитории (через helm переменные — это правильно), то как быть с секретами?

Вариант 1: использовать helm-secrets, sops и другие решения
Получаем в репозитории зашифрованные файлы с секретами. При деплое указываем путь к файлу, helm дефишрует и деплоит вместе с секретами приложение. Однако тут же прилетают накладные расходы в виде системы для передачи ключа с которым шифровали, обязательно не забыть в gitignore добавить *.dec, иначе в какой то момент секреты утекают в репозиторий и какой был смысл тогда? Ну и велик риск потерять ключ и секреты заодно. Главный минус подхода — при изменении секрета нужно заново пройти весь pipeline, а значит и проверки, и валидацию кода, и тесты — долго.
Вариант 2: использование управление секретами через внешние системы.
Дополнительно нам понадобится:

  • Место для хранения секретов. Я использую Yandex Cloud Lockbox, можно использовать например Vault или похожие системы
  • External secret operator, как инструмент для синхронизации секретов
  • Kyverno, как инструмент для выполнения политик к кластере k8s


Выбор хранилища секретов ложиться на плечи системного инженера или архитектора. По опыту скажу — я использовал vault и lockbox. Первый, конечно более мощный, но для небольшого энтерпрайза хватит второго решения.
Далее разворачиваем в кластере External Secret Operator
Создаем сервисный аккаунт с ролью lockbox.editor

export HELM_EXPERIMENTAL_OCI=1 && \
helm pull oci://cr.yandex/yc-marketplace/yandex-cloud/external-secrets/chart/external-secrets \
  --version 0.5.5 \
  --untar && \
helm install \
  --namespace external-secret-operator \
  --create-namespace \
  --set-file auth.json=sa-key.json \
  external-secrets ./external-secrets/


Для работы с секретами так же развернем secretstore. Стоит обратить внимание, что secretstore работает в режиме namespace и изолирует секреты в рамках одного ns, ClusterSecretStore позволяет делить секреты между разными ns. Это следует помнить и не шарить лишний раз секреты, которые не будут использоваться в двух и более ns.
Предварительно создайте секрет (в примере yc-auth) c ключами от сервисного аккаунта созданного ранее. Затем установите secretstore в нужный namespace

apiVersion: external-secrets.io/v1alpha1
kind: SecretStore
metadata:
  name: secret-store
spec:
  provider:
    yandexlockbox:
      auth:
        authorizedKeySecretRef:
          name: yc-auth
          key: authorized-key'

На данном этапе подготовительные работы выполнены.
В чарт приложения можно добавить шаблон для external-secret (или измените пример для использования, как обычный манифест)


apiVersion: external-secrets.io/v1alpha1
kind: ExternalSecret
metadata:
  namespace: {{.Values.namespace }}
  name: {{ include "helm-chart.name" . }}
spec:
  refreshInterval: {{ .Values.secretRefreshInterval }}
  secretStoreRef:
    name: secret-store
    kind: SecretStore
  target:
    name: {{ include "helm-chart.name" . }}
  data:
  - secretKey: DATABASE_URL
    remoteRef:
      key: {{ .Values.secretRemote }}
      property: DATABASE_URL

В данном примере создается external-secret ресурс, который называется по названию чарта, вызывает создание одноименного k8s секрета из внешнего секрета.
Обратим внимание на два параметра:
secretRefreshInterval — интевал с которым secret-operator будет опрашивать lockbox на предмет изменения секрета (по умолчанию 1h0m0s)
secretRemote — идентификатор секрета в lockbox (будет захвачена последняя актуальная версия секрета)

После применения манифеста убедитесь, что ресурс перешел в состояние synced

На этом моменте мы получили управляемые секреты в к8s, меняем в lockbox — через какое то время — меняется в кубернетес. Но этого мало, деплоймент не узнает о том, что вы поменяли секрет или добавили новую переменную в него.
Тут на сцену выходит инструмент Kyverno (если будет необходимость — о настройке я напишу отдельный материал). В данный момент предположим, что он у вас уже стоит.


apiVersion: kyverno.io/v1
kind: ClusterPolicy
metadata:
  name: deployment-restart-policy
  namespace: test-ns
  annotations:
    policies.kyverno.io/title: Restart Deployment On Secret Change
    policies.kyverno.io/category: Other
    policies.kyverno.io/severity: medium
    policies.kyverno.io/subject: Deployment
    kyverno.io/kyverno-version: 1.9.0
    policies.kyverno.io/minversion: 1.7.0
    kyverno.io/kubernetes-version: "1.27"
spec:
  schemaValidation: false
  rules:
  - name: update-secret
    match:
      any:
      - resources:
          kinds:
          - Secret
          names:
          - test-database-adapter
          namespaces:
          - sup
    preconditions:
      all:
      - key: "{{request.operation || 'BACKGROUND'}}"
        operator: Equals
        value: UPDATE
    mutate:
      targets:
        - apiVersion: apps/v1
          kind: Deployment
          name: test-database-adapter
          namespace: test-ns
      patchStrategicMerge:
        spec:
          template:
            metadata:
              annotations:
                ops.corp.com/triggerrestart: "{{request.object.metadata.resourceVersion}}"

Данная политика не претендует на эталон, но она выполняет свою задачу —, а именно выполнить restart деплоймента, если изменится контрольная сумма указанного секрета.

В итоге, какое решение мы получили:
При деплое приложения создается external secret, который занимается обновлением секрета k8s. Изменение последнего служит триггером для перезапуска приложения.
gifmrjvt_5m79v17-3fhnsyzbso.png

© Habrahabr.ru