Добрый день, Хабр! Мы — Михаил Панов и Евгений Прудченко, 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)


  1. cthtuf
    00.00.0000 00:00
    +1

    Хорошая инструкция ????

    Есть такая неочевидная штука, о которой я бы упомянул - если вы захотите использовать только переменные окружения, вместо файлов, то вам все равно нужно будет замаунтить вольюм в контейнер (указать его в секции volumeMounts пода), иначе переменные окружения не появятся.


    1. eapdark
      00.00.0000 00:00

      да все верно первым делом маунтим а потом уже переменная .


  1. bordakovskiy
    00.00.0000 00:00
    +1

    Хорошая статья. Есть vault-bank от banzaicloud, сразу можно монтировать все секреты в env. Но через сайдкар.


    1. Stok1y Автор
      00.00.0000 00:00

      Хороший инструмент, тоже его используем.


  1. agat_khafizov
    00.00.0000 00:00

    Статья хорошая, но жаль что в кубернетес создается сущность секрета (в вашем случае "dbpass") и этот секрет остается в кодировке base64.


  1. akelsey
    00.00.0000 00:00

    Мы используем external secret operator (https://external-secrets.io) который делает вроде бы то что нужно и вам, синхронизирует пароли из hashicorp vault в секреты кубера. Вы его не рассматривали или отмели потому что он чем то не подошёл?


    1. Stok1y Автор
      00.00.0000 00:00

      Мы его не рассматривали, так как Hashicorp уже предоставляет решение которое выполняет все необходимые функции.
      Тут вопрос функционала, этим инструментом можно сразу закрыть несколько платформ(aws,gcp,k8s). А у нас Kubernetes as is, поэтому Vault Provider вполне подходит.