Настройка модуля для работы c Deckhouse Stronghold

Для автоматической настройки работы модуля secrets-store-integration в связке с модулем Deckhouse Stronghold потребуется ранее включенный и настроенный Stronghold.

Далее достаточно применить следующий ресурс:

1apiVersion: deckhouse.io/v1alpha1
2kind: ModuleConfig
3metadata:
4  name: secrets-store-integration
5spec:
6  enabled: true
7  version: 1

Параметр connectionConfiguration можно опустить, поскольку он стоит в значении DiscoverLocalStronghold по умолчанию.

Настройка модуля для работы с внешним хранилищем

Для работы модуля требуется предварительно настроенное хранилище секретов, совместимое с HashiCorp Vault. В хранилище предварительно должен быть настроен путь аутентификации. Пример настройки хранилища секретов ниже.

Чтобы убедиться, что каждый API запрос зашифрован, послан и отвечен правильным адресатом, потребуется валидный публичный сертификат Certificate Authority, который используется хранилищем секретов. Такой публичный сертификат CA в PEM-формате необходимо использовать в качестве переменной caCert в конфигурации модуля.

Пример конфигурации модуля для использования Vault-совместимого хранилища секретов, запущенного по адресу «secretstoreexample.com» на TLS-порту по умолчанию - 443 TLS:

1apiVersion: deckhouse.io/v1alpha1
2kind: ModuleConfig
3metadata:
4 name: secrets-store-integration
5spec:
6 version: 1
7 enabled: true
8 settings:
9   connection:
10     url: "https://secretstoreexample.com"
11     authPath: "main-kube"
12     caCert: |
13       -----BEGIN CERTIFICATE-----
14       MIIFoTCCA4mgAwIBAgIUX9kFz7OxlBlALMEj8WsegZloXTowDQYJKoZIhvcNAQEL
15       ................................................................
16       WoR9b11eYfyrnKCYoSqBoi2dwkCkV1a0GN9vStwiBnKnAmV3B8B5yMnSjmp+42gt
17       o2SYzqM=
18       -----END CERTIFICATE-----

Крайне рекомендуется задавать переменную caCert. Если она не задана, будет использовано содержимое системного ca-certificates.

Подготовка тестового окружения

Для выполнения дальнейших команд необходим адрес и токен с правами root от Stronghold. Такой токен можно получить во время инициализации нового secrets store.

Далее в командах будет подразумеваться что данные настойки указаны в переменных окружения.

1export VAULT_TOKEN=xxxxxxxxxxx
2export VAULT_ADDR=https://secretstoreexample.com

В этом руководстве мы приводим два вида примерных команд:

Для использования инструкций по инжектированию секретов из примеров ниже вам понадобится:

  1. Создать в Stronghold секрет типа kv2 по пути demo-kv/myapp-secret и поместить туда значения DB_USER и DB_PASS.
  2. При необходимости добавляем путь аутентификации (authPath) для аутентификации и авторизации в Stronghold с помощью Kubernetes API удалённого кластера
  3. Создать в Stronghold политику myapp-ro-policy, разрешающую чтение секретов по пути demo-kv/myapp-secret.
  4. Создать в Stronghold роль myapp-role для сервис-аккаунта myapp-sa в неймспейсе myapp-namespace и привязать к ней созданную ранее политику.
  5. Создать в кластере неймспейс myapp-namespace.
  6. Создать в созданном неймспейсе сервис-аккаунт myapp-sa.

