Ansible Vault: як шифрувати секрети в git
Захистіть паролі й API-токени в репозиторії playbook: шифруйте vars-файл або одне значення, розділяйте staging і production та безпечно змінюйте ключ.
Що захищає Ansible Vault, а що — ні
Ansible Vault шифрує секрети у вашому репозиторії playbook, тому git зберігає зашифрований текст, а не пароль у відкритому вигляді. Команда ansible-vault шифрує весь файл або окреме значення у файлі за допомогою симетричного ключа, похідного від вибраного вами пароля. Під час виконання play Ansible розшифровує ці дані в пам’яті, тому змінна поводиться як будь-яка інша змінна.
Ця модель має чітке обмеження. Vault захищає секрет у стані спокою в репозиторії і більше нічого. Після запуску завдання значення зберігається у відкритому вигляді в пам’яті, згенерованому шаблоні, аргументах модуля та виводі виконання, якщо це не заборонити. Кожен, хто може запускати playbook, має пароль vault. Тому vault забезпечує конфіденційність від людей поза командою, але не надає індивідуального керування доступом усередині команди.
Якщо ви ще не написали playbook, почніть із першого playbook Ansible для VPS і поверніться до цього розділу, коли цьому playbook знадобиться пароль.
Шифрувати весь файл чи окреме значення?
ansible-vault encrypt замінює файл шифротекстом. Файл перетворюється на один блок тексту в base64 під рядком заголовка, що починається з $ANSIBLE_VAULT. Використовуйте цей варіант, якщо файл містить лише секрети.
ansible-vault encrypt_string шифрує одне значення та виводить фрагмент YAML, який можна вставити у звичайний файл vars. Ім’я змінної залишається видимим, а шифротекстом стає лише значення. Використовуйте цей варіант, якщо секрети зберігаються поруч із параметрами у відкритому вигляді.
У щоденній роботі важлива різниця у diff. Під час кожного збереження vault-файл повторно шифрується з новою випадковою сіллю, тому кожен байт шифротексту змінюється. Тоді git diff показує, що один нерозбірливий блок замінено іншим нерозбірливим блоком. Рецензент не може визначити, чи ви змінили один пароль, чи переписали весь файл. З encrypt_string кожен секрет є окремим блоком у файлі з відкритим текстом. Тому diff точно показує, яка змінна змінилася, а решта файлу залишається без змін.
Inline-форма має недолік, який проявляється під час ротації: ansible-vault rekey не змінює inline-блоки. Вибирайте файлову форму, якщо список секретів довгий і змінюється рідко. Вибирайте inline-форму, якщо файл містить секрети разом зі звичайними змінними, а під час code review потрібно чітко бачити зміни.
Структура 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 і не знають, звідки походить значення. Це зберігає розділення між playbook і роллю чітким. Відкритий файл 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. Вставте його у файл vars точно в тому вигляді, у якому його виведено, оскільки відступ під тегом !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 job або CI runner.
Файл паролів — це звичайний текстовий файл, у першому рядку якого міститься пароль. Спочатку створіть порожній файл із жорсткими правами доступу, а потім введіть пароль у редакторі. Так пароль не потрапить до історії shell:
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 job. Job записує пароль із власного сховища облікових даних у файл у тимчасовому каталозі, експортує змінну та видаляє файл після завершення запуску. Також додайте шаблон імені файлу до .gitignore, оскільки шлях у ansible.cfg відстежується в репозиторії, і рано чи пізно хтось створить реальний файл у checkout.
Якщо файл паролів має дозвіл на виконання, Ansible запускає його й читає пароль зі стандартного виводу, а не читає файл як текст. Так можна отримувати пароль vault із системного keyring або cloud secret manager, взагалі не записуючи його на диск. Скрипт, який використовується через --vault-id, має додаткові вимоги: його ім’я має закінчуватися на -client або на -client з розширенням, він має бути виконуваним, приймати параметр --vault-id і виводити пароль у стандартний вивід.
Два vault ID: staging і production
vault ID — це мітка, прив’язана до пароля 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Передайте всі ID, які можуть знадобитися під час запуску:
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 ID.
Якщо завантажено кілька ID, ansible-vault encrypt більше не знає, яким паролем шифрувати. Укажіть його через --encrypt-vault-id prod або задайте vault_encrypt_identity у ansible.cfg, щоб у репозиторії було значення за замовчуванням.
Перевага полягає в обмеженні області розгортання. CI job, який розгортає staging, отримує лише пароль staging, тому скомпрометований runner не може прочитати облікові дані production. Коли ви запускаєте play на групі 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, якщо немає причини його змінювати, оскільки label записується в заголовок кожного файлу, який команда перезаписує.
Саме тут inline-форма створює додаткову роботу. ansible-vault rekey працює з повністю зашифрованими файлами, тому блок !vault усередині plaintext vars-файлу залишиться без змін. Спочатку знайдіть такі блоки, а потім повторно згенеруйте кожен із них за допомогою encrypt_string з новим паролем:
grep -rl '!vault' group_vars/ host_vars/Це повний компроміс. Inline-блоки дають змогу читати diff і вимагають ручної обробки під час ротації. Повністю зашифровані файли можна перевипустити однією командою, але під час перевірки в них немає корисного вмісту.
Чому секрет досі з’являється у виведенні
Vault завершує роботу одразу після розшифрування значення. Ansible виводить результат завдання, а модуль, який відображає свої аргументи, додає облікові дані до цього результату. Запуск у verbose-режимі, --diff для завдання з шаблоном, помилка завдання з виведенням його аргументів або callback plugin, який записує виведення у файл, — кожен із цих варіантів зберігає відкритий текст. Шифрування файлу не усуває жодної з цих проблем.
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 розшифровує файл у pager і нічого не записує на диск. 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 тепер виводитиме production-секрети у ваш термінал, тому вони потраплять до scrollback і будь-якої демонстрації екрана. Це локальна зручність для однієї людини на одному комп’ютері, тому залиште git config локальним і врахуйте, що checkout інших користувачів поводитиметься інакше, якщо вони не налаштують те саме.
Коли vault перестає бути правильним інструментом
Vault — це формат файлів з одним паролем для кожної мітки, і саме така структура визначає його обмеження. Перейдіть на спеціалізоване сховище секретів, якщо виконується хоча б одна з наведених умов.
- Вам потрібен доступ для окремих користувачів. Усі, хто запускає playbook, мають той самий пароль, а vault IDs розділяють доступ за середовищами, але не за користувачами.
- Вам потрібен журнал аудиту. Vault не записує, хто і коли розшифрував певні дані.
- Вам потрібна планова ротація. Vault не підтримує термін дії та версії, тому ніщо не повідомить, що облікові дані не змінювалися протягом двох років.
- Самому застосунку потрібен секрет під час роботи. Сервіс, який зчитує пароль до бази даних під час запуску, не повинен отримувати його з репозиторію розгортання.
Тоді схема змінюється на протилежну. Ansible припиняє зберігати секрети й починає отримувати їх під час роботи через lookup plugin із HashiCorp Vault (іншим продуктом із заплутано схожою назвою), secret manager хмарного провайдера або keyring на керувальній машині. У репозиторії зберігається шлях, у сховищі — значення, а сховище веде журнал доступу. Для невеликої команди ту саму задачу в меншому масштабі може виконувати self-hosted password manager з API, наприклад сервер Vaultwarden.
Один обліковий секрет залишається поза цією схемою. SSH key, який керувальна машина використовує для підключення до серверів, не є проблемою vault, оскільки Ansible потребує його ще до запуску будь-якого play. Керуйте ним через agent і passphrase, дотримуючись принципів, описаних у матеріалі основи керування SSH keys.
FAQ
Чи слід шифрувати весь файл vars, чи лише рядок із секретом?
Шифруйте весь файл, якщо в ньому містяться лише секрети, оскільки одна команда оновлює їх усі, а структура залишається простою. Використовуйте 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 нехай job записує пароль із власного сховища облікових даних у тимчасовий файл, експортує змінну та видаляє файл після завершення job. Якщо файл має право на виконання, 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 захищає секрет лише в стані спокою в репозиторії. Після запуску task значення стає відкритим текстом, і докладний вивід або помилка task може потрапити до журналу. Додайте no_log: true до кожного task, який працює з обліковими даними, задайте обмежувальні mode і owner для будь-якого файлу, який ви створюєте за допомогою шаблону, і не передавайте секрети як аргументи команди, оскільки під час виконання команди вони видимі у списку процесів на цільовому хості.