Ansible Vault: шифруем секреты прямо в репозитории
Ansible 1.7 добавил Vault для шифрования секретов в playbook-файлах. Разбираем процесс миграции с файлов вне git, ротацию ключа и интеграцию с CI.
Ansible 1.7 представил Vault - встроенное шифрование секретов внутри playbook-файлов и переменных
До появления Vault у нас было стандартное решение: секреты - пароли к базам, токены API, приватные ключи - лежали в отдельных файлах вне репозитория. Playbook-и в git, секреты на общем сетевом ресурсе или просто в ~/secrets/ у каждого из команды. Синхронизировали вручную. Всё работало ровно до момента, когда кто-то деплоил со старым файлом переменных после ротации пароля. Ansible не ругался - просто использовал устаревший пароль, и сервис падал уже после деплоя. Искать потом было весело.
В Ansible 1.7 появился Vault - AES-256 прямо внутри YAML. Зашифрованный файл переменных живёт рядом с playbook-ом в том же репозитории.
Как это выглядит на практике
Переменные с секретами раньше хранились в group_vars/production/secrets.yml - файл был в .gitignore, передавался отдельно. Теперь он шифруется на месте:
ansible-vault encrypt group_vars/production/secrets.yml
Команда запрашивает vault-пароль, перезаписывает файл. После этого содержимое выглядит так:
$ANSIBLE_VAULT;1.1;AES256
36663430383938373066643234323733303936623865623162626263323039323236306461343734
...
Файл можно коммитить - без vault-пароля это просто набор hex-байт. Ansible при запуске просит пароль сам:
ansible-playbook site.yml --ask-vault-pass
или берёт из файла:
ansible-playbook site.yml --vault-password-file ~/.vault_pass
Структура, которая у нас сложилась
После миграции на роли в июле разделили переменные на два уровня: vars.yml с незасекреченными параметрами (порты, пути, флаги) и secrets.yml с паролями и токенами - только второй под Vault. Это удобно: можно читать vars.yml без пароля и понимать что вообще настраивается.
group_vars/
production/
vars.yml # открытый, в git
secrets.yml # зашифрован Vault, тоже в git
В playbook-е оба файла подхватываются автоматически по директории.
Ротация ключа vault
Слабое место схемы - если vault-пароль утёк, нужно перезашифровать всё. Команда rekey для этого:
ansible-vault rekey group_vars/production/secrets.yml
Команда просит старый пароль, затем новый. Один файл - одна операция. Если зашифрованных файлов несколько, приходится делать по одному - скриптуем через find:
find . -name 'secrets.yml' | xargs -I{} ansible-vault rekey {}
Важно: rekey меняет ключ шифрования, но не меняет сами секреты. Если нужна полная ротация (и пароль vault, и пароли внутри), это два отдельных шага.
Интеграция с CI
На управляемых серверах у нас деплои идут через Jenkins. Передавать vault-пароль интерактивно очевидно нельзя, файл с паролем на агент тоже класть не хочется - ещё один объект для защиты.
Пока остановились на таком компромиссе: vault-пароль хранится в Jenkins в разделе Credentials как секретный текст, в pipeline-е пишется во временный файл перед запуском playbook-а и удаляется после:
withCredentials([string(credentialsId: 'vault-pass', variable: 'VAULT_PASS')]) {
sh '''
echo "$VAULT_PASS" > /tmp/vp_$BUILD_NUMBER
ansible-playbook site.yml --vault-password-file /tmp/vp_$BUILD_NUMBER
rm -f /tmp/vp_$BUILD_NUMBER
'''
}
Не идеально - в случае аварийного завершения файл может остаться. Правим через trap в sh-блоке. Решение рабочее, но требует аккуратности.
Другой вариант, который смотрели - --vault-password-file с исполняемым скриптом, который достаёт пароль из защищённого хранилища. Но это уже требует отдельного хранилища секретов, а у нас его пока нет.
Что изменилось в работе
Секреты в одном репозитории с кодом. Нет рассинхронизации между кодом и переменными - они версионируются вместе. Если в коммите меняется переменная, видно сразу что именно.
Дифф зашифрованных файлов. В git-е зашифрованные файлы меняются полностью при каждом изменении - осмысленного diff-а нет. Это раздражает: не видно что конкретно поменялось в secrets.yml. Vault не умеет шифровать отдельные значения в YAML - только файл целиком. Обходим комментариями в незашифрованном vars.yml типа «пароль обновлён, причина - плановая ротация».
Grep по секретам невозможен. Очевидно - зашифровано. Если нужно найти где используется переменная db_password, ищем по коду, а не по значению. Это нормально.
Один пароль на всё. Сейчас у нас один vault-пароль на весь репозиторий. В Ansible 1.7 это единственный вариант - разные пароли для разных файлов не поддерживаются. Если понадобится разделить доступ - придётся держать отдельные репозитории.
Итог
Vault закрыл конкретную боль: секреты теперь версионируются вместе с playbook-ами и не расходятся. Компромиссы есть - нечитаемый diff, один общий пароль, необходимость аккуратно работать с CI. Но это ощутимо лучше, чем синхронизировать файлы вручную и ловить деплои со старыми паролями.