HashiCorp Vault 0.11 и dynamic secrets: как мы перестали хранить пароли к PostgreSQL в конфигах
Перевели ротацию credentials к PostgreSQL на Vault dynamic secrets. Пароль живёт 1 час, создаётся под каждый деплой - утечка из конфига перестала быть рабочей угрозой.
HashiCorp Vault 0.11 - dynamic secrets для баз данных и PKI, улучшенный UI и performance standby nodes
Vault у нас в инфраструктуре появился года полтора назад, и всё это время мы использовали его в основном как защищённое хранилище статических секретов: положили пароль, достали пароль, зашифровали. Полезно, но принципиально не отличается от зашифрованного S3-бакета с правильными политиками. В сентябре вышел Vault 0.11, и мы наконец дошли до того, ради чего он изначально проектировался, - dynamic secrets для PostgreSQL.
Что не так со статическими паролями
Проблема не в том что пароли ненадёжные. Проблема в жизненном цикле. Статический пароль к базе данных - это объект с неопределённым сроком жизни: создали при деплое, положили в конфиг или в переменную окружения, и он там живёт месяцами. А иногда годами, если ротацию никто не настроил.
Сценарий утечки простой: разработчик скопировал .env с продакшн-сервера на ноутбук «посмотреть», ноутбук угнали, или файл осел в git-репозитории в момент отладки, или попал в логи через --verbose в CI. Пароль валиден - значит атакующий имеет доступ к базе ровно столько, сколько пароль существует. Если его никто не ротирует - фактически бесконечно.
Инцидентов такого рода у наших клиентов напрямую не было, но в аудитах мы видели конфиги с паролями пятилетней давности в production-окружениях. Это не теоретический риск.
Как работают dynamic secrets в Vault
Идея прямолинейная. Vault подключается к PostgreSQL с правами администратора, и когда приложение запрашивает credentials, он:
- Создаёт нового пользователя в базе с уникальным именем вида
v-app-xK3mP9q-1537952341 - Выдаёт этому пользователю нужные привилегии (которые описаны в role-шаблоне)
- Возвращает логин и пароль приложению
- Устанавливает TTL - после истечения автоматически отзывает пользователя
Приложение получает credentials на конкретный промежуток времени. Когда TTL истекает - Vault удаляет пользователя из PostgreSQL. Следующий деплой получит другой пароль.
Что это означает на практике: даже если credentials утекли, они валидны только до конца TTL. С нашим выбором в один час атакующий имеет час. Не год.
Как настраивали
Настройка через Vault CLI или Terraform-провайдер. Мы использовали Terraform - он у нас уже управляет инфраструктурой, логично держать конфигурацию Vault там же.
Включение database secrets engine и подключение PostgreSQL:
vault secrets enable database
vault write database/config/myapp-postgres \
plugin_name=postgresql-database-plugin \
allowed_roles="myapp-readonly,myapp-readwrite" \
connection_url="postgresql://{{username}}:{{password}}@postgres.internal:5432/myapp?sslmode=require" \
username="vault_admin" \
password="<admin-password>"
Роль с SQL-шаблоном для создания пользователя:
vault write database/roles/myapp-readwrite \
db_name=myapp-postgres \
creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; \
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO \"{{name}}\"; \
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO \"{{name}}\";" \
default_ttl="1h" \
max_ttl="4h"
Получение credentials приложением:
vault read database/creds/myapp-readwrite
Возвращает username и password, которые валидны 1 час. Максимальный TTL 4 часа - это для случаев когда деплой завис или нужно дать больше времени на graceful shutdown.
Что изменилось в Vault 0.11
Версия 0.11 добавила несколько вещей, которые повлияли на нашу конфигурацию:
Performance standby nodes - раньше все запросы шли через активный Vault-узел, standby только реплицировали данные. В 0.11 standby-узлы могут обслуживать read-запросы, включая чтение секретов. Для database credentials это значит что при высокой нагрузке на деплой (несколько параллельных запросов credentials) нагрузка распределяется.
Улучшенный веб-интерфейс - database backends теперь видны и настраиваемы через UI. Для нас это в основном инструмент диагностики: посмотреть какие leases активны, какие пользователи сейчас существуют в базе.
Batch tokens - новый тип токенов с меньшими накладными расходами. Для приложений которые запрашивают credentials часто это даёт заметное снижение нагрузки на Vault.
Интеграция с приложением
Самый неудобный вопрос при переходе на dynamic secrets - как приложение обновляет credentials до истечения TTL. Если приложение стартует, получает пароль и держит connection pool - всё хорошо до тех пор, пока connection pool не начнёт терять соединения после истечения TTL.
Мы решили это по-простому: приложение получает credentials при старте через Vault Agent, который сам занимается renewal. Vault Agent запускается как sidecar-процесс, держит token, запрашивает credentials и кладёт их в файл или передаёт через переменные окружения. При renewal - обновляет файл, приложение перечитывает при следующем подключении из пула.
Более чистое решение - использовать Vault SDK напрямую в приложении с логикой renewal. Но это требует изменений в коде приложения, а у нас задача была перевести существующие сервисы без переписывания.
Что получилось
Три production-сервиса на PostgreSQL переведены на dynamic secrets. Пароль к базе теперь не существует как постоянная сущность: нет файла с паролем, нет значения в переменных окружения которое можно стащить и использовать долго.
В pg_roles на сервере можно видеть текущих пользователей: обычно это один-два активных Vault-созданных аккаунта плюс статические сервисные. Картина сразу показывает кто сейчас имеет доступ - это само по себе полезно для мониторинга.
Из неожиданного: нашли несколько мест где credentials хранились не только в конфигах приложения, но и в скриптах резервного копирования, в Ansible-inventory, в .pgpass на серверах. Dynamic secrets это не закрывает автоматически - пришлось отдельно проходить по каждому месту использования.
Аудит куда утекают credentials - отдельная работа, которую dynamic secrets мотивируют сделать, но не делают за тебя.