HashiCorp Vault: dynamic secrets для PostgreSQL, PKI-backend и AppRole в CI/CD
Вынесли все пароли из ansible-vault и конфигов в HashiCorp Vault 0.7: dynamic secrets для PostgreSQL, PKI для внутренних сертификатов, AppRole для CI/CD.
HashiCorp Vault 0.7 (апрель 2017) - secret management для инфраструктуры и приложений: dynamic secrets, PKI backend, улучшенный AppRole auth
Секреты в инфраструктуре - вечная головная боль. Пароли в конфигах, зашифрованные файлы в git через ansible-vault, ключи прямо в переменных окружения в CI/CD. Всё это работает ровно до момента когда нужно понять: кто имеет доступ к базе данных на проде, с каким паролем и с каких пор.
На одном из проектов решили это исправить. Потратили несколько недель на внедрение HashiCorp Vault, и вот что получилось.
Откуда пришли
Стартовая ситуация типичная. Пароль к PostgreSQL хранится в ansible-vault в репозитории с инфраструктурой. Тот же пароль продублирован в .env-файле на серверах, который разложил Ansible при первом деплое. Тот же пароль прописан в GitLab CI Variables для пайплайна, который прогоняет миграции. Итого: один пароль в трёх местах, и ни одно из них не даёт ответа на вопрос «кто сейчас имеет к нему доступ».
Ansible-vault решает задачу «не хранить открытый текст в git», но не решает задачу управления доступом. Ключ шифрования ansible-vault знает весь DevOps-отдел - значит, ротация при увольнении кого-то предполагает перешифровку всего хранилища и обновление пароля везде где он используется. Это больно.
Архитектура с Vault
Vault разворачиваем на отдельной ВМ, в кластерном режиме с Consul в качестве storage backend-а. Два узла Vault, три узла Consul - достаточно для нашей нагрузки.
┌──────────────────────────────────────────────┐
│ GitLab Runner (CI/CD) │
│ AppRole auth -> краткосрочный токен │
│ читает DB_PASSWORD / API_KEY из KV │
└──────────────────────┬───────────────────────┘
│
┌────────▼────────┐
│ Vault Cluster │
│ (2 узла + HA) │
└──┬──────────┬───┘
│ │
┌────────────▼──┐ ┌────▼────────────┐
│ PostgreSQL │ │ PKI Backend │
│ dynamic creds │ │ internal CA │
└───────────────┘ └────────────────┘
Три backend-а, которые реально используем:
KV (key-value) - для статических секретов которые пока не требуют динамики: API-ключи внешних сервисов, пароли к legacy-системам, SMTP credentials. Мигрировали сюда всё содержимое ansible-vault. Это не главная фича Vault, но уже лучше: доступ контролируется политиками, есть audit log.
Database secrets engine - вот за этим и шли. Vault сам подключается к PostgreSQL и создаёт временных пользователей с ограниченным TTL. Приложение просит токен у Vault, получает логин/пароль которые живут, например, час, использует их, они автоматически удаляются. На стороне PostgreSQL в любой момент видно какие роли созданы Vault-ом, они все именованные по шаблону v-gitlab-readonly-XXXX.
Настройка database backend-а минималистична:
vault mount database
vault write database/config/pg-prod \
plugin_name=postgresql-database-plugin \
allowed_roles="app-readonly,app-readwrite,migrations" \
connection_url="postgresql://vault-admin:{{password}}@pg.internal:5432/appdb" \
username="vault-admin" \
password="<admin-password>"
vault write database/roles/app-readonly \
db_name=pg-prod \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN ENCRYPTED PASSWORD '{{password}}' \
VALID UNTIL '{{expiration}}'; GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="24h"
PKI backend - внутренний CA для взаимной TLS-аутентификации между сервисами. Прежде самоподписанные сертификаты лежали в ansible-vault и перевыпускались вручную раз в год с заметкой в календаре. Теперь PKI backend является корневым CA, выпускает сертификаты через API с TTL в несколько дней, сервисы сами их обновляют через vault write pki/issue/internal-services. Ротация - автоматическая.
AppRole для CI/CD
GitLab runner должен уметь логиниться в Vault без того чтобы в CI Variables лежал долгоживущий Vault-токен. AppRole решает это через пару role_id + secret_id: role_id публичен (это просто идентификатор роли, не секрет), secret_id выдаётся на одно использование или на короткое время.
В пайплайне:
# before_script
- export VAULT_TOKEN=$(vault write -field=token auth/approle/login \
role_id="${VAULT_ROLE_ID}" \
secret_id="${VAULT_SECRET_ID}")
# deploy stage
- export DB_PASSWORD=$(vault read -field=password kv/prod/postgres/app)
- export REDIS_URL=$(vault read -field=url kv/prod/redis)
VAULT_ROLE_ID и VAULT_SECRET_ID в GitLab Variables - это не сами секреты, а credentials для получения доступа к секретам. secret_id настроен с num_uses=1 для production-деплоя: использовал - сгорел. Для staging num_uses=5 и TTL 24 часа - удобнее для отладки.
Политика для CI-runner-а максимально узкая: только чтение, только нужные пути:
path "kv/prod/*" {
capabilities = ["read"]
}
path "database/creds/app-readonly" {
capabilities = ["read"]
}
Что скрипит
Vault требует unseal при каждом запуске. Два узла в HA-кластере снижают риск, но при перезагрузке сервера кто-то должен ввести unseal keys (или настроить auto-unseal через HSM / cloud KMS, что мы пока не сделали). Это организационный вопрос, не технический, но он реальный.
Audit log пишет в файл на локальный диск - его нужно агрегировать в ELK или куда-то централизованно. Пока настроена ротация и отправка через filebeat, но полноценного анализа аудит-лога нет.
Database backend создаёт роли в PostgreSQL при каждом запросе. На тестовом окружении где пайплайны гоняются часто, в pg_roles накапливаются expired-роли - они не мусор, Vault их удаляет по TTL, но выглядит непривычно. Первый раз напугало.
Где сейчас
KV backend заменил ansible-vault полностью - ни одного зашифрованного файла в git больше нет. Database dynamic secrets подключены для приложений читающих PostgreSQL. PKI backend выдаёт сертификаты для трёх внутренних сервисов.
Про сопровождение инфраструктуры с Vault - добавляется пласт «а что если Vault недоступен»: приложения должны кешировать полученные credentials или корректно деградировать. Это меняет подход к деплою: нельзя просто положить Vault и надеяться что всё продолжит работать.
Ansible и дальше используем для провизии, но секреты туда больше не идут - Ansible-роли при необходимости сами ходят в Vault через hashi_vault lookup plugin. Про эту интеграцию, кстати, разговор отдельный.
Vault 0.7 выходит в апреле - там обещают Response Wrapping улучшения и доработки AppRole. Текущая 0.6.5 стабильна, проблем на проде не было, но следим за changelog.