Всем привет! Меня зовут Сергей Смирнов, я DevOps экосистемы «Своё» в РСХБ-Интех (технологическая дочка Россельхозбанка).
Сегодня я расскажу вам об одном из способов доставки секретов в kubernetes из Hashicorp Vault.

Hashicorp Vault уже довольное время на слуху и зарекомендовал как надежная система хранения конфиденциальной информации(секретов): пароли, ssh ключи, api токены, сертификатов и пр.

Hashicorp Vault поможет решить сразу несколько задач:

• Создать единственное хранилище секретов;
• Защитить от утечки секретов;
• Сделать работу с секретами для пользователей удобной.

На текущий момент существует довольно много интергации Hashicorp Vault с популярными системами CI/CD, облачными провайдерами, системами оркестрации/контейнеризации

Мы в работе активно используем kubernetes. Основной задачей было найти решение, чтобы получить на выходе секрет kubernetes.
Перепробовав несколько способов доставить секреты в kubernetes используя Hashicorp Vault, мы остановились на External Secrets Operator.

Пробовали:

  • Средствами Hashicorp Vault. Vault Agent монтирует директорию с секретами через init или sidecar контейнеры. Не подходит, т.к. возможна лишняя нагрузка на кластер при создании лишних подов. Плюс не умеют преобразовать секреты Vault в секреты Kubernetes, а сохраняет секреты в файлы.

  • Bank Vault Не подходит, сохраняет секреты в переменные окружения.

  • ArgoCD Vault Plugin Умеет работать с секретами kubernetes, но показался сложным в настройке.

External Secrets Operator простыми словами - это оператор для kubernetes, который автоматически синхронизирует секреты из внешних API и создает их в kubernetes. Если секрет во внешнем API изменяется, контроллер обновляет секреты.

С точки зрения безопасности kubernetes есть пара моментов:

  • Необходимо настраивать RBAC в kubernetes, чтобы исключить доступ до ресурса secrets. Для того, чтобы пользователь не смог прочитать секрет.

  • Если есть необходимость, ограничить доступ к кластеру. Если у пользователя есть доступ к поду, он сможет посмотреть переменные/значение секрета через терминал или логи пода.

Со стороны Hashicorp Vault все зашифровано. Чтобы получить доступ к api, необходимо владеть соответствующими привилегиями для авторизации.
Плюс при рестарте кластер запечатывается, если конечно не настроено автораспечатывание кластера(autounseal) транзитными ключами. Для ручного распечатывания по умолчанию требуются 3 из 5 ключей(unseal keys). В идеале ключи должны храниться у разных людей.

Реализация по этапам


Что потребуется для реализации?

  • Кластер kubernetes версии минимум 1.22+ согласно документации

  • Helm версии 3.6+ согласно документации

Этап 1

1. Установка vault

Добавляем репозиторий hashicorp и устанавливаем vault. В качестве бекенда Integrated Storage (Raft).

Integrated Storage (Raft) работает таким образом, что все узлы в кластере Vault будут иметь реплицированную копию данных Vault. Integrated Storage Raft официально поддерживается Hashicorp.

Бекап можно запускать на любом из узлов.
Команды довольно простые:

  • vault operator raft snapshot save new.snapshot

  • vault operator raft snapshot restore new.snapshot

helm repo add hashicorp https://helm.releases.hashicorp.com

helm install vault hashicorp/vault \
  --set='server.ha.enabled=true' \
  --set='server.ha.raft.enabled=true' \
  -n vault \
  --create-namespace 

Проверяем, что все поды запустились:

kubectl get po -n vault

NAME                                               READY   STATUS    RESTARTS   AGE
vault-0                                             1/1     Running   0          1d
vault-1                                             1/1     Running   0          1d
vault-2                                             1/1     Running   0          1d

2. Инициализация и распечатывание кластера Hashicorp Vault

Инициализируем vault

kubectl exec -ti vault-0 -- vault operator init

После инициализации vault получим корневой ключ и 5 ключей для распечатки.

Unseal Key 1: wQRU1yjL88Plb3rPSQPUfLw1KOCsPMACpXLY0Ixbdhfg
Unseal Key 2: ai3NajyVDFyqG5Lz6pDYNX118ti9Slqo2vueQhtg6Usq
Unseal Key 3: x1a0VneA8cBkcXMDkxPwOOByPzlwuUw3dNaa7hfUqDAx
Unseal Key 4: oztkcLGBAesVQwyO7Kc059xlqq9YSh1vkEkQFzKlnwae
Unseal Key 5: 5cVmmDVZ7BIbQolCQdCoUXhdTRojPD2rgE1t83QgRKNn
 
Initial Root Token: s.lTEYiTAv63CLsf0FqBcS672x                     

