В этой статье рассмотрим, как можно организовать простое управление секретами для приложений в Kubernetes при использовании GitOps-подхода. Храним секреты в git безопасно и управляем ими из Helm Chart приложения.

Kubernetes с секретами by Kandinsky 3.1
Kubernetes с секретами by Kandinsky 3.1

Рассмотрим приложение, которое развертывается в кластере 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-файла.

Комментарии (2)


  1. chukengem
    12.09.2024 13:09

    Я правильно понял, что первичная инициализация происходит либо через файл, либо через echo? А то сижу перечитываю и никак не пойму))


    1. mmlepeshkin Автор
      12.09.2024 13:09

      Если вопрос про kubeseal, то он может читать из файла, пожет из stdin. В данном случае kubeseal используется в raw mode, подробности тут: https://github.com/bitnami-labs/sealed-secrets?tab=readme-ov-file#raw-mode-experimental