Стадия жизненного цикла модуля: General Availability
У модуля есть требования для установки
В этом разделе приведены примеры использования модуля secrets-store-integration.
CLI-утилита d8 для команд Stronghold
Deckhouse CLI (d8) — это универсальный инструмент, необходимый для выполнения команд вида d8 stronghold в терминале.
Чтобы установить d8, воспользуйтесь одним из способов, описанных в документации CLI-утилиты.
Настройка модуля для работы c Deckhouse Stronghold
-
Включите модуль
stronghold, следуя инструкции. -
Чтобы включить модуль
secrets-store-integration, примените следующий ресурс:apiVersion: deckhouse.io/v1alpha1 kind: ModuleConfig metadata: name: secrets-store-integration spec: enabled: true version: 1Параметр
connectionConfigurationможно не задавать, так как по умолчанию используется значениеDiscoverLocalStronghold.
Настройка модуля для работы с внешним хранилищем
Для работы модуля требуется предварительно настроенное хранилище секретов, совместимое с HashiCorp Vault. В хранилище должен быть заранее настроен путь аутентификации. Пример настройки хранилища секретов приведен ниже.
Чтобы убедиться, что каждый API-запрос зашифрован, отправлен и обработан правильным адресатом, потребуется валидный публичный сертификат Certificate Authority, который используется хранилищем секретов. Такой публичный сертификат CA в PEM-формате необходимо использовать в качестве переменной caCert в конфигурации модуля.
Пример конфигурации модуля для использования Vault-совместимого хранилища секретов, запущенного по адресу stronghold.domain.tld на TLS-порту по умолчанию (443):
apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
name: secrets-store-integration
spec:
version: 1
enabled: true
settings:
connection:
url: "https://stronghold.domain.tld"
authPath: "main-kube"
caCert: |
-----BEGIN CERTIFICATE-----
MIIFoTCCA4mgAwIBAgIUX9kFz7OxlBlALMEj8WsegZloXTowDQYJKoZIhvcNAQEL
................................................................
WoR9b11eYfyrnKCYoSqBoi2dwkCkV1a0GN9vStwiBnKnAmV3B8B5yMnSjmp+42gt
o2SYzqM=
-----END CERTIFICATE-----
connectionConfiguration: ManualРекомендуется задавать переменную caCert. Если она не задана, будет использовано содержимое системного ca-certificates.
Подготовка тестового окружения
В этом разделе приведены два варианта команд с примерами:
- команды с использованием CLI-утилиты
d8; - команды с использованием
curlдля выполнения прямых запросов в API хранилища секретов.
Перед инжектированием секретов подготовьте тестовое окружение.
-
Создайте в Stronghold секрет типа
kv2по путиdemo-kv/myapp-secretи поместите туда значенияDB_USERиDB_PASS.-
Включите и создайте Key-Value-хранилище:
d8 stronghold secrets enable -path=demo-kv -version=2 kvАльтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request POST \ --data '{"type":"kv","options":{"version":"2"}}' \ ${VAULT_ADDR}/v1/sys/mounts/demo-kv -
Задайте имя пользователя и пароль базы данных в качестве значения секрета:
d8 stronghold kv put demo-kv/myapp-secret DB_USER="username" DB_PASS="secret-password"Альтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"data":{"DB_USER":"username","DB_PASS":"secret-password"}}' \ ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret -
Проверьте записанный секрет:
d8 stronghold kv get demo-kv/myapp-secretАльтернативный вариант проверки с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ ${VAULT_ADDR}/v1/demo-kv/data/myapp-secret
-
-
При необходимости добавьте путь аутентификации (
authPath) для аутентификации и авторизации в Stronghold с помощью Kubernetes API удаленного кластера.-
По умолчанию в Stronghold включен и настроен под именем
kubernetes_localметод аутентификации через Kubernetes API кластера, на котором запущен сам Stronghold. Если требуется настроить доступ через удаленные кластеры, задайте путь (authPath) и включите аутентификацию и авторизацию в Stronghold с помощью Kubernetes API для каждого кластера:d8 stronghold auth enable -path=remote-kube-1 kubernetesАльтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request POST \ --data '{"type":"kubernetes"}' \ ${VAULT_ADDR}/v1/sys/auth/remote-kube-1 -
Задайте адрес Kubernetes API для каждого кластера:
d8 stronghold write auth/remote-kube-1/config \ kubernetes_host="https://api.kube.my-deckhouse.com"Альтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"kubernetes_host":"https://api.kube.my-deckhouse.com"}' \ ${VAULT_ADDR}/v1/auth/remote-kube-1/config
-
-
Создайте в Stronghold политику
myapp-ro-policy, разрешающую чтение секретов по путиdemo-kv/data/myapp-secret:d8 stronghold policy write myapp-ro-policy - <<EOF path "demo-kv/data/myapp-secret" { capabilities = ["read"] } EOFАльтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"policy":"path \"demo-kv/data/myapp-secret\" {\n capabilities = [\"read\"]\n}\n"}' \ ${VAULT_ADDR}/v1/sys/policies/acl/myapp-ro-policy -
Создайте в Stronghold роль для сервис-аккаунта
myapp-saв пространстве именmyapp-namespaceи привяжите к ней созданную ранее политику.Помимо настроек со стороны Stronghold, необходимо настроить разрешения авторизации используемых ServiceAccount в кластере Kubernetes.
Ознакомьтесь с необходимыми настройками в следующем разделе.
-
Создайте роль, состоящую из названия пространства имен и политики. Свяжите ее с ServiceAccount
myapp-saиз пространства именmyapp-namespaceи политикойmyapp-ro-policy:Рекомендованное значение TTL для токена Kubernetes составляет
10m.d8 stronghold write auth/kubernetes_local/role/myapp-role \ bound_service_account_names=myapp-sa \ bound_service_account_namespaces=myapp-namespace \ policies=myapp-ro-policy \ ttl=10mАльтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/kubernetes_local/role/myapp-role -
Повторите то же самое для удаленных кластеров, указав другой путь аутентификации:
d8 stronghold write auth/remote-kube-1/role/myapp-role \ bound_service_account_names=myapp-sa \ bound_service_account_namespaces=myapp-namespace \ policies=myapp-ro-policy \ ttl=10mАльтернативный вариант с использованием
curl:curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp-sa","bound_service_account_namespaces":"myapp-namespace","policies":"myapp-ro-policy","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/remote-kube-1/role/myapp-role
Эти настройки позволяют любому поду из пространства имен
myapp-namespaceиз обоих кластеров Kubernetes, который использует ServiceAccountmyapp-sa, аутентифицироваться и авторизоваться в Stronghold для чтения секретов согласно политикеmyapp-ro-policy. -
-
Создайте в кластере пространство имен
myapp-namespace:d8 k create namespace myapp-namespace -
Создайте в созданном пространстве имен сервис-аккаунт
myapp-sa:d8 k -n myapp-namespace create serviceaccount myapp-sa
Как разрешить ServiceAccount авторизоваться в Stronghold
Для авторизации в Stronghold под использует токен, сгенерированный для своего ServiceAccount. Чтобы Stronghold мог проверить валидность предоставляемых данных ServiceAccount, сервис Stronghold должен иметь разрешение на действия get, list и watch для эндпоинтов tokenreviews.authentication.k8s.io и subjectaccessreviews.authorization.k8s.io. Для этого также можно использовать ClusterRole system:auth-delegator.
Stronghold может использовать различные авторизационные данные для выполнения запросов в API Kubernetes:
- Токен приложения, которое пытается авторизоваться в Stronghold. В этом случае для каждого сервиса, авторизующегося в Stronghold, требуется, чтобы используемый ServiceAccount имел
ClusterRolesystem:auth-delegatorлибо права на API, перечисленные выше. За примером обратитесь к документации Stronghold. - Статичный токен специально созданного для Stronghold ServiceAccount, у которого имеются необходимые права. Настройка Stronghold для такого случая подробно описана в документации Stronghold.
Инжектирование переменных окружения
Как работает инжектирование
При включении модуля в кластере появляется mutating-webhook, который при наличии у пода аннотации secrets-store.deckhouse.io/role изменяет манифест пода, добавляя туда инжектор.
В измененном поде:
- Добавляется init-контейнер.
- Init-контейнер помещает из служебного образа статически собранный бинарный файл-инжектор в общую для всех контейнеров под временную директорию.
- В остальных контейнерах оригинальные команды запуска заменяются на запуск файла-инжектора.
- Инжектор получает из Vault-совместимого хранилища необходимые данные, используя для подключения сервисный аккаунт приложения.
- Инжектор помещает эти переменные в
ENVпроцесса. - Инжектор запускает оригинальную команду. Способ запуска зависит от режима:
- По умолчанию инжектор выполняет системный вызов
execve, заменяя собой текущий процесс — приложение становится PID 1 контейнера. - В режимах мониторинга секретов, при установке аннотации
secrets-store.deckhouse.io/restart-on-secret-changeв значенияwatch-for-leaseилиwatch-for-data, инжектор используетfork+exec: остаётся родительским процессом контейнера (PID 1), а приложение запускается как дочерний процесс. Это нужно для продления lease и/или периодической проверки значений секретов.
- По умолчанию инжектор выполняет системный вызов
Если в манифесте пода у контейнера отсутствует команда запуска, выполняется извлечение манифеста образа из хранилища образов контейнеров, и команда извлекается из него.
Для получения манифеста из приватного хранилища образов используются заданные в манифесте пода учетные данные из imagePullSecrets.
Аннотации инжектора
Доступные аннотации, позволяющие изменять поведение инжектора:
| Аннотация | Значение по умолчанию | Описание |
|---|---|---|
secrets-store.deckhouse.io/addr |
Из модуля | Адрес хранилища секретов в формате https://stronghold.mycompany.tld:8200 |
secrets-store.deckhouse.io/tls-secret |
Из модуля | Имя объекта Secret в Kubernetes, в котором есть ключ ca.crt со значением сертификата CA (Центра сертификации) в формате PEM |
secrets-store.deckhouse.io/tls-skip-verify |
false |
Отключение проверки TLS-сертификата сервера |
secrets-store.deckhouse.io/auth-path |
Из модуля | Путь, который следует использовать при аутентификации |
secrets-store.deckhouse.io/namespace |
Из модуля | Неймспейс, который будет использоваться для подключения к хранилищу |
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 |
Список имен контейнеров через пробел, к которым не будет применяться инжектирование | |
secrets-store.deckhouse.io/service-account-token-path |
<service-account-token-volume-name>/token |
Путь к файлу JWT-токена ServiceAccount для аутентификации инжектора в хранилище. |
secrets-store.deckhouse.io/restart-on-secret-change |
none |
Режим реакции на изменение секретов: none — без мониторинга (по умолчанию), watch-for-lease — завершить приложение при истечении lease (для динамических секретов, например database), watch-for-data — периодически проверять значения секретов и перезапускать контейнер при изменении |
secrets-store.deckhouse.io/secret-poll-interval |
120s |
Интервал опроса секретов в режиме watch-for-data |
Используя инжектор, вы сможете задавать в манифестах пода вместо значений env шаблоны, которые будут заменяться на этапе запуска контейнера значениями из хранилища.
Подключение переменных из ветки хранилища имеет более высокий приоритет, чем подключение явно заданных переменных из хранилища. Это значит, что при одновременном использовании аннотации secrets-store.deckhouse.io/env-from-path с путем до секрета, который содержит, например, ключ MY_SECRET, и переменной окружения в манифесте с тем же именем:
env:
- name: MY_SECRET
value: secrets-store:demo-kv/data/myapp-secret#passwordв переменную окружения MY_SECRET внутри контейнера будет записано значение секрета из аннотации.
Пример извлечения из Vault-совместимого хранилища ключа DB_PASS из kv2-секрета по адресу demo-kv/myapp-secret:
env:
- name: PASSWORD
value: secrets-store:demo-kv/data/myapp-secret#DB_PASSПример извлечения из Vault-совместимого хранилища ключа DB_PASS версии 4 из kv2-секрета по адресу demo-kv/myapp-secret:
env:
- name: PASSWORD
value: secrets-store:demo-kv/data/myapp-secret#DB_PASS#4Шаблоны значений (templater)
Вместо имени ключа во второй части ссылки (path#key) можно указать Go-шаблон с разделителями ${ и }. Шаблон выполняется по данным секрета, прочитанным из хранилища. Это позволяет собрать значение переменной окружения из нескольких полей одного секрета.
Допустим, в хранилище по пути demo-kv/data/database/config лежит секрет:
{
"username": "app",
"password": "s3cr3t",
"host": "db.example.com",
"port": "5432"
}Можно задать переменную с URL подключения к базе данных:
env:
- name: DATABASE_URL
value: secrets-store:demo-kv/data/database/config#postgresql://${.username}:${.password}@${.host}:${.port}/mydbДля указания конкретной версии секрета добавьте номер версии третьим сегментом через #:
env:
- name: DATABASE_URL
value: secrets-store:demo-kv/data/database/config#postgresql://${.username}:${.password}@${.host}:${.port}/mydb#4В шаблонах доступны функции Sprig, а также функции file (чтение содержимого файла) и accessor (placeholder для accessor-пути Vault).
Настройка пути к токену ServiceAccount
По умолчанию инжектор читает JWT-токен ServiceAccount из файла /var/run/secrets/kubernetes.io/serviceaccount/token. Если токен смонтирован в другом месте (например, через projected volume или отдельный Secret), укажите путь к файлу с помощью аннотации secrets-store.deckhouse.io/service-account-token-path:
kind: Pod
apiVersion: v1
metadata:
name: myapp
annotations:
secrets-store.deckhouse.io/role: "myapp-role"
secrets-store.deckhouse.io/service-account-token-path: /var/run/secrets/tokens/vault-token
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: myapp:latestШаблон может также находиться в ConfigMap или в Secret и быть подключен с помощью envFrom:
envFrom:
- secretRef:
name: app-secret-env
- configMapRef:
name: app-envИнжектирование реальных секретов из Vault-совместимого хранилища выполняется только на этапе запуска приложения. В Secret и ConfigMap будут находиться шаблоны.
Перезапуск при изменении секретов
По умолчанию (restart-on-secret-change: none) инжектор читает секреты один раз при старте контейнера и запускает приложение через execve, заменяя собой текущий процесс. Аннотация secrets-store.deckhouse.io/restart-on-secret-change включает мониторинг и автоматический перезапуск контейнера; в этом случае приложение запускается через fork+exec, а инжектор остаётся PID 1 контейнера.
watch-for-lease — режим для динамических секретов с lease (например, database credentials). Инжектор остаётся родительским процессом, продлевает lease и завершает дочерний процесс при истечении срока действия секрета. Kubernetes перезапускает контейнер, и секреты читаются заново.
watch-for-data — режим для статических секретов (KV). Инжектор периодически (с интервалом secret-poll-interval, по умолчанию 120s) перечитывает значения секретов, сравнивает их хэш с исходным и при изменении завершает дочерний процесс, после чего выходит сам. Kubernetes перезапускает контейнер целиком, и секреты инжектируются заново. Фактический интервал опроса выбирается при старте случайно в диапазоне ±30% от заданного, поэтому реплики одного приложения замечают изменение секрета в разные моменты и не перезапускаются одновременно. В этом режиме секреты, полученные через POST >>secrets-store:
не учитываются в расчете дайджеста.
Пример для KV-секретов с автоматическим перезапуском при изменении:
kind: Pod
apiVersion: v1
metadata:
name: myapp
annotations:
secrets-store.deckhouse.io/role: "myapp-role"
secrets-store.deckhouse.io/restart-on-secret-change: "watch-for-data"
secrets-store.deckhouse.io/secret-poll-interval: "30s"
spec:
serviceAccountName: myapp-sa
containers:
- name: myapp
image: myapp:latest
env:
- name: PASSWORD
value: secrets-store:demo-kv/data/myapp-secret#DB_PASSПодключение переменных из ветки хранилища
В этом сценарии подключаются все ключи одного секрета.
-
Создайте под с именем
myapp1, который подключит все переменные из хранилища по путиdemo-kv/data/myapp-secret:kind: Pod apiVersion: v1 metadata: name: myapp1 namespace: myapp-namespace annotations: secrets-store.deckhouse.io/role: "myapp-role" secrets-store.deckhouse.io/env-from-path: demo-kv/data/common-secret,demo-kv/data/myapp-secret spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 name: myapp command: - sh - -c - while printenv; do sleep 5; done -
Примените созданный манифест:
d8 k create --filename myapp1.yaml -
Проверьте логи пода после его запуска. В результате должны быть выведены все переменные из
demo-kv/data/myapp-secret:d8 k -n myapp-namespace logs myapp1 -
Удалите под:
d8 k -n myapp-namespace delete pod myapp1 --force
Подключение явно заданных переменных из хранилища
-
Создайте тестовый под с именем
myapp2, который подключит требуемые переменные из хранилища по шаблону:kind: Pod apiVersion: v1 metadata: name: myapp2 namespace: myapp-namespace annotations: secrets-store.deckhouse.io/role: "myapp-role" spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 env: - name: DB_USER value: secrets-store:demo-kv/data/myapp-secret#DB_USER - name: DB_PASS value: secrets-store:demo-kv/data/myapp-secret#DB_PASS name: myapp command: - sh - -c - while printenv; do sleep 5; done -
Примените созданную конфигурацию:
d8 k create --filename myapp2.yaml -
Проверьте логи пода после его запуска. В результате должны быть выведены переменные из
demo-kv/data/myapp-secret:d8 k -n myapp-namespace logs myapp2 -
Удалите под:
d8 k -n myapp-namespace delete pod myapp2 --force
Монтирование секрета из хранилища в качестве файла в контейнер
Для доставки секретов в приложение используйте кастомный ресурс SecretsStoreImport.
В этом примере используются сервисный аккаунт myapp-sa и неймспейс myapp-namespace, созданные на этапе подготовки тестового окружения.
-
Создайте в кластере кастомный ресурс SecretsStoreImport с именем
myapp-ssi:apiVersion: deckhouse.io/v1alpha1 kind: SecretsStoreImport metadata: name: myapp-ssi namespace: myapp-namespace spec: type: CSI role: myapp-role files: - name: "db-password" source: path: "demo-kv/data/myapp-secret" key: "DB_PASS" -
Создайте в кластере тестовый под с именем
myapp3, который подключит секрет из хранилища в виде файла:kind: Pod apiVersion: v1 metadata: name: myapp3 namespace: myapp-namespace spec: serviceAccountName: myapp-sa containers: - image: alpine:3.20 name: backend command: - sh - -c - while cat /mnt/secrets/db-password; do echo; sleep 5; done volumeMounts: - name: secrets mountPath: "/mnt/secrets" volumes: - name: secrets csi: driver: secrets-store.csi.deckhouse.io volumeAttributes: secretsStoreImport: "myapp-ssi"После применения этих ресурсов будет создан под, внутри которого запустится контейнер
backend. В файловой системе контейнера будет каталог/mnt/secretsс примонтированным к нему томомsecrets. Внутри этого каталога будет находиться файлdb-passwordс паролем от базы данных (DB_PASS) из Key-Value-хранилища Stronghold. -
Проверьте логи пода после его запуска. Должно выводиться содержимое файла
/mnt/secrets/db-password:d8 k -n myapp-namespace logs myapp3 -
Удалите под:
d8 k -n myapp-namespace delete pod myapp3 --force
Доставка бинарных файлов в контейнер
В некоторых случаях может потребоваться доставить в контейнер бинарный файл, например:
- JKS-контейнер с ключами;
keytabдля Kerberos-аутентификации.
В этом случае можно закодировать бинарный файл в Base64 и поместить его в хранилище секретов. При извлечении CSI-драйвер раскодирует данные и поместит в контейнер бинарный файл. Для этого нужно установить параметр decodeBase64 в true для соответствующего файла.
Если декодирование выполнить не удастся, например если в хранилище находится невалидный Base64, контейнер не будет создан.
Пример:
-
Закодируйте файл в Base64 и поместите его в хранилище:
d8 stronghold kv put demo-kv/myapp-secret keytab=$(cat /path/to/keytab_file | base64 -w0) -
Создайте манифест SecretsStoreImport, указав параметр для раскодирования файла:
apiVersion: deckhouse.io/v1alpha1 kind: SecretsStoreImport metadata: name: myapp-ssi namespace: myapp-namespace spec: type: CSI role: myapp-role files: - name: "keytab" decodeBase64: true source: path: "demo-kv/data/myapp-secret" key: "keytab" -
В контейнере будет создан бинарный файл с именем
keytab.
Функция авторотации
Функция авторотации секретов в модуле secrets-store-integration включена по умолчанию. Каждые две минуты модуль опрашивает Stronghold и синхронизирует секреты в примонтированном файле в случае их изменения.
Существует два способа отслеживания изменений файла с секретом в поде:
- следить за временем изменения примонтированного файла и реагировать на его изменение;
- использовать
inotifyAPI, который предоставляет механизм подписки на события файловой системы.
Inotify является частью ядра Linux. При обнаружении изменений существует множество вариантов реагирования в зависимости от архитектуры приложения и используемого языка программирования. Самый простой способ — заставить Kubernetes перезапустить под, перестав отвечать на livenessProbe.
Пример использования inotify в приложении на Python:
#!/usr/bin/python3
import inotify.adapters
def _main():
i = inotify.adapters.Inotify()
i.add_watch('/mnt/secrets-store/db-password')
for event in i.event_gen(yield_nones=False):
(_, type_names, path, filename) = event
if 'IN_MODIFY' in type_names:
print("file modified")
if __name__ == '__main__':
_main()Пример использования inotify в приложении на Go:
watcher, err := inotify.NewWatcher()
if err != nil {
log.Fatal(err)
}
err = watcher.Watch("/mnt/secrets-store/db-password")
if err != nil {
log.Fatal(err)
}
for {
select {
case ev := <-watcher.Event:
if ev == 'InModify' {
log.Println("file modified")
}
case err := <-watcher.Error:
log.Println("error:", err)
}
}Ограничения при обновлении секретов
Файлы с секретами не будут обновляться, если используется subPath.
volumeMounts:
- mountPath: /app/settings.ini
name: app-config
subPath: settings.ini
...
volumes:
- name: app-config
csi:
driver: secrets-store.csi.deckhouse.io
volumeAttributes:
secretsStoreImport: "python-backend"