Стадия жизненного цикла модуля: 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-совместимого хранилища секретов, запущенного по адресу secretstoreexample.com на TLS-порту по умолчанию (443):
apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
name: secrets-store-integration
spec:
version: 1
enabled: true
settings:
connection:
url: "https://secretstoreexample.com"
authPath: "main-kube"
caCert: |
-----BEGIN CERTIFICATE-----
MIIFoTCCA4mgAwIBAgIUX9kFz7OxlBlALMEj8WsegZloXTowDQYJKoZIhvcNAQEL
................................................................
WoR9b11eYfyrnKCYoSqBoi2dwkCkV1a0GN9vStwiBnKnAmV3B8B5yMnSjmp+42gt
o2SYzqM=
-----END CERTIFICATE-----
connectionConfiguration: ManualРекомендуется задавать переменную caCert. Если она не задана, будет использовано содержимое системного ca-certificates.
Подготовка тестового окружения
Для выполнения дальнейших команд необходим адрес и токен с правами root от Stronghold.
Такой токен можно получить во время инициализации нового хранилища секретов.
Далее в командах подразумевается, что эти настройки заданы в переменных окружения:
export VAULT_TOKEN=xxxxxxxxxxx
export VAULT_ADDR=https://secretstoreexample.comВ этом разделе приведены два варианта команд с примерами:
- команды с использованием 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, запуская оригинальную команду.
Если в манифесте пода у контейнера отсутствует команда запуска, выполняется извлечение манифеста образа из хранилища образов контейнеров, и команда извлекается из него.
Для получения манифеста из приватного хранилища образов используются заданные в манифесте пода учетные данные из 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 |
Список имен контейнеров через пробел, к которым не будет применяться инжектирование |
Используя инжектор, вы сможете задавать в манифестах пода вместо значений 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Шаблон может также находиться в ConfigMap или в Secret и быть подключен с помощью envFrom:
envFrom:
- secretRef:
name: app-secret-env
- configMapRef:
name: app-envИнжектирование реальных секретов из Vault-совместимого хранилища выполняется только на этапе запуска приложения. В Secret и ConfigMap будут находиться шаблоны.
Подключение переменных из ветки хранилища
В этом сценарии подключаются все ключи одного секрета.
-
Создайте под с именем
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"