Пример команд, с помощью которых можно подготовить окружение

  • Включим и создадим Key-Value хранилище:

    1stronghold secrets enable -path=demo-kv -version=2 kv
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request POST \
    4  --data '{"type":"kv","options":{"version":"2"}}' \
    5  ${VAULT_ADDR}/v1/sys/mounts/demo-kv
    
  • Зададим имя пользователя и пароль базы данных в качестве значения секрета:

    1stronghold kv put demo-kv/myapp-secret DB_USER="username" DB_PASS="secret-password"
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request PUT \
    4  --data '{"data":{"DB_USER":"username","DB_PASS":"secret-password"}}' \
    5  ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret
    
  • Проверим, правильно ли записались секреты:

    1stronghold kv get demo-kv/myapp-secret
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret
    
  • По умолчанию метод аутентификации в Stronghold через Kubernetes API кластера, на котором запущен сам Stronghold, – включён и настроен под именем kubernetes_local. Если требуется настроить доступ через удалённые кластера, задаём путь аутентификации (authPath) и включаем аутентификацию и авторизацию в Stronghold с помощью Kubernetes API для каждого кластера:

    1stronghold auth enable -path=remote-kube-1 kubernetes
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request POST \
    4  --data '{"type":"kubernetes"}' \
    5  ${VAULT_ADDR}/v1/sys/auth/remote-kube-1
    
  • Задаём адрес Kubernetes API для каждого кластера:

    1stronghold write auth/remote-kube-1/config \
    2  kubernetes_host="https://api.kube.my-deckhouse.com"
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request PUT \
    4  --data '{"kubernetes_host":"https://api.kube.my-deckhouse.com"}' \
    5  ${VAULT_ADDR}/v1/auth/remote-kube-1/config
    
  • Создаём в Stronghold политику с названием myapp-ro-policy, разрешающую чтение секрета myapp-secret:

    1stronghold policy write myapp-ro-policy - <<EOF
    2path "demo-kv/data/myapp-secret" {
    3  capabilities = ["read"]
    4}
    5EOF
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request PUT \
    4  --data '{"policy":"path \"demo-kv/data/myapp-secret\" {\n capabilities = [\"read\"]\n}\n"}' \
    5  ${VAULT_ADDR}/v1/sys/policies/acl/myapp-ro-policy
    
  • Создаём роль, состоящую из названия пространства имён и политики. Связываем её с ServiceAccount myapp-sa из пространства имён myapp-namespace и политикой myapp-ro-policy:

    Важно! Помимо настроек со стороны Stronghold, вы должны настроить разрешения авторизации используемых serviceAccount в кластере kubernetes. Подробности в пункте ниже

    1stronghold write auth/kubernetes_local/role/myapp-role \
    2    bound_service_account_names=myapp-sa \
    3    bound_service_account_namespaces=myapp-namespace \
    4    policies=myapp-ro-policy \
    5    ttl=10m
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request PUT \
    4  --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \
    5  ${VAULT_ADDR}/v1/auth/kubernetes_local/role/myapp-role
    
  • Повторяем то же самое для остальных кластеров, указав другой путь аутентификации:

    1stronghold write auth/remote-kube-1/role/myapp-role \
    2    bound_service_account_names=myapp-sa \
    3    bound_service_account_namespaces=myapp-namespace \
    4    policies=myapp-ro-policy \
    5    ttl=10m
    

    Команда с использованием curl:

    1curl \
    2  --header "X-Vault-Token: ${VAULT_TOKEN}" \
    3  --request PUT \
    4  --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \
    5  ${VAULT_ADDR}/v1/auth/remote-kube-1/role/myapp-role
    

Важно! Рекомендованное значение TTL для токена Kubernetes составляет 10m.

Эти настройки позволяют любому поду из пространства имён myapp-namespace из обоих K8s-кластеров, который использует ServiceAccount myapp-sa, аутентифицироваться и авторизоваться в Stronghold для чтения секретов согласно политике myapp-ro-policy.

  • Создадим namespace и ServiceAccount в указанном namespace:
    1kubectl create namespace myapp-namespace
    2kubectl -n myapp-namespace create serviceaccount myapp-sa
    

Как разрешить ServiceAccount авторизоваться в Stronghold?

