Всем привет! Меня зовут Сергей Смирнов, я 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)
sloniki
08.09.2023 14:52+2С недавних пор HashiCorp предлагает родной оператор https://github.com/hashicorp/vault-secrets-operator
smirnov-sa Автор
08.09.2023 14:52+1Вы им пользовались? Если да, то поделитесь, пожалуйста, его плюсами и минусами.
sloniki
08.09.2023 14:52+1Я только установил и проверил, что работает. Глубокого тестирования не делал. Какое-то сравнение видел здесь https://www.hashicorp.com/blog/kubernetes-vault-integration-via-sidecar-agent-injector-vs-csi-provider
citius
08.09.2023 14:52+1Bank vaults умеет мутировать также и секреты. И даже конфигмапы (ну вдруг).
smirnov-sa Автор
08.09.2023 14:52+1Спасибо за комментарий. Да, знаем.
Тем не менее ESO нам показался более простым в настройке и выполняет все необходимые функции.
Adel-S
Мы в своё время тоже столкнулись с данным вопросом - т.к. тоже используем Vault + k8s (точнее GKE, но там различия минимальны), перепробовали разные варианты, но все показались небезопасными, так что в итоге написали своё решение - Vault Secret Fetcher, который действует более секьюрным образом - загрузившись через sidecar init container предоставляет секреты как environment variables доступные только приложению. Т.е. даже имея доступ к поду и проломившись в контейнер не получится сдампить env vars.
AlexGluck
xargs --null --max-args=1 echo < /proc/<pid>/environ
А если так?
Inlore
Так переменные будет видно
При вызове syscall.Exec, который вызывает glibc'шный execve, данные в /proc/<pid>/ обновляются под новую вызванную программу, в том числе обновляется /proc/<pid>/environ
Пруф
1) 2 простые программы на go:
- exec будет устанавливать переменную окружения FOO и вызывать другую программу через syscall.Exec
- sleep будет просто спать
2) Запускаем в контейнере и "проваливаемся" туда
3) Проверяем
А вот если установить переменную окружения через os.Setenv и не вызывать другую программу, а продолжать работать в текущей, то значение такой переменной придётся уже искать в памяти с помощью какого-нибудь gdb
AlexGluck
Получается состояние гонки, секретные данные всё равно доступны какое то время и поверхность атаки не закрыта, а уменьшена.
smirnov-sa Автор
Спасибо Вам за полезный комментарий!