Приложение в Kubernetes может получать секреты из Stronghold несколькими способами. Все они опираются на метод аутентификации Kubernetes: под предъявляет токен своего ServiceAccount, а Stronghold выдаёт токен с политиками, привязанными к роли.

Сравнение способов

В таблице способы сравниваются по месту хранения секрета и способу его обновления:

СпособКак секрет попадает в подПопадает ли секрет в etcdОбновление секретаКогда использовать
CSI-драйвер модуля secrets-store-integrationФайл в томе, смонтированном в контейнерНетАвтоматически каждые две минуты; приложение должно перечитать файлПриложение читает секреты из файлов; кластер под управлением Deckhouse Platform
env-injector модуля secrets-store-integrationПеременная окружения процессаНетТолько при перезапуске подаПриложение читает конфигурацию только из переменных окружения
Stronghold Agent (init-контейнер или sidecar)Файл, отрендеренный по шаблону, или переменные окружения дочернего процессаНетSidecar перерисовывает шаблоны при изменении секрета или продлении арендыНужны шаблоны, динамические секреты, продление аренд без изменения кода
External Secrets Operator (ESO)Объект Secret Kubernetes, который под подключает как том или переменныеДаПо refreshInterval: Secret обновляется, файлы в томе — с задержкой kubelet, переменные — только после перезапускаПриложение и манифесты уже рассчитаны на обычные Secret; GitOps
Прямой вызов API из приложенияПриложение запрашивает секрет самоНетПолностью под контролем приложенияПриложение использует SDK и умеет продлевать токены и аренды

При выборе способа учитывайте следующее:

  • Если секрет не должен храниться в etcd, выбирайте CSI, env-injector, Stronghold Agent или прямой вызов API.
  • Секреты в переменных окружения видны процессам с доступом к /proc/<PID>/environ и могут попасть в дампы и логи. У файлов в томе этого недостатка нет.
  • Для динамических секретов и сертификатов с коротким сроком жизни используйте Stronghold Agent в режиме sidecar или прямой вызов API: только они продлевают аренды (leases) в течение жизни пода.

Подготовка: роль и политика

Пример ниже подходит для всех способов. Предполагается, что механизм секретов KV версии 2 включён по пути secret, а метод аутентификации Kubernetes — по пути kubernetes.

  1. Создайте политику, которая разрешает чтение секретов приложения:

    d8 stronghold policy write myapp-read - <<'POLICY'
    path "secret/data/myapp/*" {
      capabilities = ["read"]
    }
    POLICY
  2. Сохраните секрет:

    d8 stronghold kv put -mount=secret myapp/db username=app password=S3cr3t
  3. Создайте роль, которая связывает ServiceAccount myapp в неймспейсе myapp с политикой:

    d8 stronghold write auth/kubernetes/role/myapp bound_service_account_names=myapp bound_service_account_namespaces=myapp token_policies=myapp-read token_ttl=1h

    В DP в режиме Automatic метод аутентификации Kubernetes для текущего кластера создаётся модулем по пути kubernetes_local: в этом случае используйте auth/kubernetes_local вместо auth/kubernetes здесь и в примерах ниже (mount_path, URL входа). Путь auth/kubernetes относится к методу, включённому вручную.

  4. Создайте ServiceAccount в кластере:

    d8 k create namespace myapp
    d8 k -n myapp create serviceaccount myapp

Настройка самого метода аутентификации (auth/kubernetes/config) описана в разделе «Метод Kubernetes». В Deckhouse Platform модуль stronghold в режиме Automatic автоматически подключает текущий кластер для работы модуля secrets-store-integration.

CSI-драйвер secrets-store-integration

Модуль Deckhouse Platform secrets-store-integration монтирует секреты в под как файлы через CSI-драйвер. Секрет не сохраняется в объекте Secret и не попадает в etcd.

Пример SecretsStoreImport и пода, который его монтирует:

apiVersion: deckhouse.io/v1alpha1
kind: SecretsStoreImport
metadata:
  name: myapp-db
  namespace: myapp
spec:
  type: CSI
  role: myapp
  files:
    - name: db-password
      source:
        path: secret/data/myapp/db
        key: password
---
apiVersion: v1
kind: Pod
metadata:
  name: myapp
  namespace: myapp
spec:
  serviceAccountName: myapp
  containers:
    - name: app
      image: registry.example.com/myapp:1.0.0
      volumeMounts:
        - name: secrets
          mountPath: /mnt/secrets
          readOnly: true
  volumes:
    - name: secrets
      csi:
        driver: secrets-store.csi.deckhouse.io
        volumeAttributes:
          secretsStoreImport: myapp-db

Приложение читает пароль из файла /mnt/secrets/db-password. CSI-драйвер обновляет файл автоматически каждые две минуты. Чтобы использовать новое значение секрета, приложение должно перечитать файл.

env-injector secrets-store-integration

env-injector из того же модуля подставляет значения секретов в переменные окружения при старте контейнера. Значения существуют только в памяти процесса и не записываются в спецификацию пода.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
  namespace: myapp
