The problem
The database role needs the database password. The playbooks live in git, reviewed in pull requests, cloned onto laptops and the job runners that deploy them. A password in a group_vars file is a password on every one of those machines, forever - git history does not forget. You need the secret encrypted at rest in the repository, decrypted only at run time by someone who holds the key. And you need it not to leak at run time either, which is the part teams forget.
What you need to know already: the variables lesson (group_vars directories), the roles lesson, 2.21 (why environment variables are not secret), 4.7 (file modes: 600).
ansible-vault
ansible-vault encrypts a whole file, or a single value, with a password (AES-256). The encrypted file is still a text file, safe to commit:
$ cd ~/oncall-lab/labs/1a-ansible/try/vault
$ head -3 group_vars/db/vault.yml
$ANSIBLE_VAULT;1.1;AES256
39316536303131353733393331376135306138376437636333303333633163396564646535613364
6631353666323363333166316632313532393163633232310a623437373365643263306533393635
The first line is the header: format version 1.1, cipher AES256. With a vault ID (below) it is $ANSIBLE_VAULT;1.2;AES256;prod. The rest is the encrypted payload in hex.
The subcommands:
ansible-vault create FILE new encrypted file, opens $EDITOR
ansible-vault edit FILE decrypt to a temp file, $EDITOR, re-encrypt
ansible-vault view FILE decrypt to the pager
ansible-vault encrypt FILE... encrypt existing plain files in place
ansible-vault decrypt FILE... the reverse (rarely a good idea)
ansible-vault rekey FILE... re-encrypt with a new password
ansible-vault encrypt_string encrypt one value for pasting into a YAML file
($EDITOR picks the editor; set export EDITOR=nano or you get vi.)
Where the password comes from
Every command that needs to decrypt asks for the password one of these ways:
--ask-vault-pass (-J) prompt: Vault password:
--vault-password-file ~/.vault_pass read it from a file (first line)
vault_password_file = ~/.vault_pass the same, in ansible.cfg
--vault-id prod@~/.vault_pass_prod labelled: one password per environment
--vault-id dev@prompt labelled, prompted
A password file must be chmod 600, outside the repository (or in .gitignore), and never in the job log. If the file is executable, Ansible runs it and reads the password from its output - that is how teams fetch the vault password from a real secrets manager instead of keeping it on disk.
Without any of them, the run stops at the first encrypted value it needs:
$ ansible-vault view --vault-password-file .vault_pass group_vars/db/vault.yml
vault_db_password: S3cr3t-for-db
$ ansible-playbook db.yml --vault-password-file .vault_pass
PLAY [Database hosts] **********************************************************
TASK [Who we connect as] *******************************************************
ok: [db-1] => {
"msg": "connecting as orders"
}
PLAY RECAP *********************************************************************
db-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The vars + vault layout
Teams split a group's variables in two files:
group_vars/db/vars.yml plain, readable, reviewable:
db_user: orders
db_password: "{{ vault_db_password }}"
group_vars/db/vault.yml encrypted, holds only the secrets:
vault_db_password: S3cr3t-for-db
Everything stays greppable: grep -r db_password finds where it is used and that it comes from a vault variable, without decrypting anything. A pull request that changes vars.yml has a readable diff; one that changes vault.yml shows "the secret changed", which is what reviewers need to know.
One value at a time
encrypt_string encrypts a single value for the middle of a normal YAML file:
$ ansible-vault encrypt_string --vault-password-file .vault_pass 'S3cr3t-api-key' --name api_key
Encryption successful
api_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
33366364386435326461623038623161653065376437353235623639346632313035623561636435
6533303365336664366437303462333332356437326236360a643838376465343264653530646332
36356463353034393362646436373165623038346137383961303632643832336564303133323738
6235616563353431360a30323737636535646366626163383162363734313964626165613732
Paste the output into any vars file. The file stays readable; only that value is opaque. The downside: ansible-vault rekey works on files, so rotating the vault password means re-running encrypt_string for every inline value. Prefer vault files.
The leak nobody encrypted
The vault protects the file in git. At run time the value is decrypted in memory and used like any other variable - and Ansible prints a lot:
-vprints every task's result;-vvvalso prints the module's arguments ("invocation": {"module_args": ...}), including the password you passed to it.- A failing task prints its result, which can include the command line.
debug: var=db_passwordprints it, plainly.--diffon a template that contains the secret prints the secret.- The run's output ends up in a job log that half the company can read.
The fix is no_log: true on every task that handles a secret:
- name: Database credentials
ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
mode: "0600"
no_log: true
With no_log, Ansible replaces the task's result with a fixed text in all output and logs, at every verbosity:
$ ansible-playbook leak.yml --vault-password-file .vault_pass -v
Using /home/learner/oncall-lab/labs/1a-ansible/try/vault/ansible.cfg as config file
PLAY [Database hosts] **********************************************************
TASK [Check the password is set] ***********************************************
ok: [db-1] => {"ansible_facts": {"discovered_interpreter_python": "/usr/bin/python3.14"}, "changed": false, "cmd": ["/usr/bin/test", "-n", "S3cr3t-for-db"], "delta": "0:00:00.000317", "end": "2026-09-22 20:00:07.760320", "msg": "", "rc": 0, "start": "2026-09-22 20:00:07.760320", "stderr": "", "stderr_lines": [], "stdout": "", "stdout_lines": []}
PLAY RECAP *********************************************************************
db-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
$ ansible-playbook safe.yml --vault-password-file .vault_pass -v
Using /home/learner/oncall-lab/labs/1a-ansible/try/vault/ansible.cfg as config file
PLAY [Database hosts] **********************************************************
TASK [Check the password is set] ***********************************************
ok: [db-1] => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result", "changed": false}
PLAY RECAP *********************************************************************
db-1 : ok=1 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
The first run prints the password in the result; the second prints "censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result". Note what no_log costs: when that task fails you cannot see why. Re-run it by hand with no_log turned off, on one host, not in the shared log.
Modules mark their own secret parameters (the password of user, the url_password of uri): their values are replaced by VALUE_SPECIFIED_IN_NO_LOG_PARAMETER in the printed arguments. Your own variables get no such help - that is your job.
In an interview: "ansible-vault encrypts the file in git, but at run time the value is plain in memory: -v and -vvv, failed tasks, debug and --diff can print it into the job log. So every task that touches a secret gets no_log: true, the vault password comes from a file outside the repo or a script that reads a secrets manager, and secrets live in a separate vault.yml referenced from vars.yml."
A secrets manager goes one step further: it hands out short-lived secrets at run time, so the repository holds no secret at all, encrypted or not. The course has a chapter on HashiCorp Vault later; ansible-vault is the step most teams start with.
What you can now do
- Encrypt files and single values with ansible-vault, read the header, view, edit and rekey them.
- Supply the vault password safely (a 600 file outside the repo, an executable script, vault IDs per environment).
- Lay out secrets as
vars.yml+vault.yml. - Find every way a decrypted secret can leak into output, and stop it with
no_log.