Как настроить HashiCorp Vault в качестве secret store для использования с модулем secrets-store-integration?

Для выполнения дальнейших команд необходим адрес и токен с правами root от Vault. Такой токен можно получить во время инициализации нового secrets store.

Далее в командах будет подразумеваться что данные настойки указаны в переменных окружения.

export VAULT_TOKEN=xxxxxxxxxxx
export VAULT_ADDR=https://secretstoreexample.com

В этом руководстве мы приводим два вида примерных команд:

  • команда с использованием консольной версии HashiCorp Vault (руководство по установке);
  • команда с использованием curl для выполнения прямых запросов в API secrets store.

В данном разделе в качестве примера приводятся настройки которые необходимо произвести, для того чтобы под сервиса мог получить доступ до секрета, расположенного в Key-Value хранилище. В качестве секрета будет рассмотрен пароль для базы данных который использует приложение на Python.

  • Включим и создадим Key-Value хранилище:

    vault 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
    
  • Зададим пароль базы в качестве значения секрета:

    vault kv put secret/database-for-python-app password="db-secret-password"
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request PUT \
      --data '{"data":{"password":"db-secret-password"}}' \
      ${VAULT_ADDR}/v1/secret/data/database-for-python-app
    
  • Проверим, правильно ли записался пароль:

    vault kv get secret/database-for-python-app
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      ${VAULT_ADDR}/v1/secret/data/database-for-python-app
    
  • Задаём путь аутентификации (authPath) и включаем аутентификацию и авторизацию в Vault с помощью Kubernetes API:

    vault auth enable -path=main-kube kubernetes
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request POST \
      --data '{"type":"kubernetes"}' \
      ${VAULT_ADDR}/v1/sys/auth/main-kube
    
  • Если требуется настроить доступ для более чем одного кластера, то задаём путь аутентификации (authPath) и включаем аутентификацию и авторизацию в Vault с помощью Kubernetes API для каждого кластера кластера:

    vault auth enable -path=secondary-kube kubernetes
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request POST \
      --data '{"type":"kubernetes"}' \
      ${VAULT_ADDR}/v1/sys/auth/secondary-kube
    
  • Задаём адрес Kubernetes API для каждого кластера:

    vault write auth/main-kube/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/main-kube/config
    

    Для другого кластера:

    vault write auth/secondary-kube/config \
      kubernetes_host="https://10.11.12.10:443"
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request PUT \
      --data '{"kubernetes_host":"https://10.11.12.10:443"}' \
      ${VAULT_ADDR}/v1/auth/secondary-kube/config
    
  • Создаём в Vault политику с названием «backend», разрешающую чтение секрета database-for-python-app:

    vault policy write backend - <<EOF
    path "secret/data/database-for-python-app" {
      capabilities = ["read"]
    }
    EOF
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request PUT \
      --data '{"policy":"path \"secret/data/database-for-python-app\" {\n capabilities = [\"read\"]\n}\n"}' \
      ${VAULT_ADDR}/v1/sys/policies/acl/backend
    
  • Создаём роль, состоящую из названия пространства имён и приложения. Связываем её с ServiceAccount «backend-sa» из пространства имён «my-namespace1» и политикой «backend»:

    Важно!
    Помимо настроек со стороны Vault, вы должны настроить разрешения авторизации используемых serviceAccount в кластере kubernetes.
    Подробности в разделе FAQ

    vault write auth/main-kube/role/my-namespace1_backend \
        bound_service_account_names=backend-sa \
        bound_service_account_namespaces=my-namespace1 \
        policies=backend \
        ttl=10m
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request PUT \
      --data '{"bound_service_account_names":"backend-sa","bound_service_account_namespaces":"my-namespace1","policies":"backend","ttl":"10m"}' \
      ${VAULT_ADDR}/v1/auth/main-kube/role/my-namespace1_backend
    
  • Повторяем то же самое для остальных кластеров, указав другой путь аутентификации:

    vault write auth/secondary-kube/role/my-namespace1_backend \
        bound_service_account_names=backend-sa \
        bound_service_account_namespaces=my-namespace1 \
        policies=backend \
        ttl=10m
    

    Команда с использованием curl:

    curl \
      --header "X-Vault-Token: ${VAULT_TOKEN}" \
      --request PUT \
      --data '{"bound_service_account_names":"backend-sa","bound_service_account_namespaces":"my-namespace1","policies":"backend","ttl":"10m"}' \
      ${VAULT_ADDR}/v1/auth/secondary-kube/role/my-namespace1_backend
    

    Важно!
    Рекомендованное значение TTL для токена Kubernetes составляет 10m.

Эти настройки позволяют любому поду из пространства имён my-namespace1 из обоих K8s-кластеров, который использует ServiceAccount backend-sa, аутентифицироваться и авторизоваться в Vault для чтения секретов согласно политике backend.

Как разрешить ServiceAccount авторизоваться в Vault?

Для авторизации в Vault pod k8s использует токен сгенерированный для своего ServiceAccount. Для того чтобы Vault мог проверить валидность предоставляемых данных ServiceAccount используемый сервисом, Vault должен иметь разрешение на действия get, list и watch для endpoints tokenreviews.authentication.k8s.io и subjectaccessreviews.authorization.k8s.io. Для этого также можно использовать clusterRole system:auth-delegator.

Vault может использовать различные авторизационные данные для осуществления запросов в API Kubernetes:

  1. Использовать токен приложения, которое пытается авторизоваться в Vault. В этом случае для каждому сервису авторизующейся в Vault требуется в используемом ServiceAccount иметь clusterRole system:auth-delegator (либо права на API представленные выше).
  2. Использовать статичный токен отдельно созданного специально для Vault ServiceAccount у которого имеются необходимые права. Настройка Vault для такого случая подробно описана в документации Vault.

Как использовать авторотацию секретов, примонтированных как файл в контейнер без его перезапуска?

Функция авторотации секретов в модуле secret-store-integration включена по умолчанию. Каждые две минуты модуль опрашивает Vault и синхронизирует секреты в примонтированном файле в случае его изменения.

Создадим ServiceAccount backend-sa

apiVersion: v1
kind: ServiceAccount
metadata:
  name: backend-sa
  namespace: my-namespace1

Пример CustomResource SecretStoreImport:

apiVersion: deckhouse.io/v1alpha1
kind: SecretsStoreImport
metadata:
  name: python-backend
  namespace: my-namespace1
spec:
  type: CSI
  role: my-namespace1_backend
  files:
    - name: "db-password"
      source:
        path: "secret/data/database-for-python-app"
        key: "password"

Пример Deployment backend, который использует указанный выше SecretStoreImport как том, чтоб доставить пароль от базы данных в файловую систему приложения:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
  namespace: my-namespace1
  labels:
    app: backend
spec:
  selector:
    matchLabels:
      app: backend
  template:
    metadata:
      labels:
        app: backend
    spec:
      serviceAccountName: backend-sa
      containers:
      - image: some/app:0.0.1
        name: backend
        volumeMounts:
        - name: secrets
          mountPath: "/mnt/secrets"
      volumes:
      - name: secrets
        csi:
          driver: secrets-store.csi.deckhouse.io
          volumeAttributes:
            secretsStoreImport: "python-backend"

После применения этих ресурсов будет запущен под с названием backend, внутри которого будет каталог /mnt/secrets с примонтированным внутрь томом secrets. Внутри каталога будет лежать файл db-password с паролем от базы данных из Vault.

Есть два варианта следить за изменениями файла с секретом в поде. Первый - следить за временем изменения примонтированного файла, реагируя на его изменение. Второй - использовать 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"