spec:
  replicas: 1
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
      annotations:
        secrets-store.deckhouse.io/role: myapp
    spec:
      serviceAccountName: myapp
      containers:
        - name: app
          image: registry.example.com/myapp:1.0.0
          env:
            - name: DB_USER
              value: secrets-store:secret/data/myapp/db#username
            - name: DB_PASSWORD
              value: secrets-store:secret/data/myapp/db#password

Чтобы приложение получило новое значение секрета, перезапустите под, например командой d8 k -n myapp rollout restart deployment myapp.

Stronghold Agent

Stronghold Agent аутентифицируется методом Kubernetes, получает секреты и рендерит их в файлы по шаблонам. В поде Agent запускается:

  • как init-контейнер с exit_after_auth = true — секреты рендерятся один раз при старте пода;
  • как sidecar-контейнер — Agent продлевает токен и аренды и перерисовывает шаблоны при изменении секретов.

Файлы передаются приложению через общий том emptyDir с medium: Memory, поэтому секреты не записываются на диск узла.

Пример конфигурации Agent в ConfigMap (формат секций описан в разделе «Настройки»):

apiVersion: v1
kind: ConfigMap
metadata:
  name: myapp-agent
  namespace: myapp
data:
  agent.hcl: |
    stronghold {
      address = "https://stronghold.example.com"
    }

    auto_auth {
      method "kubernetes" {
        mount_path = "auth/kubernetes"
        config = {
          role = "myapp"
        }
      }
    }

    template {
      destination = "/secrets/db.env"
      contents    = <<-EOT
      {{ with secret "secret/data/myapp/db" }}
      DB_USER={{ .Data.data.username }}
      DB_PASSWORD={{ .Data.data.password }}
      {{ end }}
      EOT
    }

В примере пода ниже используется образ модуля stronghold из хранилища образов контейнеров Deckhouse. Точка входа этого образа — stronghold, поэтому в args передаются agent и -config= с путём к agent.hcl. В <STRONGHOLD_IMAGE> укажите адрес этого образа.

У образа, собранного командой stronghold bootstrap docker, нет ENTRYPOINT (подробнее — в разделе «Локальный Stronghold для разработки»). Для такого образа вместо args задайте command: ["stronghold", "agent", "-config=/etc/stronghold-agent/agent.hcl"].

Пример пода с Agent в режиме sidecar:

apiVersion: v1
kind: Pod
metadata:
  name: myapp
  namespace: myapp
spec:
  serviceAccountName: myapp
  containers:
    - name: stronghold-agent
      image: <STRONGHOLD_IMAGE>
      args: ["agent", "-config=/etc/stronghold-agent/agent.hcl"]
      volumeMounts:
        - name: agent-config
          mountPath: /etc/stronghold-agent
        - name: secrets
          mountPath: /secrets
    - name: app
      image: registry.example.com/myapp:1.0.0
      volumeMounts:
        - name: secrets
          mountPath: /secrets
          readOnly: true
  volumes:
    - name: agent-config
      configMap:
        name: myapp-agent
    - name: secrets
      emptyDir:
        medium: Memory

Для варианта с init-контейнером перенесите контейнер stronghold-agent в initContainers и добавьте в agent.hcl параметр exit_after_auth = true.

External Secrets Operator

External Secrets Operator — сторонний оператор с провайдером vault. Stronghold совместим с API Vault, поэтому провайдер можно направить на Stronghold.

ESO создаёт обычный объект Secret, поэтому значение секрета хранится в etcd. Включите шифрование объектов Secret в etcd средствами кластера и ограничьте доступ к Secret через RBAC.

apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
  name: stronghold
  namespace: myapp
spec:
  provider:
    vault:
      server: https://stronghold.example.com
      path: secret
      version: v2
      auth:
        kubernetes:
          mountPath: kubernetes
          role: myapp
          serviceAccountRef:
            name: myapp
---
apiVersion: external-secrets.io/v1
kind: ExternalSecret
metadata:
  name: myapp-db
  namespace: myapp
spec:
  refreshInterval: 15m
  secretStoreRef:
    name: stronghold
    kind: SecretStore
  target:
    name: myapp-db
  data:
    - secretKey: password
      remoteRef:
        key: myapp/db
        property: password

Прямой вызов API

Приложение может само пройти аутентификацию с токеном своего ServiceAccount и прочитать секрет. Пример на curl:

SA_TOKEN=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)

STRONGHOLD_TOKEN=$(curl -s --request POST --data "{\"role\": \"myapp\", \"jwt\": \"${SA_TOKEN}\"}" https://stronghold.example.com/v1/auth/kubernetes/login | jq -r '.auth.client_token')

curl -s --header "X-Vault-Token: ${STRONGHOLD_TOKEN}" https://stronghold.example.com/v1/secret/data/myapp/db | jq '.data.data'

Примеры для SDK и рекомендации по продлению токенов и аренд приведены в разделе «Клиенты приложений».