ADG Оставить заявку
Блог Информационная безопасность 2 мин чтения

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 снижает порог входа до разумного уровня.

Контакт

Нужна такая же инженерная работа?

Опишите задачу и контекст. Ответим в течение рабочего дня, при необходимости подпишем NDA.