Настройка модуля для работы c Deckhouse Stronghold
Для автоматической настройки работы модуля secrets-store-integration в связке с модулем Deckhouse Stronghold потребуется ранее включенный и настроенный Stronghold.
Далее достаточно применить следующий ресурс:
apiVersion: deckhouse.io/v1alpha1
kind: ModuleConfig
metadata:
name: secrets-store-integration
spec:
enabled: true
version: 1
Параметр connectionConfiguration можно опустить, поскольку он стоит в значении DiscoverLocalStronghold
по умолчанию.
Настройка модуля для работы с внешним хранилищем
Для работы модуля требуется предварительно настроенное хранилище секретов, совместимое с HashiCorp Vault. В хранилище предварительно должен быть настроен путь аутентификации. Пример настройки хранилища секретом в FAQ.
Чтобы убедиться, что каждый API запрос зашифрован, послан и отвечен правильным адресатом, потребуется валидный публичный сертификат Certificate Authority, который используется хранилищем секретов. Такой публичный сертификат CA в PEM-формате необходимо использовать в качестве переменной caCert
в конфигурации модуля.
Пример конфигурации модуля для использования Vault-совместимого хранилища секретов, запущенного по адресу «secretstoreexample.com» на TLS-порту по умолчанию - 443 TLS:
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-----
Крайне рекомендуется задавать переменную caCert
. Если она не задана, будет использовано содержимое системного ca-certificates.
Подготовка тестового окружения
Для выполнения дальнейших команд необходим адрес и токен с правами root от Stronghold. Такой токен можно получить во время инициализации нового secrets store.
Далее в командах будет подразумеваться что данные настойки указаны в переменных окружения.
export VAULT_TOKEN=xxxxxxxxxxx
export VAULT_ADDR=https://secretstoreexample.com
В этом руководстве мы приводим два вида примерных команд:
- команда с использованием консольной версии HashiCorp Vault (руководство по установке);
- команда с использованием curl для выполнения прямых запросов в API secrets store.
Для использования инструкций по инжектированию секретов из примеров ниже вам понадобится:
- Создать в Stronghold секрет типа kv2 по пути
secret/myapp
и поместить туда значенияDB_USER
иDB_PASS
. - При необходимости добавляем путь аутентификации (authPath) для аутентификации и авторизации в Stronghold с помощью Kubernetes API удалённого кластера
- Создать в Stronghold политику, разрешающую чтение секретов по пути
secret/myapp
. - Создать в Stronghold роль
my-namespace_backend
для сервис-аккаунтаmyapp
в неймспейсеmy-namespace
и привязать к ней созданную ранее политику. - Создать в кластере неймспейс
my-namespace
. - Создать в созданном неймспейсе сервис-аккаунт
myapp
.
Пример команд, с помощью которых можно подготовить окружение
-
Включим и создадим Key-Value хранилище:
stronghold secrets enable -path=secret -version=2 kv
Команда с использованием curl:
curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request POST \ --data '{"type":"kv","options":{"version":"2"}}' \ ${VAULT_ADDR}/v1/sys/mounts/secret
-
Зададим имя пользователя и пароль базы данных в качестве значения секрета:
stronghold kv put secret/myapp 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/secret/data/myapp
-
Проверим, правильно ли записались секреты:
stronghold kv get secret/myapp
Команда с использованием curl:
curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ ${VAULT_ADDR}/v1/secret/data/myapp
-
По умолчанию метод аутентификации в Stronghold через Kubernetes API кластера, на котором запущен сам Stronghold, – включён и настроен под именем
kubernetes_local
. Если требуется настроить доступ через удалённые кластера, задаём путь аутентификации (authPath
) и включаем аутентификацию и авторизацию в Stronghold с помощью Kubernetes API для каждого кластера: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 для каждого кластера:
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 политику с названием
backend
, разрешающую чтение секретаmyapp
:stronghold policy write backend - <<EOF path "secret/data/myapp" { capabilities = ["read"] } EOF
Команда с использованием curl:
curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"policy":"path \"secret/data/myapp\" {\n capabilities = [\"read\"]\n}\n"}' \ ${VAULT_ADDR}/v1/sys/policies/acl/backend
-
Создаём роль, состоящую из названия пространства имён и политики. Связываем её с ServiceAccount
myapp
из пространства имёнmy-namespace
и политикойbackend
:Важно!
Помимо настроек со стороны Stronghold, вы должны настроить разрешения авторизации используемыхserviceAccount
в кластере kubernetes.
Подробности в пункте нижеstronghold write auth/kubernetes_local/role/my-namespace_backend \ bound_service_account_names=myapp \ bound_service_account_namespaces=my-namespace \ policies=backend \ ttl=10m
Команда с использованием curl:
curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp","bound_service_account_namespaces":"my-namespace","policies":"backend","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/kubernetes_local/role/my-namespace_backend
-
Повторяем то же самое для остальных кластеров, указав другой путь аутентификации:
stronghold write auth/remote-kube-1/role/my-namespace_backend \ bound_service_account_names=myapp \ bound_service_account_namespaces=my-namespace \ policies=backend \ ttl=10m
Команда с использованием curl:
curl \ --header "X-Vault-Token: ${VAULT_TOKEN}" \ --request PUT \ --data '{"bound_service_account_names":"myapp","bound_service_account_namespaces":"my-namespace","policies":"backend","ttl":"10m"}' \ ${VAULT_ADDR}/v1/auth/remote-kube-1/role/my-namespace_backend
Важно!
Рекомендованное значение TTL для токена Kubernetes составляет 10m.
Эти настройки позволяют любому поду из пространства имён my-namespace
из обоих K8s-кластеров, который использует ServiceAccount myapp
, аутентифицироваться и авторизоваться в Stronghold для чтения секретов согласно политике backend
.
- Создадим namespace и ServiceAccount в указанном namespace:
kubectl create namespace my-namespace kubectl -n my-namespace create serviceaccount myapp
Как разрешить 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:
- Использовать токен приложения, которое пытается авторизоваться в Stronghold. В этом случае для каждого сервиса авторизующейся в Stronghold требуется в используемом ServiceAccount иметь clusterRole
system:auth-delegator
(либо права на API представленные выше). - Использовать статичный токен отдельно созданного специально для 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 |
Используя инжектор вы сможете задавать в манифестах пода вместо значений env-шаблоны, которые будут заменяться на этапе запуска контейнера на значения из хранилища.
Пример: извлечь из Vault-совместимого хранилища ключ mypassword
из kv2-секрета по адресу secret/myapp
:
env:
- name: PASSWORD
value: secrets-store:secret/data/myapp#mypassword
Пример: извлечь из Vault-совместимого хранилища ключ mypassword
версии 4
из kv2-секрета по адресу secret/myapp
:
env:
- name: PASSWORD
value: secrets-store:secret/data/myapp#mypassword#4
Шаблон может также находиться в ConfigMap или в Secret и быть подключен с помощью envFrom
envFrom:
- secretRef:
name: app-secret-env
- configMapRef:
name: app-env
Инжектирование реальных секретов из Vault-совместимого хранилища выполнится только на этапе запуска приложения, в Secret и ConfigMap будут находиться шаблоны.
Подключение переменных из ветки хранилища (всех ключей одного секрета)
Создадим под с названием myapp1
, который подключит все переменные из хранилища по пути secret/data/myapp
:
kind: Pod
apiVersion: v1
metadata:
name: myapp1
namespace: my-namespace
annotations:
secrets-store.deckhouse.io/role: "my-namespace_backend"
secrets-store.deckhouse.io/env-from-path: secret/data/myapp
spec:
serviceAccountName: myapp
containers:
- image: alpine:3.20
name: myapp
command:
- sh
- -c
- while printenv; do sleep 5; done
Применим его:
kubectl create --filename myapp1.yaml
Проверим логи пода после его запуска, мы должны увидеть все переменные из secret/data/myapp
:
kubectl -n my-namespace logs myapp1
Удалим под
kubectl -n my-namespace delete pod myapp1 --force
Подключение явно заданных переменных из хранилища
Создадим тестовый под с названием myapp2
, который подключит требуемые переменные из хранилища по шаблону:
kind: Pod
apiVersion: v1
metadata:
name: myapp2
namespace: my-namespace
annotations:
secrets-store.deckhouse.io/role: "my-namespace_backend"
spec:
serviceAccountName: myapp
containers:
- image: alpine:3.20
env:
- name: DB_USER
value: secrets-store:secret/data/myapp#DB_USER
- name: DB_PASS
value: secrets-store:secret/data/myapp#DB_PASS
name: myapp
command:
- sh
- -c
- while printenv; do sleep 5; done
Применим его:
kubectl create --filename myapp2.yaml
Проверим логи пода после его запуска, мы должны увидеть переменные из secret/data/myapp
:
kubectl -n my-namespace logs myapp2
Удалим под
kubectl -n my-namespace delete pod myapp2 --force
Монтирование секрета из хранилища в качестве файла в контейнер
Для доставки секретов в приложение нужно использовать CustomResource “SecretStoreImport”.
В этом примере используем уже созданные ServiceAccount myapp
и namespace my-namespace
из шага Подготовка тестового окружения
Создайте в кластере CustomResource SecretsStoreImport с названием “myapp”:
apiVersion: deckhouse.io/v1alpha1
kind: SecretsStoreImport
metadata:
name: myapp-ssi
namespace: my-namespace
spec:
type: CSI
role: my-namespace_backend
files:
- name: "db-password"
source:
path: "secret/data/myapp"
key: "DB_PASS"
Создайте в кластере тестовый под с названием myapp3
, который подключит требуемые переменные из хранилища в виде файла:
kind: Pod
apiVersion: v1
metadata:
name: myapp3
namespace: my-namespace
spec:
serviceAccountName: myapp
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
с паролем от базы данных из Stronghold.
Проверьте логи пода после его запуска (должно выводиться содержимое файла /mnt/secrets/db-password
):
kubectl -n my-namespace logs myapp3
Удалите под:
kubectl -n my-namespace delete pod myapp3 --force
Функция авторотации
Функция авторотации секретов в модуле secret-store-integration включена по умолчанию. Каждые две минуты модуль опрашивает Stronghold и синхронизирует секреты в примонтированном файле в случае его изменения.
Есть два варианта следить за изменениями файла с секретом в поде. Первый - следить за временем изменения примонтированного файла, реагируя на его изменение. Второй - использовать inotify API, который предоставляет механизм для подписки на события файловой системы. Inotify является частью ядра Linux. После обнаружения изменений есть большое количество вариантов реагирования на событие изменения в зависимости от используемой архитектуры приложения и используемого языка программирования. Самый простой — заставить K8s перезапустить под, перестав отвечать на 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"