SSD Nodes Learn 🎉 VPS od $5.50/mies.
Przewodniki Matt ConnorAutor: Matt Connor

Ansible Vault: jak bezpiecznie szyfrować sekrety w Git

Dowiedz się, jak chronić hasła i tokeny API w repozytorium Ansible. Instrukcja szyfrowania plików oraz pojedynczych ciągów znaków za pomocą ansible-vault i zarządzania kluczami.

Co chroni Ansible Vault, a czego nie chroni

Ansible Vault szyfruje sekrety wewnątrz repozytorium playbooków, dzięki czemu git przechowuje tekst zaszyfrowany zamiast hasła w postaci jawnej. Polecenie ansible-vault szyfruje cały plik lub pojedynczą wartość wewnątrz pliku, używając klucza symetrycznego wygenerowanego na podstawie wybranego hasła. Ansible odszyfrowuje tę zawartość w pamięci podczas wykonywania playbooka, więc zmienna zachowuje się jak każda inna zmienna.

Ten model ma jedną wyraźną granicę. Vault chroni sekret w spoczynku w repozytorium i nic więcej. Gdy zadanie zostanie uruchomione, wartość jest tekstem jawnym w pamięci, w wygenerowanym szablonie, w argumentach modułu oraz w danych wyjściowych uruchomienia, chyba że zostanie to ograniczone. Każdy, kto może uruchomić playbook, posiada hasło do vaulta, więc vault zapewnia poufność wobec osób spoza zespołu, a nie kontrolę dostępu dla poszczególnych osób wewnątrz niego.

Jeśli jeszcze nie napisano żadnego playbooka, należy zacząć od pierwszego playbooka Ansible dla VPS i wrócić, gdy playbook będzie wymagał hasła.

Szyfrowanie całego pliku czy pojedynczego ciągu znaków?

ansible-vault encrypt zastępuje plik tekstem zaszyfrowanym. Plik staje się jednym blokiem tekstu w formacie base64 pod linią nagłówka zaczynającą się od $ANSIBLE_VAULT. Należy używać tej metody, gdy plik zawiera wyłącznie dane poufne.

ansible-vault encrypt_string szyfruje jedną wartość i wyświetla fragment YAML, który wkleja się do zwykłego pliku zmiennych. Nazwa zmiennej pozostaje czytelna, a zaszyfrowana jest tylko wartość. Należy używać tej metody, gdy dane poufne znajdują się obok ustawień w postaci jawnej.

Różnica istotna w codziennej pracy dotyczy narzędzia diff. Plik typu vault jest ponownie szyfrowany przy każdym zapisie z użyciem nowego, losowego salt, więc każdy bajt tekstu zaszyfrowanego ulega zmianie. git diff pokazuje wtedy jeden nieczytelny blok zastąpiony innym nieczytelnym blokiem, co oznacza, że osoba sprawdzająca kod nie jest w stanie stwierdzić, czy zmieniono jedno hasło, czy nadpisano cały plik. W przypadku encrypt_string każdy sekret jest osobnym blokiem wewnątrz pliku tekstowego, więc diff pokazuje dokładnie, która zmienna uległa zmianie, pozostawiając resztę pliku bez zmian.

Forma wbudowana (inline) wiąże się z pewnym kosztem, który ujawnia się podczas rotacji: ansible-vault rekey nie modyfikuje bloków wbudowanych. Należy wybrać formę plikową, gdy lista sekretów jest długa i zmienia się rzadko. Należy wybrać formę wbudowaną, gdy plik łączy sekrety ze zwykłymi zmiennymi i wymagana jest czytelność przeglądu kodu (code review).

Układ group_vars wskazujący na chronione dane

Ansible wczytuje group_vars/<group>.yml, a także każdy plik znajdujący się wewnątrz katalogu group_vars/<group>/. Forma katalogowa jest zalecana, ponieważ pozwala na przechowywanie pliku tekstowego jawnego oraz pliku zaszyfrowanego obok siebie w ramach jednej grupy.

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

Każdy vault.yml jest zaszyfrowany. Każdy vars.yml zawiera tekst jawny. Czytelnik może zidentyfikować chronione wartości bez otwierania plików, ponieważ informuje o tym nazwa pliku.

Druga część tego wzorca to pośrednictwo. Wewnątrz zaszyfrowanego pliku należy poprzedzić każdą zmienną przedrostkiem vault_.

vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"