Для авторизации в Stronghold Pod использует токен, сгенерированный для своего ServiceAccount’а. Для того чтобы Stronghold мог проверить валидность предоставляемых данных ServiceAccount, используемый сервисом Stronghold должен иметь разрешение на действия get, list и watch для endpoints tokenreviews.authentication.k8s.io и subjectaccessreviews.authorization.k8s.io. Для этого также можно использовать clusterRole system:auth-delegator.

Stronghold может использовать различные авторизационные данные для осуществления запросов в API Kubernetes:

  1. Использовать токен приложения, которое пытается авторизоваться в Stronghold. В этом случае для каждого сервиса, авторизующегося в Stronghold, требуется в используемом ServiceAccount’е иметь clusterRole system:auth-delegator (либо права на API представленные выше).
  2. Использовать статичный токен отдельно созданного специально для Stronghold ServiceAccount у которого имеются необходимые права. Настройка Stronghold для такого случая подробно описана в документации Vault.

Инжектирование переменных окружения

Как работает

При включении модуля в кластере появляется mutating-webhook, который при наличии у пода аннотации secrets-store.deckhouse.io/role изменяет манифест пода, добавляя туда инжектор. В измененном поде добавляется инит-контейнер, который помещает из служебного образа собранный статически бинарный файл-инжектор в общую для всех контейнеров пода временную директорию. В остальных контейнерах оригинальные команды запуска заменяются на запуск файла-инжектора, который получает из Vault-совместимого хранилища необходимые данные, используя для подключения сервисный аккаунт приложения, помещает эти переменные в ENV процесса, после чего выполняет системный вызов execve, запуская оригинальную команду.

Если в манифесте пода у контейнера отсутствует команда запуска, то выполняется извлечение манифеста образа из хранилица образов (реджистри), и команда извлекается из него. Для получения манифеста из приватного хранилища образов используются заданные в манифесте пода учетные данные из imagePullSecrets.

Доступные аннотации, позволяющие изменять поведение инжектора

Аннотация Умолчание Назначение
secrets-store.deckhouse.io/role Задает роль, с которой будет выполнено подключение к хранилищу секретов
secrets-store.deckhouse.io/env-from-path Строка, содержащя список путей к секретам в хранилище через запятую, из которых будут извлечены все ключи и помещены в environment. Приоритет имеют ключи, которые находятся в списке ближе к концу.
secrets-store.deckhouse.io/ignore-missing-secrets false Запускает оригинальное приложение в случае ошибки получения секрета из хранилища
secrets-store.deckhouse.io/client-timeout 10s Таймаут операции получения секретов
secrets-store.deckhouse.io/mutate-probes false Инжектирует переменные окружения в пробы
secrets-store.deckhouse.io/log-level info Уровень логирования
secrets-store.deckhouse.io/enable-json-log false Формат логов, строка или json
secrets-store.deckhouse.io/skip-mutate-containers Список имен контейнеров через пробел, к которым не будет применятся инжектирование

Используя инжектор вы сможете задавать в манифестах пода вместо значений env-шаблоны, которые будут заменяться на этапе запуска контейнера на значения из хранилища.

Пример: извлечь из Vault-совместимого хранилища ключ DB_PASS из kv2-секрета по адресу demo-kv/myapp-secret:

1env:
2  - name: PASSWORD
3    value: secrets-store:demo-kv/data/myapp-secret#DB_PASS

Пример: извлечь из Vault-совместимого хранилища ключ DB_PASS версии 4 из kv2-секрета по адресу demo-kv/myapp-secret:

1env:
2  - name: PASSWORD
3    value: secrets-store:demo-kv/data/myapp-secret#DB_PASS#4

Шаблон может также находиться в ConfigMap или в Secret и быть подключен с помощью envFrom

1envFrom:
2  - secretRef:
3      name: app-secret-env
4  - configMapRef:
5      name: app-env

Инжектирование реальных секретов из Vault-совместимого хранилища выполнится только на этапе запуска приложения, в Secret и ConfigMap будут находиться шаблоны.

