Управление секретами Kubernetes с Sealed Secrets и Helm
Рассмотрим приложение, которое развертывается в кластере Kubernetes с использованием Helm Chart и GitOps. Согласно принципам GitOps все данные, необходимые для развертывания приложения, должны храниться в git-репозитории. Артефакты: docker-образы, Helm-чарты и т.п., могут храниться в отдельных реестрах или репозиториях, но должны быть однозначно идентифицированы, например, с помощью версионирования. Таким образом, git-репозиторий является единым источником истины для развертывания приложения. Однако складывать секреты в git в открытом виде, или, как предлагают стандартные средства Kubernetes и Helm, просто в base64, совершенно не безопасно.
Конечно, для хранения секретов можно воспользоваться специальными инструментами, вроде HashiCorp Vault, когда это оправдано масштабом проекта. В этой статье я хочу остановиться на простом решении, почти не требующем дополнительных внешних зависимостей и дополнительных усилий при эксплуатации. Оно вполне применимо для небольших систем и простых политик безопасности.
Для решения задачи будем использовать следующие инструменты:
- Универсальный Helm Chart от Nixys
- Flux CD в качестве GitOps инструментария
- Sealed Secrets для шифрования секретов
Аналогичную конструкцию можно реализовать для любого Helm Chart и другой GitOps-системы, например, ArgoCD.
Sealed Secrets представляет собой решение от Bitnami, специально предназначенное для организации хранения секретов в git-репозитории и работы в связке с GitOps-системами. Секрет предварительно зашифровывается и может быть сохранен в git в виде объекта типа SealedSecret
. Контроллер Sealed Secrets расшифровывает секреты и предоставляет их приложениям обычным способом. Он довольно легковесный, не требует настройки и практически не потребляет ресурсы кластера. Шифрование секретов производится с помощью консольной команды kubeseal
. При стандартной способе использования она создает готовый манифест для объекта SealedSecret
.
Но тут кроется одно неудобство. Если размещать секрет в виде отдельного манифеста, он становится недоступен для управления из Helm Chart приложения. Например, затруднительно отслеживать его изменения для рестарта приложения, а также гарантировать наличие секрета до запуска пода. Одним из решений может быть включение зашифрованного секрета непосредственно в Helm Chart приложения.
Реализация
Для удобства использования нашего решения добавим в универсальный Helm Chart темплейт и хелпер для Sealed Secrets.
{{- range $sName, $val := .Values.sealedSecrets -}} --- apiVersion: bitnami.com/v1alpha1 kind: SealedSecret metadata: name: {{ include "helpers.app.fullname" (dict "name" $sName "context" $) }} namespace: {{ $.Release.Namespace | quote }} labels: {{- include "helpers.app.labels" $ | nindent 4 }} {{- with $val.labels }}{{- include "helpers.tplvalues.render" (dict "value" . "context" $) | nindent 4 }}{{ end }} annotations: {{- include "helpers.app.hooksAnnotations" $ | nindent 4 }} {{- with $val.annotations }}{{- include "helpers.tplvalues.render" (dict "value" . "context" $) | nindent 4 }}{{ end }} spec: encryptedData: {{- include "helpers.sealedSecrets.render" (dict "value" $val.encryptedData) | indent 4 }} template: metadata: name: {{ include "helpers.app.fullname" (dict "name" $sName "context" $) }} namespace: {{ $.Release.Namespace | quote }} labels: {{- include "helpers.app.labels" $ | nindent 8 }} {{- with $val.labels }}{{- include "helpers.tplvalues.render" (dict "value" . "context" $) | nindent 8 }}{{ end }} annotations: {{- with $val.annotations }}{{- include "helpers.tplvalues.render" (dict "value" . "context" $) | nindent 8 }}{{ end }} {{- end }} {{- define "helpers.sealedSecrets.render" -}} {{- $v := dict -}} {{- if kindIs "string" .value -}} {{- $v = fromYaml .value }} {{- else -}} {{- $v = .value }} {{- end -}} {{- range $key, $value := $v }} {{ printf "%s: %s" $key $value }} {{- end -}} {{- end -}}
Наш темплейт будет создавать секреты из раздела .Values.sealedSecrets
, добавлять к ним лейблы и аннотации, определенные, как для приложения, так и для самого ресурса. Зашифрованные данные помещаются в encryptedData
в виде стандартного словаря.
Стоит обратить внимание на то, что здесь используются хуки, с помощью которых Helm создает объект SealedSecret
до создания и запуска подов приложения. Этот подход используется в Helm Chart от Nixys для объектов типа ConfigMap
и Secret
. Он гарантирует, что приложение при запуске получит правильную версию конфигурации, однако при этом ресурс не будет автоматически удален, когда перестанет использоваться. Аналогичным образом можно определить темплейт и без хуков, если такое поведение неудобно.
Если нужно, чтобы приложение рестартовало автоматически при изменении секрета, к его подам можно добавить аннотацию с контрольной суммой всех секретов.
checksum/secrets: '{{ include "helpers.workload.checksum" (printf "%s" $.Values.sealedScrets) }}'
Теперь мы можем зашифровать секрет, например так:
kubeseal --raw --scope=namespace-wide --namespace=yournamespace --from-file=yoursecret.txt
Таким образом мы получаем строку содержащую контент файла yoursecret.txt
в зашифрованном виде. Мы указали тут скоуп namespace-wide
для того, чтобы не привязываться к имени ресурса, которое может генерировать Helm при рендеринге чарта.
Полученную строку мы добавим в Values следующим образом:
sealedSecrets: yoursecretname: annotations: sealedsecrets.bitnami.com/namespace-wide: "true" encryptedData: FOO: "encrypted-secret-string"
Стоит обратить внимание, что здесь мы дополнительно добавляем аннотацию sealedsecrets.bitnami.com/namespace-wide: "true"
, чтобы скоуп ресурса соответствовал нашим зашифрованным данным.
Проверка
Опишем наше приложение через values универсального чарта. Для примера возьмем тестовый микросервис podinfo, который не требует какой-либо конфигурации, но позволит нам протестировать правильную передачу секрета.
Для начала зашифруем наш секрет. Для удобства передадим его прямо из командной строки:
echo -n 'very-secret-string' | \ kubeseal --raw --scope=namespace-wide --namespace=podinfo --from-file=/dev/stdin
Для деплоя приложения через Flux CD создадим описание объекта HelmRelease
, содержащее минимально необходимые параметры values для деплоя podinfo с помощью универсального чарта. Мы определим deployment
, service
, ingress
и SealedSecret
. Полученную ранее зашифрованную строку вставим в sealedSecrets.app-secret.encryptedData
.
apiVersion: helm.toolkit.fluxcd.io/v2beta1 kind: HelmRelease metadata: name: podinfo namespace: podinfo spec: interval: 10m chart: spec: chart: universal-chart version: '>=2.8.0' sourceRef: kind: HelmRepository name: your-helm-repository namespace: your-repository-namespace interval: 10m values: deployments: app: containers: - name: podinfo image: stefanprodan/podinfo imagePullPolicy: IfNotPresent ports: - name: http containerPort: 9898 envSecrets: - app-secret services: app: type: ClusterIP ports: - name: http protocol: TCP port: 9898 ingresses: app: hosts: - hostname: podinfo.example.com paths: - serviceName: app servicePort: 9898 path: "/" sealedSecrets: app-secret: annotations: sealedsecrets.bitnami.com/namespace-wide: "true" encryptedData: SECRET_VARIABLE: <your-encrypted-string>
После деплоя проверим, правильно ли передался секрет в приложение. Для podinfo достаточно выполнить команду:
curl -X 'GET' 'https://podinfo.example.com/env'
В ответ мы должны получить массив переменных, содержащий и наш секрет:
[ ... "SECRET_VARIABLE=very-secret-string", ... ]
Аналогично можно добавить темплейт для SealedSecret
в любой другой "библиотечный" Chart, например генерируемый helm create
. При этом отличаться будут только используемые внутри хелперы и структура values-файла.