Управление секретами при деплое в 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. Изменение последнего служит триггером для перезапуска приложения.