OnCallReady

Lesson 33.23 · Ansible: Configuration as Code · 17 min read

Secrets: ansible-vault and no_log

In plain words

ansible-vault is a locked box for the secret parts of your project. The database password goes into a small file, and the file is locked with a key (the vault password). Anyone can see the box in the shared cupboard (git), but only someone with the key can open it. When Ansible runs, it uses the key to open the box, reads the password and uses it.

The catch: once the box is open during a run, Ansible talks a lot, and it may read the password out loud into the log. no_log: true tells it to keep quiet about that task.

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:

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

Why it helps

Every real Ansible project has secrets: database passwords, API tokens, keys. Putting them in git in plain text puts them on every laptop and in every clone forever. Vault is the standard first step, and the vars.yml plus vault.yml layout is what reviewers expect to see.

The second half matters just as much: secrets leak at run time through debug, verbose output and diffs into job logs that many people can read. Knowing that, and fixing it with no_log, is exactly the kind of thing a security review or an interview will ask about.

Commands in this lesson

cd head ansible-vault ansible-playbook

FAQ

Where should the vault password live?

Never in the repository. A file with mode 600 in your home directory, pointed at by vault_password_file in ansible.cfg or --vault-password-file, or an executable script that fetches it from a secrets manager. On a shared runner, the runner's own secret storage provides it.

Should I encrypt whole files or single values?

Prefer whole files: group_vars/db/vault.yml with only the secrets, referenced from a plain vars.yml. ansible-vault rekey can then rotate the vault password across all files. encrypt_string is handy for one value inside a normal file, but rotating means re-encrypting every inline value by hand.

What does the $ANSIBLE_VAULT header mean?

It marks the file as encrypted: $ANSIBLE_VAULT;1.1;AES256 is format 1.1 with AES-256. A file encrypted with a vault ID label has format 1.2 and the label at the end, for example ;prod. The rest of the file is the encrypted data written as hex, safe to commit.

Does no_log hide everything?

It replaces that task's result in the output and logs with a censored message, at every verbosity. It does not protect the secret anywhere else: a debug task, a template --diff on another task, or the secret written into a world-readable file still leak. And when a no_log task fails, you cannot see why, so debug it by hand on one host.

What if a secret was already printed in a log?

Treat it as compromised. Rotate it: change the real password, put the new value in the vault, deploy it. Then fix the playbook so it cannot print again, and delete or restrict the log. Deleting the log alone does not help, because you cannot know who already read it.

In an interview Junior

How do you handle secrets in Ansible?

With ansible-vault: secrets go in an encrypted file such as group_vars/db/vault.yml (vault_db_password), and a plain vars.yml maps them (db_password: "{{ vault_db_password }}"), so they are encrypted at rest in git but still easy to find and review. The vault password lives outside the repository, in a 600 file referenced by vault_password_file, or an executable that reads a secrets manager. At run time the value is plain, so every task that uses it gets no_log: true, because -v, -vvv, debug and --diff can print it into a job log. If a secret ever leaks, I rotate it.

Also asked: How would you rotate the vault password? · What is the difference between encrypt and encrypt_string? · Why is an environment variable not a safe place for a secret?

Practise this lesson in the terminal Free, in your browser - a real Ubuntu terminal to try it in, with missions that check your work.