В этой статье рассмотрим, как можно организовать простое управление секретами для приложений в Kubernetes при использовании GitOps-подхода. Храним секреты в git безопасно и управляем ими из Helm Chart приложения.
![Kubernetes с секретами by Kandinsky 3.1 Kubernetes с секретами by Kandinsky 3.1](https://habrastorage.org/getpro/habr/upload_files/34f/e93/d71/34fe93d710d4be3790aa893af1b06d1b.png)
Рассмотрим приложение, которое развертывается в кластере 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-файла.