Vault + Kubernetes: поды получают credentials для PostgreSQL без паролей в ConfigMap
Интегрируем HashiCorp Vault с Kubernetes через Kubernetes auth method: поды сами получают short-lived токены для доступа к PostgreSQL без статических паролей.
HashiCorp Vault приближается к 1.0 - Kubernetes auth method и dynamic secrets для PostgreSQL
Vault подбирается к версии 1.0 - команда HashiCorp анонсировала RC, и это хороший момент чтобы посмотреть на Kubernetes auth method, который появился в 0.8 и с тех пор заметно повзрослел. Мы разворачивали это для одного из managed-кластеров и хотим поделиться тем, что реально работает, а что пока требует аккуратности.
Проблема с паролями в ConfigMap
Стандартная схема которую мы видим у клиентов выглядит примерно так: пароль от PostgreSQL лежит в ConfigMap или прямо в переменных окружения Pod-а, иногда даже в самом манифесте захардкоженным. В лучшем случае это Secret Kubernetes - но Secret в base64 это не шифрование, это кодирование. Кто имеет доступ к namespace - тот читает секрет. Кто сделал kubectl get secret -o yaml - тот видит пароль.
Ротация паролей в такой схеме - отдельная боль. Надо поменять значение в Secret, потом перезапустить поды чтобы они подхватили новое значение, при этом не уронить сервис. На практике пароли не ротируются месяцами или годами просто потому что это неудобно.
Vault с dynamic secrets предлагает другую модель: пароль живёт несколько часов или минут, генерируется специально для конкретного пода, после истечения TTL автоматически отзывается в PostgreSQL. Pod никогда не знает «основного» пароля - только свой одноразовый.
Как работает Kubernetes auth method
Ключевая идея в том, что каждый pod в Kubernetes уже имеет ServiceAccount с JWT-токеном, смонтированным в /var/run/secrets/kubernetes.io/serviceaccount/token. Vault умеет верифицировать этот токен через Kubernetes API - то есть pod может доказать Vault-у кто он такой, не зная никакого заранее выданного секрета.
Схема выглядит примерно так:
sequenceDiagram
participant Pod
participant Vault
participant K8s API
participant PostgreSQL
Pod->>Vault: POST /auth/kubernetes/login (JWT из ServiceAccount)
Vault->>K8s API: TokenReview (верифицировать JWT)
K8s API-->>Vault: OK, это pod "app" в namespace "production"
Vault-->>Pod: Vault token (короткоживущий)
Pod->>Vault: GET /database/creds/app-role (с Vault token)
Vault->>PostgreSQL: CREATE ROLE vault_app_xxx WITH LOGIN PASSWORD '...' VALID UNTIL '...'
Vault-->>Pod: username + password (TTL 1h)
Pod->>PostgreSQL: подключение с временными credentials
Настройка на стороне Vault:
# Включаем Kubernetes auth
vault auth enable kubernetes
# Настраиваем верификацию через K8s API
vault write auth/kubernetes/config \
kubernetes_host=https://kubernetes.default.svc \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
# Политика - что может делать приложение
vault policy write app-policy - <<EOF
path "database/creds/app-role" {
capabilities = ["read"]
}
EOF
# Роль: связываем ServiceAccount с политикой
vault write auth/kubernetes/role/app-role \
bound_service_account_names=app-sa \
bound_service_account_namespaces=production \
policies=app-policy \
ttl=1h
На стороне PostgreSQL Vault нужны права создавать временных пользователей - заводим для этого отдельного пользователя с нужными GRANT-ами. Vault сам пишет в pg через database secrets engine:
vault secrets enable database
vault write database/config/mydb \
plugin_name=postgresql-database-plugin \
connection_url="postgresql://vault_admin:{{password}}@pg-host:5432/mydb" \
allowed_roles=app-role \
username=vault_admin \
password=<пароль vault_admin>
vault write database/roles/app-role \
db_name=mydb \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT SELECT, INSERT, UPDATE ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl=1h \
max_ttl=4h
Что делает pod при старте
Само приложение должно уметь получить credentials из Vault перед тем как подключиться к базе. Есть несколько подходов - через init container, через Vault Agent (sidecar), или через библиотеку внутри приложения.
Мы остановились на Vault Agent как sidecar-контейнере - он сам обновляет credentials по мере истечения TTL и кладёт их в общий volume в виде файла. Приложение читает credentials из файла, и когда Vault Agent обновляет его - при следующем подключении подхватывает новые. Это работает без изменений в коде приложения, что важно когда приложение не наше.
spec:
serviceAccountName: app-sa
volumes:
- name: vault-secrets
emptyDir: {}
initContainers:
- name: vault-init
image: vault:0.10.3
command:
- sh
- -c
- |
vault agent -config=/vault/config/agent.hcl -exit-after-auth
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
containers:
- name: app
image: myapp:latest
env:
- name: DB_CREDENTIALS_FILE
value: /vault/secrets/db-creds
volumeMounts:
- name: vault-secrets
mountPath: /vault/secrets
Что мы поймали на практике
Ротация credentials и persistent connections. PostgreSQL-пул соединений держит коннекты открытыми - временный пользователь истёк, Vault его удалил, а пул ещё держит старые коннекты. При следующем запросе - ошибка авторизации. Нужно либо настраивать max_ttl так чтобы credentials жили дольше чем соединения в пуле, либо научить приложение переподключаться при ошибке auth. Второе правильнее, первое - компромисс на время отладки.
TokenReview требует прав. Vault-у нужен ServiceAccount с ClusterRole для вызова TokenReview API. Если права не выданы - auth method не может верифицировать JWT и возвращает ошибку. Это разовая настройка, но её легко пропустить в документации.
Lease renewal и сетевые проблемы. Vault Agent обновляет токены до истечения TTL. Если связь с Vault прерывается на время дольше чем TTL - credentials протухают и pod теряет доступ к базе. Для production нужен HA-деплоймент Vault с Consul в качестве storage backend, иначе единственный инстанс Vault становится критической точкой отказа которой раньше не было.
Аудит лог из коробки. Vault умеет логировать каждый вызов - кто, когда, какой секрет запросил, выдан или отказано. Для аудита безопасности это ценнее чем любой паттерн с ConfigMap, где вся история доступа к паролям непрозрачна.
Где мы сейчас
Vault с Kubernetes auth работает у нас на одном клиентском кластере в режиме пилота около трёх недель. PostgreSQL dynamic secrets работают стабильно, ротация происходит без участия человека. Vault Agent как sidecar добавляет ресурсов и усложняет манифесты - это реальная цена, о которой стоит предупреждать заранее.
Отдельная история - Vault HA. Мы пока запустили single-node с консулом для storage, и именно это звено сейчас вызывает больше всего вопросов по надёжности. Задеплоить Vault без отдельного Consul-кластера не получается - это открытый вопрос в roadmap HashiCorp.
Для тех кто сейчас хранит пароли в ConfigMap или переменных окружения - это уже не единственный вариант, и Kubernetes auth method снижает порог входа до разумного уровня.