Как зашифровать секреты в Ansible Vault для Git
Используйте Ansible Vault для защиты паролей и API токенов в репозитории. В статье разобраны команды для шифрования файлов, переменных, смена ключей и разделение сред.
Что защищает Ansible Vault, а что нет
Ansible Vault шифрует секретные данные внутри репозитория с плейбуками, поэтому в git сохраняется зашифрованный текст, а не пароль в открытом виде. Команда ansible-vault шифрует либо весь файл целиком, либо отдельное значение внутри файла, используя симметричный ключ, производный от выбранного вами пароля. Ansible расшифровывает это содержимое в оперативной памяти во время выполнения плейбука, поэтому переменная ведет себя так же, как и любая другая переменная.
У этой модели есть четкая граница. Vault защищает секрет в состоянии покоя в репозитории, и не более того. Как только задача запущена, значение становится открытым текстом в оперативной памяти, в сгенерированном шаблоне, в аргументах модуля и в выводе процесса выполнения, если вы не ограничите его отображение. Каждый, кто может запустить плейбук, владеет паролем от vault, поэтому vault обеспечивает конфиденциальность от людей вне команды, а не разграничение доступа между участниками внутри нее.
Если вы еще не написали ни одного плейбука, начните с первого плейбука Ansible для VPS и вернитесь сюда, когда этому плейбуку потребуется пароль.
Шифровать весь файл или одну строку?
ansible-vault encrypt заменяет файл зашифрованным текстом. Файл превращается в один блок текста в формате base64 под заголовочной строкой, начинающейся с $ANSIBLE_VAULT. Используйте этот способ, если файл содержит только секретные данные.
ansible-vault encrypt_string шифрует одно значение и выводит фрагмент YAML, который вы вставляете в обычный файл переменных. Имя переменной остается читаемым, а зашифрованным является только значение. Используйте этот способ, если секреты соседствуют с обычными настройками.
Разница, важная для повседневной работы, заключается в diff. Файл vault при каждом сохранении перешифровывается с новым случайным солью, поэтому каждый байт зашифрованного текста меняется. В результате git diff показывает один нечитаемый блок, замененный другим нечитаемым блоком. Это означает, что проверяющий не сможет понять, обновили ли вы один пароль или переписали весь файл. При использовании encrypt_string каждый секрет является отдельным блоком внутри обычного текстового файла, поэтому diff точно показывает, какая переменная изменилась, не затрагивая остальное содержимое файла.
У встроенной формы есть недостаток, который проявляется при ротации: ansible-vault rekey не затрагивает встроенные блоки. Выбирайте файловую форму, если список секретов длинный и меняется редко. Выбирайте встроенную форму, если в файле секреты смешаны с обычными переменными и вы хотите, чтобы код-ревью имел смысл.
Структура group_vars, демонстрирующая защиту данных
Ansible загружает group_vars/<group>.yml, а также все файлы внутри каталога group_vars/<group>/. Использование каталога предпочтительнее, так как это позволяет хранить для одной группы обычный текстовый файл и зашифрованный файл рядом друг с другом.
inventory/
hosts.ini
group_vars/
all/
vars.yml
vault.yml
web/
vars.yml
vault.yml
host_vars/
db01/
vars.yml
vault.yml
playbooks/
site.ymlКаждый vault.yml зашифрован. Каждый vars.yml содержит открытый текст. Читающий может увидеть, какие значения защищены, не открывая файлы, так как об этом говорит имя файла.
Вторая часть этого шаблона — косвенное обращение. Внутри зашифрованного файла добавляйте к имени каждой переменной префикс vault_.
vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"Затем ссылайтесь на эти имена из расположенного рядом текстового файла.
db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"Роли и шаблоны используют db_password и не «знают», откуда поступило значение, что сохраняет разделение между плейбуком и ролью в чистоте. Текстовый файл vars.yml также служит поисковым индексом: grep -r vault_ group_vars/ содержит список всех секретов, ожидаемых репозиторием, без необходимости расшифровки. Платой за это является одно дополнительное имя для каждого секрета, а опечатка в имени vault_ проявится во время выполнения как неопределенная переменная, а не как синтаксическая ошибка.
Шифрование одной переменной с помощью encrypt_string
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
--stdin-name 'vault_db_password'Введите секретное значение и нажмите Ctrl-D. --stdin-name считывает значение из стандартного потока ввода, что позволяет не сохранять его в файле истории командной оболочки. Другой способ предполагает передачу значения непосредственно в командной строке, где оболочка сохранит его в истории:
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
'a real password' --name 'vault_db_password'В любом случае команда выведет YAML-блок. Вставьте его в файл переменных в точности в том виде, в котором он был выведен, так как отступы под тегом !vault являются частью значения.
vault_db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
6638643965323633646262656665306333616466396630323136393465356136396436383331
3131303163306665326539353837343663313762616561306534373963383531613664393332Тег !vault сообщает загрузчику YAML, что скаляр является зашифрованным текстом, а не обычными данными. Заголовок содержит версию формата, алгоритм шифрования и метку vault ID, которая использовалась при шифровании. Значение, зашифрованное без указания vault ID, содержит заголовок 1.1 без метки; такой вариант продолжает работать, но предоставляет меньше информации о происхождении пароля.
Где хранится пароль от vault?
Вне репозитория. Это единственное правило, исключений из которого нет.
--ask-vault-pass запрашивает пароль один раз за запуск и нигде его не сохраняет. Это подходит для ноутбука, но не для cron-задач или CI-раннеров.
Файл пароля — это обычный текстовый файл, в первой строке которого записан пароль. Создайте его пустым с ограниченными правами доступа, затем впишите пароль через редактор, чтобы он не попал в историю командной оболочки:
mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txtУкажите путь к нему в любой команде с помощью --vault-password-file:
ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
--vault-password-file ~/.ansible/vault-prod.txtПовторять этот флаг в каждой команде легко забыть, поэтому задайте его один раз в ansible.cfg в корне репозитория.
[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txtЭта же настройка считывается из переменной окружения ANSIBLE_VAULT_PASSWORD_FILE, именно так её обычно передает CI-задача. Задача записывает пароль из своего хранилища учетных данных во временный файл, экспортирует переменную и удаляет файл после завершения работы. Добавьте шаблон имени файла в .gitignore, так как путь в ansible.cfg попадает в коммит, и рано или поздно кто-нибудь создаст реальный файл внутри рабочей копии.
Если файл пароля является исполняемым, Ansible запускает его и считывает пароль из стандартного вывода вместо чтения файла как текста. Именно так можно получить пароль vault из системной связки ключей или облачного менеджера секретов, вообще не записывая его на диск. Скрипт, используемый через --vault-id, должен соответствовать дополнительным требованиям: его имя должно заканчиваться на -client или на -client с расширением, он должен быть исполняемым, принимать опцию --vault-id и выводить пароль в стандартный поток вывода.
Два идентификатора vault: staging и production
Идентификатор vault — это метка, привязанная к паролю vault, которая записывается как label@source. Источником служит prompt, путь к файлу пароля или путь к клиентскому скрипту. Метки позволяют хранить в одном репозитории секреты, защищенные разными паролями, чтобы пароль от staging не открывал файлы production.
ansible-vault encrypt --vault-id staging@~/.ansible/vault-staging.txt \
group_vars/staging/vault.yml
ansible-vault encrypt --vault-id prod@~/.ansible/vault-prod.txt \
group_vars/prod/vault.ymlПередавайте все идентификаторы, которые могут потребоваться при запуске:
ansible-playbook playbooks/site.yml \
--vault-id staging@~/.ansible/vault-staging.txt \
--vault-id prod@~/.ansible/vault-prod.txtИли укажите их один раз в ansible.cfg:
[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txtОдно поведение часто вызывает удивление. По умолчанию метка является подсказкой, а не блокировкой. Ansible пробует каждый имеющийся у него секрет для расшифровки файла, пока один из них не сработает. Поэтому файл с меткой staging все равно откроется, если пароль от production окажется подходящим ключом. Установите vault_id_match = True в секции [defaults] или используйте переменную окружения ANSIBLE_VAULT_ID_MATCH, и тогда Ansible будет использовать только тот секрет, метка которого совпадает с заголовком файла. Эта проверка требует наличия заголовка 1.2, поэтому она применяется только к содержимому, которое изначально было зашифровано с использованием идентификатора vault.
Если загружено более одного идентификатора, ansible-vault encrypt больше не знает, какой пароль использовать для шифрования. Укажите его с помощью --encrypt-vault-id prod или установите vault_encrypt_identity в ansible.cfg, чтобы у репозитория был пароль по умолчанию.
Преимущество заключается в разграничении прав при развертывании. CI-задача, выполняющая деплой в staging, получает только пароль от staging, поэтому скомпрометированный раннер не сможет прочитать учетные данные production. Когда вы запускаете плейбуки на парке серверов Linux с одной управляющей машины, такое разделение определяет разницу между небольшим инцидентом и очень крупным.
Смена ключа хранилища при увольнении сотрудника
Смена ключа меняет пароль хранилища и повторно шифрует содержимое с использованием нового пароля. Это действие не отменяет предыдущие операции. Любой, кто ранее владел старым паролем, по-прежнему может расшифровать имеющуюся у него копию репозитория, включая все старые коммиты в этой копии. Поэтому считайте пароль хранилища скомпрометированным в тот момент, когда сотрудник, имевший к нему доступ, уходит, и выполняйте ротацию в следующем порядке.
- Измените реальные учетные данные на серверах и в сторонних сервисах. Именно этот шаг фактически отзывает доступ.
- Внесите новые значения в файлы хранилища с помощью
ansible-vault edit. - Перешифруйте каждый зашифрованный файл с использованием нового пароля хранилища.
- Передайте новый пароль хранилища тем, кому он по-прежнему необходим, используя канал связи, не связанный с репозиторием.
ansible-vault rekey --vault-id prod@~/.ansible/vault-prod-old.txt \
--new-vault-id prod@prompt \
group_vars/prod/vault.yml host_vars/db01/vault.ymlrekey принимает несколько файлов в одной команде, а --new-vault-id prod@prompt запрашивает новый пароль один раз, вместо того чтобы считывать его с диска. Сохраняйте прежнюю метку (label), если у вас нет причин её менять, так как метка записывается в заголовок каждого файла, который перезаписывает команда.
Здесь проявляется недостаток использования встроенных блоков. ansible-vault rekey работает с полностью зашифрованными файлами, поэтому блок !vault, находящийся внутри обычного текстового файла переменных, остаётся без изменений. Сначала найдите их, а затем пересоздайте каждый с помощью encrypt_string под новым паролем:
grep -rl '!vault' group_vars/ host_vars/Таков полный расклад. Встроенные блоки обеспечивают читаемые diff-файлы, но требуют ручной обработки во время ротации. Полностью зашифрованные файлы проходят ротацию одной командой, но не дают никакой полезной информации при просмотре изменений.
Почему секрет всё ещё появляется в выводе
Vault завершает работу в момент расшифровки значения. Ansible сообщает результат выполнения задачи, и модуль, который выводит свои аргументы, переносит учетные данные в этот отчет. Подробный режим запуска, --diff в задаче с шаблоном, неудачная задача, выводящая свои аргументы, или callback-плагин, записывающий вывод в файл, — всё это сохраняет открытый текст. Шифрование файла никак не влияет на эти случаи.
no_log: true — это нужный флаг. Установите его для любой задачи, которая получает учетные данные.
- name: Write the application environment file
ansible.builtin.template:
src: app.env.j2
dest: /etc/myapp/app.env
owner: myapp
group: myapp
mode: "0600"
no_log: trueПосле этого Ansible скрывает результат выполнения задачи из вывода, поэтому в логах фиксируется факт выполнения задачи без записи того, что именно она обрабатывала. Устанавливайте этот флаг особенно для циклов, так как цикл сообщает по одному результату на каждый элемент, а цикл по списку учетных данных выведет весь список целиком.
Существует четыре других способа утечки расшифрованного секрета, которые no_log не покрывает:
- Файл, созданный из шаблона, наследует
modeиowner, которые вы ему задали. Установитеmode: "0600"и конкретного владельца для любого файла, содержащего учетные данные, иначе секрет станет доступен для чтения всем пользователям на целевом хосте. - Секрет, переданный в
ansible.builtin.commandилиansible.builtin.shell, появляется в списке процессов на целевом хосте во время выполнения команды, где его может прочитать любой локальный пользователь. Вместо этого передавайте его через файл или переменную окружения. - Кэширование фактов записывает собранные факты на диск управляющей машины, поэтому зарегистрированная переменная, содержащая секрет, может попасть в файл кэша, который никто не считает конфиденциальным.
- Тот же секрет обычно хранится во втором месте, например, в файле окружения, который считывается контейнером. Там действуют отдельные правила, и защита учетных данных от попадания в env-файлы Compose описывает эту сторону вопроса.
no_log затрудняет отладку, для чего он, собственно, и предназначен. Временно удалите его на тестовом хосте, если задача работает некорректно, и верните обратно перед тем, как изменения попадут в production.
Чтение и редактирование зашифрованных файлов без сохранения открытого текста
ansible-vault view group_vars/prod/vault.yml расшифровывает файл в пейджер и не записывает данные на диск. ansible-vault edit расшифровывает файл во временное хранилище, открывает его в вашем $EDITOR и выполняет повторное шифрование при закрытии. Используйте эти инструменты вместо ansible-vault decrypt, так как последний оставляет файл в открытом виде в рабочем каталоге. Случайное добавление расшифрованного файла хранилища в индекс — самый частый способ попадания реальных учетных данных в публичный репозиторий.
Git может отображать читаемые diff для полностью зашифрованных файлов, расшифровывая их «на лету»:
git config --local diff.ansible-vault.textconv "ansible-vault view --vault-password-file ~/.ansible/vault-prod.txt"
printf '%s\n' 'group_vars/**/vault.yml diff=ansible-vault' >> .gitattributesПрежде чем включать эту функцию, разберитесь, как она работает. Теперь git diff будет выводить производственные секреты прямо в терминал, что приведет к их сохранению в истории прокрутки и попаданию на экран при демонстрации. Это локальное удобство для одного пользователя на одной машине, поэтому храните git config в локальной конфигурации и учитывайте, что у других пользователей репозиторий будет вести себя иначе, если они не настроят аналогичное поведение.
Когда Vault перестает быть подходящим инструментом
Vault — это формат файлов, где на каждую метку приходится один пароль, и именно эта структура определяет границы его применимости. Переходите на полноценное хранилище секретов, если верно хотя бы одно из следующих утверждений:
- Вам нужен индивидуальный доступ для каждого пользователя. Все, кто запускает playbook, используют один и тот же пароль, а идентификаторы Vault разделяют доступ по окружениям, но не по сотрудникам.
- Вам нужен журнал аудита. Vault не записывает информацию о том, кто и когда расшифровал данные.
- Вам требуется регулярная ротация. В Vault нет механизмов истечения срока действия или версионности, поэтому ничто не укажет на то, что учетные данные не менялись два года.
- Приложению требуется секрет во время выполнения. Сервис, считывающий пароль от базы данных при загрузке, не должен получать его из вашего репозитория развертывания.
В этом случае схема меняется на противоположную. Ansible перестает хранить секреты и начинает получать их во время выполнения через lookup-плагин из HashiCorp Vault (это другой продукт с похожим названием), менеджера секретов облачного провайдера или связки ключей на управляющей машине. В репозитории хранится путь, в хранилище — значение, а само хранилище ведет журнал доступа. Для небольшой команды с этой задачей при меньших затратах ресурсов справится self-hosted менеджер паролей с API, например сервер Vaultwarden.
Одни учетные данные остаются вне этой системы. SSH-ключ, который использует ваша управляющая машина для доступа к серверам, не является задачей для Vault, так как он нужен Ansible еще до запуска любого play. Управляйте им с помощью агента и кодовой фразы, следуя принципам основ управления SSH-ключами.
FAQ
Нужно ли шифровать весь файл переменных или только секретную строку?
Шифруйте весь файл, если в нем содержатся только секреты: одна команда обновляет их все, а структура остается простой. Используйте ansible-vault encrypt_string, когда секреты соседствуют с обычными переменными, так как в этом случае в diff-файле меняется только зашифрованное значение, и проверяющий видит, какая именно переменная была изменена. Платой за это является сложность ротации. ansible-vault rekey обрабатывает файлы целиком и не затрагивает встроенные блоки !vault, поэтому их приходится пересоздавать вручную при смене пароля.
Где следует хранить файл с паролем Ansible Vault?
Вне репозитория, с правами доступа 0600, по пути вроде ~/.ansible/vault-prod.txt. Укажите его через --vault-password-file, либо задайте vault_password_file в секции [defaults] файла ansible.cfg, либо установите ANSIBLE_VAULT_PASSWORD_FILE в переменной окружения. В CI настройте задание так, чтобы оно записывало пароль из собственного хранилища учетных данных во временный файл, экспортировало переменную и удаляло файл по завершении работы. Если файл является исполняемым, Ansible запустит его и прочитает пароль из стандартного вывода, что позволяет получать его из связки ключей (keyring) вместо хранения на диске.
Как использовать разные пароли Vault для staging и production?
Присвойте каждому паролю метку с помощью --vault-id staging@/path/to/file и --vault-id prod@/path/to/file и зашифруйте файлы каждой среды под своей меткой. Передавайте оба идентификатора во время выполнения или перечислите их в vault_identity_list в секции [defaults]. По умолчанию Ansible пробует каждый имеющийся у него секрет, пока один из них не расшифрует файл, поэтому установите vault_id_match = True, если хотите, чтобы он пробовал только тот секрет, метка которого совпадает с заголовком файла. При загрузке нескольких идентификаторов выберите тот, который будет использоваться для шифрования, с помощью --encrypt-vault-id.
Предотвращает ли Ansible Vault появление пароля в выводе выполнения?
Нет. Vault защищает секрет только в состоянии покоя в репозитории. Как только задача выполняется, значение становится открытым текстом, и подробный вывод (verbose) или сбой задачи могут привести к его попаданию в лог. Добавляйте no_log: true к каждой задаче, которая работает с учетными данными, устанавливайте строгие права mode и owner на любой файл, который вы создаете по шаблону, и избегайте передачи секретов в качестве аргументов команд, так как они видны в списке процессов на целевом хосте во время выполнения команды.