Подключение переменных из ветки хранилища (всех ключей одного секрета)

Создадим под с названием myapp1, который подключит все переменные из хранилища по пути demo-kv/data/myapp-secret:

1kind: Pod
2apiVersion: v1
3metadata:
4  name: myapp1
5  namespace: myapp-namespace
6  annotations:
7    secrets-store.deckhouse.io/role: "myapp-role"
8    secrets-store.deckhouse.io/env-from-path: demo-kv/data/common-secret,demo-kv/data/myapp-secret
9spec:
10  serviceAccountName: myapp-sa
11  containers:
12  - image: alpine:3.20
13    name: myapp
14    command:
15    - sh
16    - -c
17    - while printenv; do sleep 5; done

Применим его:

1kubectl create --filename myapp1.yaml

Проверим логи пода после его запуска, мы должны увидеть все переменные из demo-kv/data/myapp-secret:

1kubectl -n myapp-namespace logs myapp1

Удалим под

1kubectl -n myapp-namespace delete pod myapp1 --force

Подключение явно заданных переменных из хранилища

Создадим тестовый под с названием myapp2, который подключит требуемые переменные из хранилища по шаблону:

1kind: Pod
2apiVersion: v1
3metadata:
4  name: myapp2
5  namespace: myapp-namespace
6  annotations:
7    secrets-store.deckhouse.io/role: "myapp-role"
8spec:
9  serviceAccountName: myapp-sa
10  containers:
11  - image: alpine:3.20
12    env:
13    - name: DB_USER
14      value: secrets-store:demo-kv/data/myapp-secret#DB_USER
15    - name: DB_PASS
16      value: secrets-store:demo-kv/data/myapp-secret#DB_PASS
17    name: myapp
18    command:
19    - sh
20    - -c
21    - while printenv; do sleep 5; done

Применим его:

1kubectl create --filename myapp2.yaml

Проверим логи пода после его запуска, мы должны увидеть переменные из demo-kv/data/myapp-secret:

1kubectl -n myapp-namespace logs myapp2

Удалим под

1kubectl -n myapp-namespace delete pod myapp2 --force

Монтирование секрета из хранилища в качестве файла в контейнер

Для доставки секретов в приложение нужно использовать CustomResource SecretStoreImport.

В этом примере используем уже созданные ServiceAccount myapp-sa и namespace myapp-namespace из шага Подготовка тестового окружения

Создайте в кластере CustomResource SecretsStoreImport с названием myapp-ssi:

1apiVersion: deckhouse.io/v1alpha1
2kind: SecretsStoreImport
3metadata:
4  name: myapp-ssi
5  namespace: myapp-namespace
6spec:
7  type: CSI
8  role: myapp-role
9  files:
10    - name: "db-password"
11      source:
12        path: "demo-kv/data/myapp-secret"
13        key: "DB_PASS"

Создайте в кластере тестовый под с названием myapp3, который подключит требуемые переменные из хранилища в виде файла:

1kind: Pod
2apiVersion: v1
3metadata:
4  name: myapp3
5  namespace: myapp-namespace
6spec:
7  serviceAccountName: myapp-sa
8  containers:
9  - image: alpine:3.20
10    name: myapp
11    command:
12    - sh
13    - -c
14    - while cat /mnt/secrets/db-password; do echo; sleep 5; done
15    name: backend
16    volumeMounts:
17    - name: secrets
18      mountPath: "/mnt/secrets"
19  volumes:
20  - name: secrets
21    csi:
22      driver: secrets-store.csi.deckhouse.io
23      volumeAttributes:
24        secretsStoreImport: "myapp-ssi"

После применения этих ресурсов будет создан под, внутри которого запустится контейнер с названием backend. В файловой системе этого контейнера будет каталог /mnt/secrets с примонтированным к нему томом secrets. Внутри этого каталога будет лежать файл db-password с паролем от базы данных (DB_PASS) из хранилища ключ-значение Stronghold.