Następnie należy odwołać się do tych nazw z pliku tekstowego znajdującego się obok.

db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"

Role i szablony używają db_password i nie otrzymują informacji o źródle wartości, co pozwala zachować rozdzielenie playbooka od roli w czystej formie. Jawny plik vars.yml pełni jednocześnie funkcję indeksu z możliwością przeszukiwania: grep -r vault_ group_vars/ zawiera listę wszystkich sekretów wymaganych przez repozytorium, bez konieczności odszyfrowywania czegokolwiek. Kosztem jest jedna dodatkowa nazwa dla każdego sekretu, a literówka w nazwie vault_ ujawnia się w czasie wykonywania jako niezdefiniowana zmienna, a nie jako błąd składni.

Szyfrowanie pojedynczej zmiennej za pomocą encrypt_string

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  --stdin-name 'vault_db_password'

Wprowadź sekret, a następnie naciśnij Ctrl-D. --stdin-name odczytuje wartość ze standardowego wejścia, co zapobiega zapisaniu jej w pliku historii powłoki. Druga forma umieszcza wartość bezpośrednio w wierszu poleceń, gdzie powłoka ją rejestruje:

ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
  'a real password' --name 'vault_db_password'

W obu przypadkach polecenie wyświetla blok YAML. Należy wkleić go do pliku vars dokładnie w takiej postaci, w jakiej został wygenerowany, ponieważ wcięcie pod tagiem !vault stanowi część wartości.

vault_db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;prod
          6638643965323633646262656665306333616466396630323136393465356136396436383331
          3131303163306665326539353837343663313762616561306534373963383531613664393332

Tag !vault informuje loader YAML, że skalar jest tekstem zaszyfrowanym, a nie zwykłym tekstem. Nagłówek zawiera wersję formatu, szyfr oraz etykietę vault ID, która została użyta do szyfrowania. Wartość zaszyfrowana bez użycia vault ID posiada nagłówek 1.1 bez etykiety, co nadal działa poprawnie, lecz dostarcza mniej informacji o pochodzeniu hasła.

Gdzie przechowywane jest hasło do vault?

Poza repozytorium. Jest to jedyna zasada, od której nie ma wyjątków.

--ask-vault-pass wyświetla monit raz na uruchomienie i nie zapisuje hasła. Rozwiązanie to sprawdza się na laptopie, ale nie nadaje się do zadań cron ani procesów CI.

Plik z hasłem to zwykły plik tekstowy, którego pierwszą linią jest hasło. Należy utworzyć go jako pusty plik z restrykcyjnymi uprawnieniami, a następnie wypełnić w edytorze, aby hasło nie trafiło do historii powłoki:

mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txt

Należy wskazać go dowolnym poleceniem za pomocą --vault-password-file:

ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
  --vault-password-file ~/.ansible/vault-prod.txt

Łatwo zapomnieć o dodawaniu tej flagi do każdego polecenia, dlatego warto ustawić ją raz w ansible.cfg w katalogu głównym repozytorium.

