Добрый день, Хабр! Мы — Михаил Панов и Евгений Прудченко, DevOps‑инженеры из команды МТС Digital, работаем на проекте External WebSSO. Мы занимаемся внедрением DevOps практик и инструментов в рамках нашего проекта. В этой статье расскажем о интеграции и доставке секретов из Vault в Kubernetes с помощью Vault CSI Provider.
Изучив вопрос доставки секретов, мы выяснили, что мало кто использует Vault CSI Provider. Нам это показалось несправедливым, ведь, на наш взгляд, это отличный инструмент. Поэтому мы и решили поделиться нашим опытом.
Основная проблема которую хотелось решить — как получить секреты из Vault, меняя всего лишь несколько строк в файле values.yaml нашего helm‑chart. Задача грандиозная, поэтому нам пришлось пройти длинный путь к ее решению.
Часть 1: Выбор и сравнение функционала (CSI Provider vs Sidecar Agent Injector)
Так как мы определились с выбором хранилища секретов, нам осталось определиться со способом доставки секретов в pods. Выбирали между CSI Provider и Sidecar Agent Injector. Оба официально поддерживаются Hashicorp и интегрируются с Kubernetes.
Чтобы выбрать, мы сравнили возможности, функционал и гибкость представленных решений.
1. Sidecar Agent Injector
Мы сразу определили для себя некоторые минусы этого подхода, а именно:
дополнительный sidecar для каждого pod — он будет дополнительно «нагружать наш кластер»;
Sidecar Agent Injector не поддерживает преобразование секретов Vault в секреты Kubernetes, что требовалось в нашем случае.
2. CSI Provider
Плюсы:
разворачивается как Daemonset;
позволяет монтировать секреты в файловую систему pod;
драйвер Vault CSI Provider поддерживает преобразование секретов Vault в секреты и переменные среды Kubernetes.
Минусы:
Vault CSI Provider не поддерживает ротацию и извлечение секретов при их изменении в Vault. Но об этом расскажем в следующем пункте.
Часть 2: Проблемы, с которыми мы столкнулись, и их решения
Проблема 1: мы не добавляли ротацию секретов, поэтому получить секреты можно только при деплое.
Решение — чтобы секреты обновлялись по мере их добавления/обновления можно использовать:
[Reloader](https://github.com/stakater/Reloader);
Hash для Helm, но тут есть трудности с тем, что секрет создается после деплоя и нужно добавлять аннотацию уже на задеплоенное приложение.
Проблема 2: переполнение кластера Vault незавершенными арендами и дальнейшая его недоступность.
Это связано с тем, что на каждый запрос секрета CSI Driver создает новую аренду в методе аутентификации kubernetes/, а так как по умолчанию ее ttl составляет 60 минут, кластер переполняется очень быстро — за 1–3 дня.
Решение — установка default‑lease‑ttl для всего метода аутентификации kubernetes/ в 30 секунд. Этого достаточно, чтобы получить секрет и не захламлять кластер.
Часть 3: Реализация
Этот этап — самый простой.
Помните — пример ниже категорически не рекомендован к установке в «продуктовом» контуре.
1. Прежде всего необходимо развернуть в кластере Kubernetes Vault и Vault CSI Provider:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm install vault hashicorp/vault \
--set "server.dev.enabled=true" \
--set "injector.enabled=false" \
--set "csi.enabled=true"
2. Переходим к установке secret store csi driver:
При установке secret store csi driver необходимо добавить пару «ключей», а именно:
включить ротацию секретов enableSecretRotation для того, чтобы получать секреты из Vault при их изменении;
добавить интервал ротации этих секретов rotationPollInterval — как часто secret store csi driver будет проверять изменения секрета.
helm repo add secrets-store-csi-driver https://kubernetes-sigs.github.io/secrets-store-csi-driver/charts
helm install csi secrets-store-csi-driver/secrets-store-csi-driver \
--set syncSecret.enabled=true \
--set enableSecretRotation=true \
--set "rotationPollInterval=30s"
3. Настраиваем авторизацию Kubernetes и Default lease TTL:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host="https://$KUBERNETES_PORT_443_TCP_ADDR:443"
Командой ниже выставляем Default-lease-ttl, для того чтобы наш Vault не заполнялся ненужными «арендами».
vault auth tune -default-lease-ttl=30s kubernetes/
4. Добавляем Роль, Политику и Секрет.
Теперь добавим тестовые данные, с которыми будем работать.
Роль:
vault write auth/kubernetes/role/database \
bound_service_account_names=webapp-sa \
bound_service_account_namespaces=default \
policies=internal-app
Политика:
vault policy write internal-app - <<EOF
path "secret/data/test-pass" {
capabilities = ["read"]
}
Секрет:
vault kv put secret/test-pass test-password="db-secret-password"
5. Тестирование.
Проверим что все pod «стартанули»:
kubectl get pods
Создадим Custom Resource:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
spec:
provider: vault
parameters:
vaultAddress: "http://vault.default:8200"
roleName: "database"
objects: |
- objectName: "test-secret"
secretPath: "secret/data/test-pass"
secretKey: "test-password"
Создадим Serviceaccount:
kubectl create serviceaccount webapp-sa
Создадим pod который будет запрашивать секрет из Vault:
kind: Pod
apiVersion: v1
metadata:
name: webapp
spec:
serviceAccountName: webapp-sa
containers:
- image: jweissig/app:0.0.1
name: webapp
volumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database"
Проверим секрет внутри нашего pod:
kubectl exec webapp -- cat /mnt/secrets-store/test-secret
Тут мы должны получить вывод: db‑secret‑password.
Теперь смонтируем секрет как переменную среды.
Добавим еще один CustomResource:
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-database
spec:
provider: vault
secretObjects:
- data:
- key: test-password
objectName: test-password
secretName: dbpass
type: Opaque
parameters:
vaultAddress: "http://vault.default:8200"
roleName: "database"
objects: |
- objectName: "test-password"
secretPath: "secret/data/test-pass"
secretKey: "test-password"
Создадим pod, который будет запрашивать секрет из Vault и монтировать как переменную среды.
kind: Pod
apiVersion: v1
metadata:
name: webapp-env
spec:
serviceAccountName: webapp-sa
containers:
- image: jweissig/app:0.0.1
name: webapp
env:
- name: DB_PASSWORD
valueFrom:
secretKeyRef:
name: dbpass
key: test-password
volumeMounts:
- name: secrets-store-inline
mountPath: "/mnt/secrets-store"
readOnly: true
volumes:
- name: secrets-store-inline
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: "vault-database"
На этом установка, настройка и тестирование закончены.
Вуаля, теперь мы можем полноценно добавлять секреты в Vault и использовать их в Kubernetes как:
Secret;
монтировать как volume в pod и использовать как конфиг‑файл.
Итог: инструмент не тривиальный и требует ряда доработок для каждого частного случая, но дает хорошую автоматизацию доставки секретов из Vault.
Спасибо за уделенное статье время, надеемся, что эта информация пригодится вам. Кстати, если у вас есть свой опыт работы с Vault CSI Driver — обязательно расскажите о нем в комментариях!
Комментарии (7)
bordakovskiy
00.00.0000 00:00+1Хорошая статья. Есть vault-bank от banzaicloud, сразу можно монтировать все секреты в env. Но через сайдкар.
agat_khafizov
00.00.0000 00:00Статья хорошая, но жаль что в кубернетес создается сущность секрета (в вашем случае "
dbpass
") и этот секрет остается в кодировке base64.
akelsey
00.00.0000 00:00Мы используем external secret operator (https://external-secrets.io) который делает вроде бы то что нужно и вам, синхронизирует пароли из hashicorp vault в секреты кубера. Вы его не рассматривали или отмели потому что он чем то не подошёл?
Stok1y Автор
00.00.0000 00:00Мы его не рассматривали, так как Hashicorp уже предоставляет решение которое выполняет все необходимые функции.
Тут вопрос функционала, этим инструментом можно сразу закрыть несколько платформ(aws,gcp,k8s). А у нас Kubernetes as is, поэтому Vault Provider вполне подходит.
cthtuf
Хорошая инструкция ????
Есть такая неочевидная штука, о которой я бы упомянул - если вы захотите использовать только переменные окружения, вместо файлов, то вам все равно нужно будет замаунтить вольюм в контейнер (указать его в секции volumeMounts пода), иначе переменные окружения не появятся.
eapdark
да все верно первым делом маунтим а потом уже переменная .