Проверьте логи пода после его запуска (должно выводиться содержимое файла /mnt/secrets/db-password):

1kubectl -n myapp-namespace logs myapp3

Удалите под:

1kubectl -n myapp-namespace delete pod myapp3 --force

Функция авторотации

Функция авторотации секретов в модуле secret-store-integration включена по умолчанию. Каждые две минуты модуль опрашивает Stronghold и синхронизирует секреты в примонтированном файле в случае его изменения.

Есть два варианта следить за изменениями файла с секретом в поде. Первый - следить за временем изменения примонтированного файла, реагируя на его изменение. Второй - использовать inotify API, который предоставляет механизм для подписки на события файловой системы. Inotify является частью ядра Linux. После обнаружения изменений есть большое количество вариантов реагирования на событие изменения в зависимости от используемой архитектуры приложения и используемого языка программирования. Самый простой — заставить K8s перезапустить под, перестав отвечать на liveness-пробу.

Пример использования inotify в приложении на Python с использованием пакета inotify:

1#!/usr/bin/python3
2
3import inotify.adapters
4
5def _main():
6    i = inotify.adapters.Inotify()
7    i.add_watch('/mnt/secrets-store/db-password')
8
9    for event in i.event_gen(yield_nones=False):
10        (_, type_names, path, filename) = event
11
12        if 'IN_MODIFY' in type_names:
13            print("file modified")
14
15if __name__ == '__main__':
16    _main()

Пример использования inotify в приложении на Go, используя пакет inotify:

1watcher, err := inotify.NewWatcher()
2if err != nil {
3    log.Fatal(err)
4}
5err = watcher.Watch("/mnt/secrets-store/db-password")
6if err != nil {
7    log.Fatal(err)
8}
9for {
10    select {
11    case ev := <-watcher.Event:
12        if ev == 'InModify' {
13        	log.Println("file modified")}
14    case err := <-watcher.Error:
15        log.Println("error:", err)
16    }
17}

Ограничения при обновлении секретов

Файлы с секретами не будут обновляться, если будет использован subPath.

1   volumeMounts:
2   - mountPath: /app/settings.ini
3     name: app-config
4     subPath: settings.ini
5...
6 volumes:
7 - name: app-config
8   csi:
9     driver: secrets-store.csi.deckhouse.io
10     volumeAttributes:
11       secretsStoreImport: "python-backend"

Скачать мультитул d8 для команд Stronghold

Официальный сайт Deckhouse Kubernetes Platform

Перейдите на официальный сайт и воспользуйтесь инструкцией

Субдомен вашей Deckhouse Kubernetes Platform

Для скачивания мультитула:

  1. Перейдите на страницу tools..<cluster_domain>, где <cluster_domain> — DNS-имя в соответствии с шаблоном из параметра modules.publicDomainTemplate глобальной конфигурации.

  2. Выберите Deckhouse CLI для вашей операционной системы.

  3. Для Linux и MacOS:

    • Добавьте права на выполнение d8 через chmod +x d8.
    • Переместите исполняемый файл в каталог /usr/local/bin/.

    Для Windows:

    • Распакуйте архив, переместите файл d8.exe в выбранный вами каталог и добавьте этот каталог в переменную $PATH операционной системы.
    • Разблокируйте файл d8.exe, например, следующим способом:
      • Щелкните правой кнопкой мыши на файле и выберите Свойства в контекстном меню.
      • В окне Свойства убедитесь, что находитесь на вкладке Общие.
      • Внизу вкладки Общие вы можете увидеть раздел Безопасность с сообщением о блокировке файла.
      • Установите флажок Разблокировать или нажмите кнопку Разблокировать, затем нажмите Применить и ОК, чтобы сохранить изменения.
  4. Проверьте, что утилита работает:

    1d8 help
    

Готово, вы установили d8 stronghold.