Настройка модуля для работы 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
В этом руководстве мы приводим два вида примерных команд:
- команда с использованием консольной версии Stronghold (Как получить бинарный файл stronghold);
- команда с использованием curl для выполнения прямых запросов в API secrets store.
Для использования инструкций по инжектированию секретов из примеров ниже вам понадобится:
- Создать в Stronghold секрет типа kv2 по пути
demo-kv/myapp-secret
и поместить туда значенияDB_USER
иDB_PASS
. - При необходимости добавляем путь аутентификации (authPath) для аутентификации и авторизации в Stronghold с помощью Kubernetes API удалённого кластера
- Создать в Stronghold политику
myapp-ro-policy
, разрешающую чтение секретов по путиdemo-kv/myapp-secret
. - Создать в Stronghold роль
myapp-role
для сервис-аккаунтаmyapp-sa
в неймспейсеmyapp-namespace
и привязать к ней созданную ранее политику. - Создать в кластере неймспейс
myapp-namespace
. - Создать в созданном неймспейсе сервис-аккаунт
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:
- Использовать токен приложения, которое пытается авторизоваться в 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 |
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
Для скачивания мультитула:
-
Перейдите на страницу
tools..<cluster_domain>
, где<cluster_domain>
— DNS-имя в соответствии с шаблоном из параметра modules.publicDomainTemplate глобальной конфигурации. -
Выберите Deckhouse CLI для вашей операционной системы.
-
Для Linux и MacOS:
- Добавьте права на выполнение
d8
черезchmod +x d8
. - Переместите исполняемый файл в каталог
/usr/local/bin/
.
Для Windows:
- Распакуйте архив, переместите файл
d8.exe
в выбранный вами каталог и добавьте этот каталог в переменную $PATH операционной системы. - Разблокируйте файл
d8.exe
, например, следующим способом:- Щелкните правой кнопкой мыши на файле и выберите Свойства в контекстном меню.
- В окне Свойства убедитесь, что находитесь на вкладке Общие.
- Внизу вкладки Общие вы можете увидеть раздел Безопасность с сообщением о блокировке файла.
- Установите флажок Разблокировать или нажмите кнопку Разблокировать, затем нажмите Применить и ОК, чтобы сохранить изменения.
- Добавьте права на выполнение
-
Проверьте, что утилита работает:
1d8 help
Готово, вы установили d8 stronghold
.