Ansible Vault: cifrare i secret in un repository Git
Scopri come cifrare file vars o singoli valori con Ansible Vault, separare staging e produzione e rigenerare le chiavi senza lasciare password in chiaro.
Cosa protegge Ansible Vault e cosa invece non protegge
Ansible Vault cifra i secret all'interno del repository del playbook, quindi git memorizza testo cifrato invece di una password in chiaro. Il comando ansible-vault cifra un file intero oppure un singolo valore all'interno di un file, usando una chiave simmetrica derivata dalla password scelta. Ansible decritta il contenuto in memoria quando esegue il play, quindi la variabile si comporta come qualsiasi altra variabile.
Questo modello ha un limite preciso. Vault protegge un secret quando è inattivo nel repository e nient'altro. Quando viene eseguito un task, il valore è presente in chiaro in memoria, nel template renderizzato, negli argomenti del modulo e nell'output dell'esecuzione, a meno che non lo si impedisca. Chiunque possa eseguire il playbook dispone della password di Vault, quindi Vault garantisce la riservatezza nei confronti delle persone esterne al team, non il controllo degli accessi per singolo utente all'interno del team.
Se non hai ancora scritto un playbook, inizia da un primo playbook Ansible su un VPS e torna qui quando quel playbook avrà bisogno di una password.
Crittografare un file intero o una singola stringa?
ansible-vault encrypt sostituisce un file con il testo cifrato. Il file diventa un unico blocco di testo codificato in base64 sotto una riga di intestazione che inizia con $ANSIBLE_VAULT. Usatelo quando il file contiene esclusivamente secret.
ansible-vault encrypt_string cifra un singolo valore e stampa uno snippet YAML da incollare in un normale file vars. Il nome della variabile resta leggibile e solo il valore viene cifrato. Usatelo quando i secret sono affiancati a impostazioni in chiaro.
La differenza importante nell'uso quotidiano riguarda il diff. Un file vault viene cifrato nuovamente con un salt casuale nuovo ogni volta che lo salvate, quindi ogni byte del testo cifrato cambia. git diff mostra quindi un blocco illeggibile sostituito da un altro blocco illeggibile. Un revisore non può capire se avete modificato una sola password o riscritto l'intero file. Con encrypt_string, ogni secret è un blocco autonomo all'interno di un file in chiaro. Il diff mostra quindi esattamente quale variabile è cambiata e lascia invariato il resto del file.
La forma inline ha un costo, che emerge al momento della rotazione: ansible-vault rekey non modifica i blocchi inline. Scegliete la forma basata su file quando l'elenco dei secret è lungo e cambia raramente. Scegliete la forma inline quando il file combina secret e variabili normali e volete che la code review sia significativa.
La struttura di group_vars che mostra cosa è protetto
Ansible carica group_vars/<group>.yml e carica anche ogni file contenuto in una directory group_vars/<group>/. La forma con directory è quella consigliata, perché consente a un singolo gruppo di contenere affiancati un file in chiaro e un file cifrato.
inventory/
hosts.ini
group_vars/
all/
vars.yml
vault.yml
web/
vars.yml
vault.yml
host_vars/
db01/
vars.yml
vault.yml
playbooks/
site.ymlOgni vault.yml è cifrato. Ogni vars.yml è in chiaro. Il lettore può capire quali valori sono protetti senza aprire alcun file, perché lo indica il nome del file.
La seconda parte del modello è l'indirezione. All'interno del file cifrato, anteponi vault_ a ogni variabile.
vault_db_password: "a real password"
vault_grafana_admin_token: "a real token"Quindi fai riferimento a questi nomi dal file in chiaro accanto.
db_password: "{{ vault_db_password }}"
grafana_admin_token: "{{ vault_grafana_admin_token }}"Ruoli e template usano db_password e non devono sapere da dove proviene il valore. In questo modo la separazione tra playbook e ruolo rimane chiara. Il file in chiaro vars.yml funge anche da indice ricercabile: grep -r vault_ group_vars/ elenca tutti i secret previsti dal repository, senza decrittografare nulla. Il costo consiste nell'aggiungere un nome per ogni secret; inoltre, un errore di battitura nel nome vault_ si manifesta a runtime come variabile non definita, anziché come errore di sintassi.
Cifratura di una variabile con encrypt_string
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
--stdin-name 'vault_db_password'Immettere il secret, quindi premere Ctrl-D. --stdin-name legge il valore dallo standard input, evitando di inserirlo nel file della cronologia della shell. L’altra forma passa il valore nella riga di comando, dove la shell lo registra:
ansible-vault encrypt_string --vault-id prod@~/.ansible/vault-prod.txt \
'a real password' --name 'vault_db_password'In entrambi i casi, il comando stampa un blocco YAML. Incollarlo nel file vars esattamente come viene stampato, perché il rientro sotto il tag !vault fa parte del valore.
vault_db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
6638643965323633646262656665306333616466396630323136393465356136396436383331
3131303163306665326539353837343663313762616561306534373963383531613664393332Il tag !vault indica al caricatore YAML che lo scalare contiene ciphertext e non testo. L’header contiene la versione del formato, il cifrario e l’etichetta del vault ID che lo ha cifrato. Un valore cifrato senza vault ID contiene un header 1.1 senza etichetta. Funziona comunque, ma fornisce meno informazioni sull’origine della password.
Dove si trova la password del vault?
Al di fuori del repository. Questa è l'unica regola senza eccezioni.
--ask-vault-pass richiede la password una volta per esecuzione e non memorizza nulla. È adatto a un laptop, ma non a un cron job o a un runner CI.
Un file della password è un file di testo normale la cui prima riga contiene la password. Crealo vuoto con permessi restrittivi, quindi compilalo in un editor, in modo che la password non finisca nella cronologia della shell:
mkdir -p ~/.ansible
install -m 600 /dev/null ~/.ansible/vault-prod.txt
$EDITOR ~/.ansible/vault-prod.txtIndica il file a qualsiasi comando con --vault-password-file:
ansible-playbook -i inventory/hosts.ini playbooks/site.yml \
--vault-password-file ~/.ansible/vault-prod.txtRipetere questo flag per ogni comando è facile da dimenticare. Impostalo quindi una volta in ansible.cfg nella radice del repository.
[defaults]
inventory = inventory/hosts.ini
vault_password_file = ~/.ansible/vault-prod.txtLa stessa impostazione legge la variabile d'ambiente ANSIBLE_VAULT_PASSWORD_FILE, che è il modo normalmente utilizzato da un job CI per fornire la password. Il job scrive la password dal proprio archivio delle credenziali in un file in una directory temporanea, esporta la variabile ed elimina il file al termine dell'esecuzione. Aggiungi il pattern del nome file anche a .gitignore, perché il percorso in ansible.cfg viene sottoposto a commit e prima o poi qualcuno creerà il file reale all'interno della working tree.
Se il file della password è eseguibile, Ansible lo esegue e legge la password dal relativo standard output invece di leggere il file come testo. In questo modo puoi recuperare la password del vault da un portachiavi di sistema o da un secret manager cloud senza scriverla affatto su disco. Uno script utilizzato tramite --vault-id deve rispettare requisiti aggiuntivi: il nome deve terminare con -client oppure con -client seguito da un'estensione, deve essere eseguibile, deve accettare un'opzione --vault-id e deve stampare la password sullo standard output.
Due ID vault: staging e produzione
Un vault ID è un'etichetta associata a una password del vault, indicata come label@source. L'origine può essere prompt, il percorso di un file delle password oppure il percorso di uno script client. Le etichette permettono di conservare i secret di un repository protetti da più password, quindi la password di staging non consente di aprire il file di produzione.
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.ymlPassare tutti gli ID necessari per l'esecuzione:
ansible-playbook playbooks/site.yml \
--vault-id staging@~/.ansible/vault-staging.txt \
--vault-id prod@~/.ansible/vault-prod.txtOppure elencarli una sola volta in ansible.cfg:
[defaults]
vault_identity_list = staging@~/.ansible/vault-staging.txt, prod@~/.ansible/vault-prod.txtUn comportamento può sorprendere. Per impostazione predefinita, l'etichetta è un suggerimento, non un vincolo. Ansible prova ogni secret attualmente disponibile con il file finché uno consente di decrittografarlo. Di conseguenza, un file con etichetta staging viene comunque aperto se la password di produzione è la chiave corretta. Impostare vault_id_match = True in [defaults] oppure la variabile d'ambiente ANSIBLE_VAULT_ID_MATCH fa sì che Ansible utilizzi soltanto il secret la cui etichetta corrisponde all'header del file. Questo controllo richiede l'header 1.2, quindi si applica solo ai contenuti cifrati originariamente con un vault ID.
Quando sono caricati più ID, ansible-vault encrypt non sa più quale password utilizzare per cifrare. Specificarla con --encrypt-vault-id prod oppure impostare vault_encrypt_identity in ansible.cfg per definire un valore predefinito per il repository.
Il vantaggio riguarda l'ambito del deployment. A un job CI che esegue il deployment in staging viene fornita soltanto la password di staging, quindi un runner compromesso non può leggere le credenziali di produzione. Quando si eseguono play su un insieme di server Linux da un'unica macchina di controllo, questa separazione può determinare la differenza tra un incidente limitato e uno di grande entità.
Ricreare la chiave del vault quando una persona lascia il team
La ricreazione della chiave cambia la password del vault e ricrittografa il contenuto con la nuova password. Non annulla nulla. Chiunque abbia avuto la vecchia password può ancora decrittografare qualsiasi copia del repository che ha conservato, inclusi tutti i commit precedenti presenti in quella copia. Considerate quindi compromessa la password del vault nel momento stesso in cui una persona che la conosce lascia il team, quindi eseguite la rotazione in questo ordine.
- Modificate le credenziali effettive sui server e nei servizi di terze parti. Questo è il passaggio che revoca realmente l'accesso.
- Inserite i nuovi valori nei file del vault con
ansible-vault edit. - Ricreate la chiave di ogni file crittografato usando una nuova password del vault.
- Comunicate la nuova password del vault alle persone che ne hanno ancora bisogno, tramite un canale diverso dal repository.
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 accetta più file in un unico comando e --new-vault-id prod@prompt richiede la nuova password una sola volta, invece di leggerla dal disco. Mantenete la stessa label, salvo che abbiate un motivo per cambiarla, perché la label viene scritta nell'header di ogni file che il comando riscrive.
È in questo punto che la modalità inline comporta un costo operativo. ansible-vault rekey opera su file completamente crittografati, quindi un blocco !vault inserito in un file vars in chiaro non viene modificato. Individuateli prima, quindi rigenerate ognuno di essi con encrypt_string usando la nuova password:
grep -rl '!vault' group_vars/ host_vars/Questo è il compromesso completo. I blocchi inline consentono di leggere facilmente le differenze, ma richiedono un passaggio manuale al momento della rotazione. I file completamente crittografati vengono ruotati con un unico comando, ma non offrono informazioni utili durante la revisione.
Perché il secret compare ancora nell'output
Vault ha terminato il proprio compito non appena il valore viene decrittografato. Ansible restituisce il risultato di un task e un modulo che riecheggia i propri argomenti trasferisce la credenziale in quel risultato. Un'esecuzione dettagliata, un --diff su un task template, un task non riuscito che espone i propri argomenti oppure un plugin callback che scrive l'output in un file conserveranno il testo in chiaro. La crittografia del file non impedisce nessuno di questi casi.
no_log: true è l'opzione da usare. Impostala su ogni task che riceve una credenziale.
- 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: trueAnsible omette quindi il risultato di quel task dall'output. Il log registra che il task è stato eseguito, senza registrare i dati elaborati. Impostala in particolare sui loop, perché un loop restituisce un risultato per ogni elemento e un loop su un elenco di credenziali restituisce l'intero elenco.
Esistono altri quattro punti in cui un secret decrittografato può fuoriuscire, e nessuno di questi è coperto da no_log:
- Un file generato da un template eredita i
modee iownerspecificati. Impostamode: "0600"e un owner specifico su qualsiasi elemento che contenga una credenziale. In caso contrario, il secret può risultare leggibile da tutti sull'host di destinazione. - Un secret passato a
ansible.builtin.commandoansible.builtin.shellcompare nell'elenco dei processi sull'host di destinazione mentre il comando è in esecuzione, dove qualsiasi utente locale può leggerlo. Passalo invece tramite un file o una variabile d'ambiente. - Il caching dei fact scrive su disco, sulla macchina di controllo, i fact raccolti. Una variabile registrata che contiene un secret può quindi finire in un file di cache che nessuno considera sensibile.
- Lo stesso secret si trova normalmente anche in un secondo punto, ad esempio in un file d'ambiente letto da un container. In quel caso valgono regole separate. Tenere le credenziali fuori dai file env di Compose tratta questo aspetto.
no_log rende più difficile il debugging, ed è esattamente il suo scopo. Rimuovilo temporaneamente su un host di test quando un task presenta problemi e ripristinalo prima che la modifica arrivi in produzione.
Leggere e modificare file crittografati senza lasciare copie in chiaro
ansible-vault view group_vars/prod/vault.yml esegue la decrittografia in un pager senza scrivere nulla su disco. ansible-vault edit esegue la decrittografia in un file temporaneo, apre il file con $EDITOR e lo ricrittografa quando lo si chiude. Preferire entrambe le opzioni a ansible-vault decrypt, che lascia un file in chiaro nella working tree. Un file del vault decrittografato aggiunto per errore all'area di staging è il modo più comune con cui una credenziale reale finisce in un repository pubblico.
Git può generare un diff leggibile per i file completamente crittografati, decrittografandoli durante l'operazione:
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' >> .gitattributesPrima di abilitarla, verificare il comportamento. git diff stamperà ora i secret di produzione nel terminale, inserendoli nella cronologia dello scroll e in qualsiasi condivisione dello schermo. È una comodità locale per una sola persona su una sola macchina. Mantenere quindi git config locale e prevedere che i checkout di altre persone si comportino in modo diverso, a meno che non configurino la stessa impostazione.
Quando vault non è più lo strumento adatto
Vault è un formato di file con una password per ogni label, e questa struttura ne determina i limiti. Passa a un vero secret store quando si verifica una delle seguenti condizioni.
- Ti serve l'accesso per singola persona. Chiunque esegua il playbook dispone della stessa password, mentre i vault ID separano l'accesso per ambiente, mai per persona.
- Ti serve una traccia di audit. Vault non registra chi ha decrittografato cosa né quando.
- Ti serve la rotazione secondo una pianificazione. Vault non gestisce scadenze né versioni, quindi nulla segnala che una credenziale non è stata modificata da due anni.
- È l'applicazione stessa ad avere bisogno del secret in fase di esecuzione. Un servizio che legge la password del database all'avvio non dovrebbe leggerla dal repository di deployment.
A questo punto il modello si inverte. Ansible smette di archiviare i secret e inizia a recuperarli in fase di esecuzione tramite un plugin di lookup, da HashiCorp Vault (un prodotto diverso con un nome simile e potenzialmente fuorviante), dal secret manager di un cloud provider oppure da un keyring sulla control machine. Il repository contiene un percorso, lo store contiene il valore e lo store conserva il log degli accessi. Per un team di piccole dimensioni, un password manager self-hosted con API, come un server Vaultwarden, svolge la stessa funzione su scala ridotta.
Una credenziale resta fuori da tutto questo. La chiave SSH utilizzata dalla control machine per raggiungere i server non è un problema di vault, perché Ansible ne ha bisogno prima che possa essere eseguito qualsiasi play. Gestiscila con un agent e una passphrase, seguendo le indicazioni di le basi della gestione delle chiavi SSH.
FAQ
Devo cifrare l'intero file vars o solo la stringa segreta?
Cifra l'intero file quando contiene soltanto secret, perché un solo comando li ruota tutti e la struttura resta semplice. Usa ansible-vault encrypt_string quando i secret sono affiancati a variabili ordinarie, perché in questo modo nel diff cambia soltanto il valore cifrato e chi effettua la revisione può vedere quale variabile è stata modificata. Il compromesso riguarda la rotazione. ansible-vault rekey gestisce i file interi e lascia invariati i blocchi !vault inline, che devono quindi essere rigenerati manualmente con la nuova password.
Dove deve essere archiviato il file della password di Ansible Vault?
Al di fuori del repository, con modalità 0600, in un percorso come ~/.ansible/vault-prod.txt. Indicalo con --vault-password-file, oppure imposta vault_password_file in [defaults] dentro ansible.cfg, oppure imposta ANSIBLE_VAULT_PASSWORD_FILE nell'ambiente. In CI, fai scrivere al job la password, recuperata dal relativo archivio di credenziali, in un file temporaneo; esporta la variabile ed elimina il file al termine del job. Se il file è eseguibile, Ansible lo esegue e legge la password dallo standard output. In questo modo puoi recuperarla da un keyring invece di archiviarla su disco.
Come posso usare password Vault diverse per staging e produzione?
Assegna a ogni password un'etichetta con --vault-id staging@/path/to/file e --vault-id prod@/path/to/file e cifra i file di ciascun ambiente usando la relativa etichetta. Passa entrambi gli ID in fase di esecuzione oppure elencali in vault_identity_list sotto [defaults]. Per impostazione predefinita, Ansible prova ogni secret disponibile finché uno consente di decifrare il file. Imposta vault_id_match = True se vuoi che provi soltanto il secret la cui etichetta corrisponde all'intestazione del file. Quando hai caricato più ID, seleziona quello usato per la cifratura con --encrypt-vault-id.
Ansible Vault impedisce che una password compaia nell'output dell'esecuzione?
No. Vault protegge il secret soltanto quando è archiviato nel repository. Quando viene eseguito un task, il valore è in chiaro e un'esecuzione verbosa o un task non riuscito può riportarlo nel log. Aggiungi no_log: true a ogni task che gestisce una credenziale, imposta mode e owner con permessi restrittivi su ogni file generato tramite template ed evita di passare i secret come argomenti del comando, perché durante l'esecuzione sono visibili nell'elenco dei processi sull'host di destinazione.