Распечатаем кластер. По умолчанию используются 3 unseal ключа.

kubectl exec -ti vault-0 -- vault operator unseal

После присоединим оставшиеся 2 пода к кластеру.

kubectl exec -ti vault-1 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -ti vault-1 -- vault operator unseal
kubectl exec -ti vault-2 -- vault operator raft join http://vault-0.vault-internal:8200
kubectl exec -ti vault-2 -- vault operator unseal

После распечатки кластера авторизируемся в vault используя корневой ключ(Initial Root Token).

kubectl exec -ti vault-0 -- vault login

Проверим состояние кластера

kubectl exec -ti vault-0 -- vault operator raft list-peers

Node                                    Address                        State       Voter
----                                    -------                        -----       -----
4rzii8af-8847-7f28-23f0-p36vwkghqxng    vault-0.vault-internal:8201    leader      true
aydfyon3-6b3x-1b7x-9b34-bdzpa1el2dzf    vault-1.vault-internal:8201     follower   true
2tjh4iqi-8bcv-8b2n-1b7s-wj6wtxgOk5cs     vault-2.vault-internal:8201    follower    true

3. Создание политики на чтение секретов из Hashicorp Vault

Мы используем вторую версию KV хранилища, поэтому путь до секрета будет /projects/data/ и /projects/metadata.

Одно из отличий между версиями, что вторая поддерживает версионирование.

Плюс выдадим права на обновление токена.

vault policy write read-secret - <<EOF
path "/projects/data/dev/*" {
    capabilities = ["read", "list"]
}

path "/projects/metadata/dev/*" {
    capabilities = ["read", "list"]
}

path "auth/token/renew-self" {
    capabilities = ["update"]
}
EOF

4. Настройка авторизации kubernetes

vault auth enable kubernetes

vault write auth/kubernetes/config \
kubernetes_host=https://${KUBERNETES_PORT_443_TCP_ADDR}:443 \
issuer="https://kubernetes.default.svc.cluster.local"

создадим роль

vault write auth/kubernetes/role/read-secret \
bound_service_account_names=vault-auth \
bound_service_account_namespaces=vault \
policies=read-secret \
alias_name_source=serviceaccount_name \
ttl=1h

создадим service account

kubectl create serviceaccount vault-auth -n vault

6. Создадим секрет

vault kv put projects/dev/app user=admin password=123456
==== Secret Path ====
projects/data/dev-app

======= Metadata =======
Key                Value
---                -----
created_time       2023-08-18T10:28:38.163062633Z
custom_metadata    <nil>
deletion_time      n/a
destroyed          false
version            1

Этап 2

1. Установка External Secrets Operator

helm repo add external-secrets https://charts.external-secrets.io

helm install external-secrets \
   external-secrets/external-secrets \
   -n vault \
   --create-namespace 

Проверяем, что все поды запустились:

kubectl get po -n vault

NAME                                                READY   STATUS    RESTARTS   AGE
external-secrets-h4ls02c0wd-1cxyr                   1/1     Running   0          58d
external-secrets-cert-controller-h4lfd8bfzg-km8ez   1/1     Running   0          58d
external-secrets-webhook-su3x3fOjk-uxsy2            1/1     Running   0          58d

2. Настройка External Secrets Operator

External Secrets Operator - набор custom resources таких как: ExternalSecret, SecretStore и ClusterSecretStore.

Мы рассмотрим только ExternalSecret и SecretStore, т.к. для каждого пространства имен(namespace) свои секреты. ClusterSecretStore будет доступен из любого пространства имен(namespace).

SecretStore указывает как получить доступ.

ExternalSecret указывает какие данные нужно извлечь.

Создадим манифесты:

apiVersion: external-secrets.io/v1beta1
kind: SecretStore
metadata:
  name: app
  namespace: dev-app
spec:
  provider:
    vault:
      server: "http://vault.vault:8200" # адрес нашего vault. Складывается из имени сервиса и пространства имен.
      path: "projects" # имя kv
      version: "v2" # версия kv
      auth:
        kubernetes: # метод авторизации
          mountPath: "kubernetes"
          role: "read-secret" # роль для авторизации
          serviceAccountRef:
            name: "vault-auth" # имя нашего serviceAccount
---
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
  name: app
  namespace: dev-app
spec:
  refreshInterval: "15s"
  secretStoreRef:
    name: app # имя SecretStore
    kind: SecretStore
  target:
    name: vault-secrets # имя будущего секрета kubernetes
  data:
    - secretKey: user # ключ секрета
      remoteRef:
        key: dev/app # путь до секрета в vault
        property: user # ключ секрета в vault
    - secretKey: password
      remoteRef:
        key: dev/app
        property: password

