ADG Оставить заявку
Блог DevOps 5 мин чтения

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.

Контакт

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

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