[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txt

To samo ustawienie odczytuje wartość ze zmiennej środowiskowej ANSIBLE_VAULT_PASSWORD_FILE, co jest standardowym sposobem przekazywania hasła w zadaniach CI. Zadanie zapisuje hasło z własnego magazynu poświadczeń do pliku w katalogu tymczasowym, eksportuje zmienną i usuwa plik po zakończeniu działania. Warto również dodać wzorzec nazwy pliku do .gitignore, ponieważ ścieżka w ansible.cfg jest zatwierdzana w repozytorium i prędzej czy później ktoś utworzy właściwy plik wewnątrz katalogu roboczego.

Jeśli plik z hasłem jest wykonywalny, Ansible uruchamia go i odczytuje hasło ze standardowego wyjścia zamiast traktować plik jako tekst. W ten sposób można pobrać hasło do vault z systemowego pęku kluczy lub chmurowego menedżera sekretów, bez zapisywania go na dysku. Skrypt używany przez --vault-id musi spełniać dodatkowe wymagania: jego nazwa musi kończyć się na -client lub -client wraz z rozszerzeniem, musi być wykonywalny, musi akceptować opcję --vault-id i musi wypisywać hasło na standardowe wyjście.

Dwa identyfikatory vault: staging i production

Identyfikator vault to etykieta przypisana do hasła vault, zapisywana jako label@source. Źródłem jest prompt, ścieżka do pliku z hasłem lub ścieżka do skryptu klienckiego. Etykiety pozwalają na przechowywanie w jednym repozytorium sekretów zabezpieczonych różnymi hasłami, dzięki czemu hasło staging nie otwiera pliku produkcyjnego.

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

Przekaż wszystkie identyfikatory wymagane przez uruchomienie:

ansible-playbook playbooks/site.yml \
  --vault-id staging@~/.ansible/vault-staging.txt \
  --vault-id prod@~/.ansible/vault-prod.txt

Alternatywnie wymień je w ansible.cfg:

[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txt

Jedno zachowanie bywa zaskakujące. Domyślnie etykieta jest wskazówką, a nie blokadą. Ansible próbuje użyć każdego posiadanego sekretu do odszyfrowania pliku, dopóki jeden z nich nie zadziała. Oznacza to, że plik oznaczony jako staging otworzy się, jeśli hasło produkcyjne okaże się właściwym kluczem. Ustaw vault_id_match = True w sekcji [defaults] lub użyj zmiennej środowiskowej ANSIBLE_VAULT_ID_MATCH, aby Ansible używał wyłącznie sekretu, którego etykieta pasuje do nagłówka pliku. Ta weryfikacja wymaga nagłówka 1.2, więc dotyczy tylko zawartości, która została pierwotnie zaszyfrowana przy użyciu identyfikatora vault.

Przy załadowaniu więcej niż jednego identyfikatora, ansible-vault encrypt nie rozpoznaje, którego hasła użyć do szyfrowania. Wskaż je za pomocą --encrypt-vault-id prod lub ustaw vault_encrypt_identity w ansible.cfg, aby repozytorium posiadało wartość domyślną.

Korzyścią jest zakres wdrożenia. Zadanie CI wdrażające środowisko staging otrzymuje wyłącznie hasło staging, więc przejęty runner nie może odczytać poświadczeń produkcyjnych. Gdy uruchamiasz playbooki na zespole serwerów Linux z jednej maszyny sterującej, taka separacja stanowi różnicę między drobnym incydentem a poważną awarią.

Zmiana klucza vault po odejściu pracownika

Zmiana klucza (rekeying) modyfikuje hasło vault i ponownie szyfruje zawartość przy użyciu nowego hasła. Operacja ta nie cofa żadnych zmian. Każda osoba, która posiadała stare hasło, nadal może odszyfrować posiadaną kopię repozytorium, w tym wszystkie stare commity w tej kopii. Z tego powodu hasło vault należy uznać za skompromitowane w momencie odejścia posiadacza i przeprowadzić rotację w następującej kolejności.

  1. Zmień rzeczywiste dane uwierzytelniające na serwerach oraz w usługach zewnętrznych. Ten krok faktycznie unieważnia dostęp.
  2. Wprowadź nowe wartości do plików vault za pomocą ansible-vault edit.
  3. Zmień klucz każdego zaszyfrowanego pliku na nowe hasło vault.
  4. Przekaż nowe hasło vault osobom, które nadal go potrzebują, korzystając z kanału komunikacji innego niż repozytorium.
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.yml

rekey akceptuje wiele plików w jednym poleceniu, a --new-vault-id prod@prompt prosi o podanie nowego hasła tylko raz, zamiast odczytywać je z dysku. Zachowaj tę samą etykietę, chyba że istnieje powód do jej zmiany, ponieważ etykieta jest zapisywana w nagłówku każdego pliku nadpisywanego przez polecenie.

W tym miejscu ujawnia się koszt stosowania formy inline. ansible-vault rekey operuje na w pełni zaszyfrowanych plikach, więc blok !vault znajdujący się wewnątrz pliku vars w postaci jawnej pozostaje nienaruszony. Należy je najpierw odnaleźć, a następnie wygenerować ponownie każdy z nich za pomocą encrypt_string przy użyciu nowego hasła:

grep -rl '!vault' group_vars/ host_vars/

To jest pełny bilans korzyści i strat. Bloki inline zapewniają czytelne diffy, ale wymagają ręcznego przetworzenia podczas rotacji. W pełni zaszyfrowane pliki podlegają rotacji za pomocą jednego polecenia, ale nie dostarczają żadnych użytecznych informacji podczas przeglądu zmian.

Dlaczego sekret nadal pojawia się w danych wyjściowych

Vault kończy działanie w momencie odszyfrowania wartości. Ansible raportuje wynik zadania, a moduł, który wyświetla swoje argumenty, przenosi poświadczenia do tego raportu. Szczegółowe logowanie, flaga --diff przy zadaniu typu template, nieudane zadanie zrzucające swoje argumenty lub wtyczka callback zapisująca dane wyjściowe do pliku – każdy z tych elementów zawiera tekst jawny. Szyfrowanie pliku nie rozwiązuje żadnego z tych problemów.

no_log: true jest odpowiednim przełącznikiem. Należy go ustawić przy każdym zadaniu, które otrzymuje poświadczenia.

- 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 wstrzymuje wtedy wynik tego zadania w danych wyjściowych, dzięki czemu log odnotowuje wykonanie zadania bez zapisywania informacji o tym, co było przetwarzane. Należy go stosować zwłaszcza w pętlach, ponieważ pętla raportuje jeden wynik na element, a pętla po liście poświadczeń raportuje całą listę.

Istnieją cztery inne miejsca, w których odszyfrowany sekret wycieka, a których no_log nie obejmuje:

  • Plik wygenerowany z szablonu dziedziczy mode oraz owner, które zostały mu nadane. Należy ustawić mode: "0600" oraz konkretnego właściciela dla każdego pliku przechowującego poświadczenia, w przeciwnym razie sekret stanie się dostępny dla wszystkich użytkowników na hoście docelowym.
  • Sekret przekazany do ansible.builtin.command lub ansible.builtin.shell pojawia się na liście procesów na hoście docelowym podczas wykonywania polecenia, gdzie każdy lokalny użytkownik może go odczytać. Zamiast tego należy przekazać go przez plik lub zmienną środowiskową.
  • Buforowanie faktów (fact caching) zapisuje zebrane fakty na dysku maszyny sterującej, więc zarejestrowana zmienna przechowująca sekret może trafić do pliku pamięci podręcznej, który nie jest traktowany jako poufny.
  • Ten sam sekret zazwyczaj znajduje się w drugim miejscu, na przykład w pliku środowiskowym odczytywanym przez kontener. Obowiązują tam odrębne zasady, a utrzymywanie poświadczeń poza plikami env w Compose opisuje ten aspekt.

no_log utrudnia debugowanie, co jest jego głównym celem. Należy go tymczasowo usunąć na hoście testowym, gdy zadanie działa nieprawidłowo, i przywrócić przed wprowadzeniem zmian na środowisko produkcyjne.

Odczyt i edycja zaszyfrowanych plików bez pozostawiania tekstu jawnego

ansible-vault view group_vars/prod/vault.yml deszyfruje plik bezpośrednio do pagera i nie zapisuje niczego na dysku. ansible-vault edit deszyfruje plik do pliku tymczasowego, otwiera go w $EDITOR, a po zamknięciu edytora ponownie szyfruje zawartość. Należy preferować obie te metody zamiast ansible-vault decrypt, ponieważ to ostatnie pozostawia plik w postaci jawnej w drzewie roboczym. Przypadkowe dodanie (staged) odszyfrowanego pliku z danymi wrażliwymi do repozytorium to najczęstsza przyczyna wycieku poświadczeń do publicznych zasobów.

Git potrafi wygenerować czytelny diff dla w pełni zaszyfrowanych plików, deszyfrując je w locie:

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

Należy zrozumieć działanie powyższego polecenia przed jego włączeniem. git diff spowoduje wyświetlanie sekretów produkcyjnych bezpośrednio w terminalu, co oznacza, że trafią one do historii przewijania (scrollback) oraz mogą zostać ujawnione podczas udostępniania ekranu. Jest to lokalne ułatwienie dla jednej osoby na jednej maszynie, dlatego należy zachować git config w konfiguracji lokalnej i założyć, że kopie robocze innych osób będą zachowywać się inaczej, chyba że skonfigurują one to samo środowisko.

Kiedy Vault przestaje być odpowiednim narzędziem

Vault to format pliku z jednym hasłem przypisanym do etykiety, a ta struktura wyznacza granice jego użyteczności. Należy przejść na profesjonalny magazyn sekretów, gdy spełniony jest którykolwiek z poniższych warunków.

  • Wymagany jest dostęp indywidualny. Każda osoba uruchamiająca playbook posiada to samo hasło, a identyfikatory Vault dzielą dostęp według środowisk, a nie użytkowników.
  • Wymagany jest dziennik audytu. Vault nie rejestruje informacji o tym, kto odszyfrował dany element ani kiedy to nastąpiło.
  • Wymagana jest rotacja haseł według harmonogramu. Vault nie obsługuje dat wygaśnięcia ani wersjonowania, więc brak jest mechanizmu informującego, że dane uwierzytelniające nie zostały zmienione od dwóch lat.
  • Aplikacja wymaga dostępu do sekretu w czasie wykonywania. Usługa odczytująca hasło do bazy danych podczas startu nie powinna pobierać go z repozytorium wdrożeniowego.

Wówczas schemat działania ulega odwróceniu. Ansible przestaje przechowywać sekrety i zaczyna pobierać je w czasie wykonywania za pomocą wtyczki lookup, odpytując HashiCorp Vault (inny produkt o myląco podobnej nazwie), menedżer sekretów dostawcy chmurowego lub keyring na maszynie sterującej. Repozytorium przechowuje ścieżkę, magazyn przechowuje wartość, a magazyn prowadzi dziennik dostępu. W przypadku małych zespołów, samodzielnie hostowany menedżer haseł z API, taki jak serwer Vaultwarden, realizuje to samo zadanie przy mniejszym narzucie.

Jedno uwierzytelnienie pozostaje poza tym zakresem. Klucz SSH, którego maszyna sterująca używa do łączenia się z serwerami, nie jest problemem dla Vault, ponieważ Ansible potrzebuje go przed uruchomieniem jakiegokolwiek play. Należy zarządzać nim za pomocą agenta i hasła, zgodnie z zasadami opisanymi w podstawach zarządzania kluczami SSH.

FAQ

Czy szyfrować cały plik vars, czy tylko ciąg znaków z sekretem?

Szyfruj cały plik, gdy zawiera on wyłącznie sekrety, ponieważ jedno polecenie pozwala na rotację całości, a struktura pozostaje przejrzysta. Używaj ansible-vault encrypt_string, gdy sekrety znajdują się obok zwykłych zmiennych, ponieważ wtedy w diffie zmienia się tylko zaszyfrowana wartość, a recenzent widzi, która zmienna została zmodyfikowana. Ceną jest rotacja. ansible-vault rekey obsługuje całe pliki i pomija wbudowane bloki !vault, więc te muszą zostać wygenerowane ręcznie przy użyciu nowego hasła.

Gdzie przechowywać plik z hasłem Ansible Vault?

Poza repozytorium, z uprawnieniami 0600, w ścieżce takiej jak ~/.ansible/vault-prod.txt. Wskaż go za pomocą --vault-password-file, ustaw vault_password_file w sekcji [defaults] w pliku ansible.cfg lub ustaw ANSIBLE_VAULT_PASSWORD_FILE w zmiennych środowiskowych. W systemach CI zadanie powinno zapisywać hasło z własnego magazynu poświadczeń do pliku tymczasowego, eksportować zmienną i usuwać plik po zakończeniu zadania. Jeśli plik jest wykonywalny, Ansible uruchamia go i odczytuje hasło ze standardowego wyjścia, co pozwala na pobranie go z pęku kluczy zamiast przechowywania na dysku.

Jak używać różnych haseł vault dla środowisk staging i production?

Nadaj każdemu hasłu etykietę za pomocą --vault-id staging@/path/to/file i --vault-id prod@/path/to/file, a następnie zaszyfruj pliki każdego środowiska pod jego własną etykietą. Przekaż oba identyfikatory w czasie wykonywania lub wymień je w vault_identity_list w sekcji [defaults]. Domyślnie Ansible próbuje użyć każdego posiadanego sekretu, dopóki nie uda się odszyfrować pliku, więc ustaw vault_id_match = True, jeśli chcesz, aby próbowano użyć tylko sekretu, którego etykieta pasuje do nagłówka pliku. Przy załadowanych kilku identyfikatorach wybierz ten szyfrujący za pomocą --encrypt-vault-id.

Czy Ansible Vault zapobiega pojawianiu się hasła w wynikach działania?

Nie. Vault chroni sekret jedynie w stanie spoczynku w repozytorium. Gdy zadanie jest uruchamiane, wartość staje się tekstem jawnym, a szczegółowe logi lub nieudane zadanie mogą przenieść ją do dziennika. Dodaj no_log: true do każdego zadania obsługującego poświadczenia, ustaw restrykcyjne mode i owner dla każdego pliku tworzonego z szablonu i unikaj przekazywania sekretów jako argumentów poleceń, ponieważ są one widoczne na liście procesów na hoście docelowym podczas wykonywania polecenia.