Стадия жизненного цикла модуля: General Availability
Настройка модуля для работы 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 иметь ClusterRole
system:auth-delegator(либо права на API, представленные выше). За примером обратитесь к документации Stronghold. -
Статичный токен созданного специально для Stronghold ServiceAccount, у которого имеются необходимые права — настройка Stronghold для такого случая подробно описана в документации Stronghold.
Инжектирование переменных окружения
Как работает инжектирование
При включении модуля в кластере появляется mutating-webhook, который при наличии у пода аннотации secrets-store.deckhouse.io/role изменяет манифест пода, добавляя туда инжектор. В измененном поде добавляется init-контейнер, который помещает из служебного образа собранный статически бинарный файл-инжектор в общую для всех контейнеров пода временную директорию. В остальных контейнерах оригинальные команды запуска заменяются на запуск файла-инжектора, который получает из Vault-совместимого хранилища необходимые данные, используя для подключения сервисный аккаунт приложения, помещает эти переменные в ENV процесса, после чего выполняет системный вызов execve, запуская оригинальную команду.
Если в манифесте пода у контейнера отсутствует команда запуска, то выполняется извлечение манифеста образа из registry, и команда извлекается из него.
Для получения манифеста из приватного хранилища образов используются заданные в манифесте пода учетные данные из 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: myapp command: - sh - -c - while cat /mnt/secrets/db-password; do echo; sleep 5; done name: backend 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.
Функция авторотации
Функция авторотации секретов в модуле secret-store-integration включена по умолчанию. Каждые две минуты модуль опрашивает Stronghold и синхронизирует секреты в примонтированном файле в случае его изменения.
Существует два способа отслеживания изменений файла с секретом в поде:
- Следить за временем изменения примонтированного файла, реагируя на его изменение.
- Использовать inotify API, который предоставляет механизм подписки на события файловой системы. Inotify является частью ядра Linux. При обнаружении изменений существует множество вариантов реагирования в зависимости от используемой архитектуры приложения и используемого языка программирования. Самый простой — заставить Kubernetes перезапустить под, перестав отвечать на liveness-пробу.
Пример использования inotify в приложении на Python с использованием пакета inotify:
#!/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 с применением пакета inotify:
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"
CLI-утилита d8 для команд Stronghold
Deckhouse CLI (d8) — это универсальный инструмент, необходимый для выполнения команд вида d8 stronghold в терминале.
Чтобы установить d8, воспользуйтесь одним из способов, описанных в документации CLI-утилиты.