Проверим, что все создалось

kubectl get externalsecrets -n vault
NAME                                  AGE   STATUS   CAPABILITIES   READY
secretstore.external-secrets.io/app   14m   Valid    ReadWrite      True

NAME                                     STORE   REFRESH INTERVAL   STATUS         READY
externalsecret.external-secrets.io/app   app     15s                SecretSynced   True

Проверим наш созданный секрет

kubectl describe secrets -n vault vault-secrets
Name:         vault-secrets
Namespace:    vault
Labels:       <none>
Annotations:  created-by: system:serviceaccount:vault:external-secrets
              reconcile.external-secrets.io/data-hash: b749f0dfb88266e3b81d63dbc2a4402b

Type:  Opaque

Data
====
password:  6 bytes
user:      5 bytes
kubectl get secrets/vault-secrets --template={{.data.user}} | base64 -D
admin
kubectl get secrets/vault-secrets --template={{.data.password}} | base64 -D
123456

Подведем небольшой итог. Этот инструмент хорошо решает задачу автоматической синхронизации секретов между Vault и kubernetes. Также ESO поддерживает другие системы управления секретами, такие как AWS Secrets Manager, GCP Secrets Manager, Yandex Lockbox, что упрощает работу. Предлагаю Вам его оценить и написать комментарий под этим постом.

Спасибо за уделенное время! Надеюсь, что эта информация будет полезной для вашей работы и общего развития. Кстати, у нас есть helm чарт для создания secret-store и external-secrets. Мы используем в связке с Argo CD, который очень нам пришёлся кстати. Может и вас он заинтересует, переходите по ссылке на github.

Комментарии (12)


  1. Adel-S
    08.09.2023 14:52
    +1

    Мы в своё время тоже столкнулись с данным вопросом - т.к. тоже используем Vault + k8s (точнее GKE, но там различия минимальны), перепробовали разные варианты, но все показались небезопасными, так что в итоге написали своё решение - Vault Secret Fetcher, который действует более секьюрным образом - загрузившись через sidecar init container предоставляет секреты как environment variables доступные только приложению. Т.е. даже имея доступ к поду и проломившись в контейнер не получится сдампить env vars.


    1. AlexGluck
      08.09.2023 14:52
      +4

      xargs --null --max-args=1 echo < /proc/<pid>/environ
      А если так?


      1. Inlore
        08.09.2023 14:52
        +1

        Так переменные будет видно

        При вызове syscall.Exec, который вызывает glibc'шный execve, данные в /proc/<pid>/ обновляются под новую вызванную программу, в том числе обновляется /proc/<pid>/environ

        Пруф

        1) 2 простые программы на go:
        - exec будет устанавливать переменную окружения FOO и вызывать другую программу через syscall.Exec
        - sleep будет просто спать

        2) Запускаем в контейнере и "проваливаемся" туда

        docker run --rm -it --name envvar -u 2000:2000 -v $(pwd):/app ubuntu:20.04 /app/exec /app/sleep
        docker exec -it envvar bash

        3) Проверяем

        А вот если установить переменную окружения через os.Setenv и не вызывать другую программу, а продолжать работать в текущей, то значение такой переменной придётся уже искать в памяти с помощью какого-нибудь gdb


        1. AlexGluck
          08.09.2023 14:52

          Получается состояние гонки, секретные данные всё равно доступны какое то время и поверхность атаки не закрыта, а уменьшена.


    1. smirnov-sa Автор
      08.09.2023 14:52

      Спасибо Вам за полезный комментарий!


  1. sloniki
    08.09.2023 14:52
    +2

    С недавних пор HashiCorp предлагает родной оператор https://github.com/hashicorp/vault-secrets-operator


    1. smirnov-sa Автор
      08.09.2023 14:52
      +1

      Вы им пользовались? Если да, то поделитесь, пожалуйста, его плюсами и минусами.


      1. sloniki
        08.09.2023 14:52
        +1

        Я только установил и проверил, что работает. Глубокого тестирования не делал. Какое-то сравнение видел здесь https://www.hashicorp.com/blog/kubernetes-vault-integration-via-sidecar-agent-injector-vs-csi-provider


        1. smirnov-sa Автор
          08.09.2023 14:52

          Понял, в любом случае, спасибо! Возьмем на заметку.


  1. citius
    08.09.2023 14:52
    +1

    Bank vaults умеет мутировать также и секреты. И даже конфигмапы (ну вдруг).


    1. smirnov-sa Автор
      08.09.2023 14:52
      +1

      Спасибо за комментарий. Да, знаем.

      Тем не менее ESO нам показался более простым в настройке и выполняет все необходимые функции.


  1. Airee
    08.09.2023 14:52

    А CSI чем не мил?