Есть два типа Kubernetes-кластеров.
В первом люди хранят секреты в values.yaml, коммитят .env в Git и называют это “временным решением”.
Во втором - поднимают HashiCorp Vault, настраивают auto unseal, раздают доступ через Kubernetes Auth, а потом спокойно пьют кофе, пока остальные экстренно ротируют токены после очередного “ой”.
Сегодня - как раз про второй вариант.
Что вообще такое Auto Unseal и зачем он нужен
Обычный Vault после старта требует unseal.
То есть:
- Vault поднялся,
- Pod красивый,
- readiness green,
- а внутри - кирпич.
Пока кто-то не введёт несколько unseal keys.
В проде это выглядит примерно как:
“Кто помнит где лежит третий ключ?”
“А кто вообще был on-call?”
“Почему Vault лежит уже 40 минут?”
Auto Unseal решает эту проблему.
Vault сам расшифровывает master key через внешний KMS:
- AWS KMS
- Google Cloud KMS
- Azure Key Vault
- Transit backend другого Vault
- HSM
Для Kubernetes чаще всего используют:
- Cloud KMS
- или Transit Auto Unseal
Transit - особенно хорош, когда:
- мультикластер,
- hybrid,
- bare metal,
- или “у нас private cloud, потому что compliance”.
Архитектура нормального Vault в Kubernetes
Минимально адекватная схема выглядит так:
+-------------------+
| Transit Vault |
| (Unseal Master) |
+---------+---------+
|
Transit Encrypt
|
+---------------------+----------------------+
| |
+-------+--------+ +---------+--------+
| Vault Cluster | | Vault Cluster |
| Kubernetes #1 | | Kubernetes #2 |
+----------------+ +------------------+
Почему хранить секреты в Kubernetes Secret - зашквар?
Потому что:
- base64 - это не encryption,
- etcd иногда живёт слишком долго,
- backup’ы тоже живут слишком долго,
- а потом кто-то находит дамп etcd за 2023 год и начинается киберпанк.
Vault решает:
- динамические креды,
- lease,
- ротацию,
- PKI,
- short-lived secrets,
- audit,
- revocation.
И внезапно безопасность перестаёт быть “таблицей в Confluence”.
Установка Vault через Helm
Официальный Helm Chart - HashiCorp Vault Helm Chart
Добавляем репозиторий:
helm repo add hashicorp https://helm.releases.hashicorp.com
helm repo update
Ну и по накатанной...
values.yaml для HA + Raft + Auto Unseal
server:
replicas: 3
ha:
enabled: true
raft:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 0
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/userconfig/tls/tls.crt"
tls_key_file = "/vault/userconfig/tls/tls.key"
tls_client_ca_file = "/vault/userconfig/tls/ca.crt"
}
storage "raft" {
path = "/vault/data"
}
seal "transit" {
address = "https://vault-transit.internal:8200"
token = "hvs.xxxxxxxxx"
key_name = "autounseal"
mount_path = "transit/"
tls_skip_verify = "false"
}
service:
enabled: true
ui:
enabled: true
Создаём transit backend
На master Vault:
vault secrets enable transit
Создаём ключ:
vault write -f transit/keys/autounseal
Политика:
path "transit/encrypt/autounseal" {
capabilities = ["update"]
}
path "transit/decrypt/autounseal" {
capabilities = ["update"]
}
Kubernetes Auth
Это место, где многие делают:
token_reviewer_jwt: ""
А потом удивляются почему auth не работает.
Настраиваем:
vault auth enable kubernetes
vault write auth/kubernetes/config \
kubernetes_host=https://kubernetes.default.svc
Создаём role:
vault write auth/kubernetes/role/app \
bound_service_account_names=app \
bound_service_account_namespaces=prod \
policies=app \
ttl=1h
Доставка секретов в Pod
Теперь начинается самое интересное.
Есть несколько способов.
И вот тут уже видно разницу между:
“мы развернули Vault”
и
“мы умеем им пользоваться”.
Вариант 1. Vault Agent Injector
Самый популярный вариант.
Pod получает sidecar:
- авторизуется,
- тянет секреты,
- рендерит template,
- обновляет их автоматически.
Пример Deployment:
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 2
template:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "app"
vault.hashicorp.com/agent-inject-secret-config: "secret/data/app"
vault.hashicorp.com/agent-inject-template-config: |
{{- with secret "secret/data/app" -}}
DATABASE_URL={{ .Data.data.database_url }}
REDIS_PASSWORD={{ .Data.data.redis_password }}
{{- end }}
spec:
serviceAccountName: app
containers:
- name: api
image: ghcr.io/company/api:latest
Внутри контейнера появится файл:
/vault/secrets/config
Почему Vault Agent хорош
Потому что:
- секреты не лежат в Git,
- не лежат в etcd,
- не светятся в CI/CD,
- и не путешествуют по Slack как “скинь токен срочно”.
Вариант 2. CSI Driver
Когда хочется именно mounted files.
Устанавливаем: Secrets Store CSI Driver
И Vault Provider: Vault CSI Provider
SecretProviderClass
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
name: vault-db
spec:
provider: vault
parameters:
vaultAddress: "https://vault.example.com"
roleName: "app"
objects: |
- objectName: "db-password"
secretPath: "secret/data/db"
secretKey: "password"
Pod
volumes:
- name: secrets
csi:
driver: secrets-store.csi.k8s.io
readOnly: true
volumeAttributes:
secretProviderClass: vault-db
Когда CSI лучше Agent Injector
CSI удобнее когда:
- нужны именно файлы,
- legacy приложения,
- Java любит .jks,
- nginx хочет PEM,
- PostgreSQL требует cert/key.
То есть всё то прекрасное enterprise-наследие, которое живёт дольше некоторых датацентров.
Вариант 3. External Secrets Operator
Очень хороший вариант.
Особенно если:
- команды привыкли к Kubernetes Secret,
- но вы не хотите хранить секреты в Git.
Проект: External Secrets Operator
ClusterSecretStore
apiVersion: external-secrets.io/v1beta1
kind: ClusterSecretStore
metadata:
name: vault-backend
spec:
provider:
vault:
server: "https://vault.example.com"
path: "secret"
version: "v2"
auth:
kubernetes:
mountPath: "kubernetes"
role: "external-secrets"
ExternalSecret
apiVersion: external-secrets.io/v1beta1
kind: ExternalSecret
metadata:
name: app-secret
spec:
refreshInterval: 1h
secretStoreRef:
name: vault-backend
kind: ClusterSecretStore
target:
name: app-secret
data:
- secretKey: password
remoteRef:
key: app
property: password
PKI через Vault
Вот тут Vault становится уже не просто “хранилищем секретов”.
А полноценным internal CA.
Можно:
- выпускать mTLS сертификаты,
- делать short-lived certs,
- автоматизировать ротацию,
- выдавать cert per pod.
Включаем PKI
vault secrets enable pki
Генерируем Root CA
vault write pki/root/generate/internal \
common_name="corp.local"
Role
vault write pki/roles/internal \
allowed_domains="corp.local" \
allow_subdomains=true \
max_ttl="72h"
Получение сертификата
vault write pki/issue/internal \
common_name=api.prod.corp.local
И вот здесь начинается настоящая магия
Когда:
- PostgreSQL получает mTLS,
- ingress получает cert автоматически,
- сервисы живут на short-lived certs,
- а revoke делается за секунды.
В этот момент инфраструктура начинает выглядеть так, будто её собирали люди, которые хотя бы иногда читают документацию не только после инцидента.
Что я обычно рекомендую в production
Маленький кластер
- Vault HA
- Raft
- Transit Auto Unseal
- Agent Injector
Большой Kubernetes
- Vault HA
- Raft
- Transit или Cloud KMS
- CSI Driver
- PKI
- dynamic secrets
- audit backend
- namespaces
- Sentinel policies
Что НЕ надо делать
Не храните root token
Особенно:
- в Jenkins,
- в GitLab variables,
- в Notion,
- в “временном txt”.
Потому что:
“временное” в инфраструктуре живёт примерно до смерти вселенной.
Не отключайте TLS
Даже “внутри кластера”.
Эта фраза:
“ну это же internal traffic”
обычно произносится за пару месяцев до большого security review.
Не используйте один policy на всех
Политика:
path "*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
Это крик о помощи.
Итог
Vault в Kubernetes - это уже давно не “модно”.
Это просто нормальная инженерная гигиена.
Особенно когда:
- Kubernetes растёт,
- сервисов становится сотни,
- CI/CD живёт своей жизнью,
- а секреты начинают размножаться быстрее rabbitmq-инстансов после пятничного релиза.
И да - auto unseal это именно тот момент, когда инфраструктура перестаёт зависеть от человека с флешкой и становится похожей на production, а не на квест “найди второго администратора”.
PS: Если время будет, расскажу, как развернуть свой доверенный центр сертификации на Волте.