Ansible Vault encrypts a file or a single value with AES256, and it stops caring the moment a playbook decrypts it. In our lab a vaulted database password came back in plain text from a one-line debug call, and again in the output of --diff, with no warning either time. Encryption at rest is the easy half; keeping the value out of logs is the half most tutorials skip.
This Ansible Vault tutorial covers both. You’ll create and edit encrypted files, encrypt single strings, feed passwords from files, scripts and ansible.cfg, split dev and prod with vault IDs, rotate passwords with rekey, and read every error we hit along the way. All of it was run in October 2026 on Debian 13 with Ansible 14.5.0 (ansible-core 2.21.5), on the same control node we set up in the Ansible on Debian 13 guide.
Prerequisites
Vault ships inside ansible-core, so there is nothing extra to install once ansible-vault --help answers. In our lab the managed host ansible01 was the control node VM itself, reached over SSH, plus localhost; that is enough to prove where secrets end up on a target, but it is not a multi-host fleet. You need:
- Ansible installed on a control node (we used ansible-core 2.21)
- One managed host reachable over SSH with passwordless sudo (or add
-Kto everyansible-playbookrun, sincesite.ymlusesbecome) - An editor set in
$EDITOR, or vi as the fallback - A Git repository for the project, so you can check what actually gets committed
If you are new to playbooks themselves, start with the playbook tutorial in this series, then come back. Readers who prefer working through a book alongside the lab will find the current titles compared in our list of Ansible books worth reading, several of which have a full chapter on Vault.
Set up a project for the vault examples
Vault reads its settings from the project’s ansible.cfg, so every command below runs from one directory. We reused the inventory from the install guide, which has a web group with one host and localhost for local work:
mkdir -p ~/vault-lab/group_vars/web ~/vault-lab/vars
cd ~/vault-lab
vim inventory.ini
Replace the host name, IP and user with your own. The web group name matters, because the encrypted variables later live in group_vars/web/ and load for that group automatically:
[web]
ansible01 ansible_host=10.0.1.50
[control]
localhost ansible_connection=local
[all:vars]
ansible_user=josphat
Start ansible.cfg with just the inventory path and quiet interpreter discovery. The vault settings get added at the end, once you know what each one does:
vim ansible.cfg
Add these three lines. Keeping the vault options out for now means every command in the next sections shows its password source on the command line, so you can see exactly which secret opens which file before you hide that behind config.
[defaults]
inventory = inventory.ini
interpreter_python = auto_silent
With that in place, ansible all -m ping should return pong from both hosts before you encrypt anything, because an SSH problem discovered halfway through a vault exercise looks like a vault problem.
Create an encrypted variables file
ansible-vault create asks for a new password twice, opens your editor on an empty file, and encrypts whatever you save. The plain text only exists in a temporary file while the editor is open, so it’s the safest way to start a secrets file: there is never a moment where an unencrypted copy sits in the project directory waiting to be committed. If your editor keeps swap or backup files, they live in that temporary location too, which is why a plain vim secrets.yml followed by encrypt is the worse habit. Create the vault file for the web group:
ansible-vault create group_vars/web/vault.yml
Ansible prompts for the password and its confirmation before the editor appears. Pick a long passphrase, because anyone with the repository can run an offline guessing attack against the file:
New Vault password:
Confirm New Vault password:
Put only secrets in this file, and prefix every name with vault_. That is the layout the Ansible docs recommend, and the reason becomes obvious the first time someone runs grep for a variable name and finds nothing because the file is ciphertext. These are the two values we saved:
---
vault_db_password: "Db-Pass-2026-Lab"
vault_api_token: "tok-lab-8f3c2a91"
Next to it, a plain-text vars.yml maps readable names onto the vaulted ones. Playbooks and templates then use db_password, and anyone reading the repository can see which variables exist without the password:
vim group_vars/web/vars.yml
Add the mapping. Nothing here is secret, so this file stays readable in code review, and you can rename or add variables without opening the vault at all.
---
db_user: appuser
db_password: "{{ vault_db_password }}"
api_token: "{{ vault_api_token }}"
Look at what landed on disk. ansible-vault create wrote the file with mode 0600, which we didn’t ask for, and the first line is the header Ansible uses to recognise vaulted content. Git doesn’t record that mode beyond the executable bit, so a fresh clone gets your umask’s default instead, but that matters less than it looks because the content is ciphertext either way:
ls -l group_vars/web/vault.yml
head -3 group_vars/web/vault.yml
1.1 is the format version used when no vault ID is given, and AES256 is the only cipher Vault supports. The rest is hex-encoded ciphertext that is safe to commit:
-rw------- 1 josphat josphat 614 Oct 17 21:16 group_vars/web/vault.yml
$ANSIBLE_VAULT;1.1;AES256
37376134303665313761326138663165373566663938613332366363356332353939383465643034
3534326136376132323266636531313134663038353838650a653539616536393638363237656362
Typing the password for every command gets old within minutes, so store it in a file that only you can read. We generate a random one for anything beyond a demo. Note that .vault_pass must never go into Git, which the last section enforces with .gitignore:
vim .vault_pass
chmod 600 .vault_pass
Put the same password you typed at the prompt on the first line and save. Ansible strips the trailing newline from password files, so a normal editor save is fine. Check that it opens the vault:
ansible-vault view group_vars/web/vault.yml --vault-password-file .vault_pass
The decrypted YAML prints to the terminal and the file on disk stays encrypted. If you get a decryption error here instead, the password in the file does not match the one you typed at the prompt, so fix that now rather than in the middle of a playbook run.
---
vault_db_password: "Db-Pass-2026-Lab"
vault_api_token: "tok-lab-8f3c2a91"
From here on, every command that needs the vault takes --vault-password-file .vault_pass, until the last section moves that into ansible.cfg.
Encrypt and decrypt existing files
Files that already exist, such as a TLS private key or a dumped credentials file, get encrypted in place with ansible-vault encrypt. Vault works on any file, not only YAML, and the copy module decrypts vaulted source files automatically when it pushes them to a host. We used a dummy key file, created with one command so you can follow along without a real key:
printf '%s\n' '-----BEGIN PRIVATE KEY-----' 'lab-only-not-a-real-key' '-----END PRIVATE KEY-----' > app.key
ansible-vault encrypt app.key --vault-password-file .vault_pass
At a terminal, the command answers Encryption successful. In a script or a CI log you see nothing, because ansible-core only prints that line (and Decryption successful) when stdout is a TTY; we confirmed both cases on the VM. Check the header with head -1 if you want proof in a pipeline. Run it a second time by mistake and Vault refuses rather than wrapping the ciphertext in another layer, which is the behaviour you want, because a double-encrypted file would decrypt to ciphertext and fail in a confusing way at deploy time:
[ERROR]: input is already encrypted
To read an encrypted file without rewriting it, send the decrypted output to stdout. ansible-vault decrypt without --output writes plain text back to disk, which is exactly how secrets end up committed by accident, so we’d reserve that form for files you are retiring from Vault:
ansible-vault decrypt app.key --output - --vault-password-file .vault_pass
The key prints, and if you run head -1 app.key afterwards the vault header is still the first line, so nothing was written back. That is the form to use in scripts that need the plain text for a moment. At a terminal a Decryption successful line comes first, which a redirect into a file or pipe does not capture:
Decryption successful
-----BEGIN PRIVATE KEY-----
lab-only-not-a-real-key
-----END PRIVATE KEY-----
Check for a stray plain-text copy with git status after any decrypt, since an editor or a redirect can leave one behind without telling you.
Encrypt single values with encrypt_string
When a vars file holds twenty settings and two secrets, encrypting the whole file makes every code review useless. encrypt_string encrypts one value and prints a YAML block you paste into an otherwise readable file. To keep the secret out of your shell history, don’t put it on the command line at all: run the command with --stdin-name and type the value when it asks.
ansible-vault encrypt_string --vault-password-file .vault_pass --stdin-name smtp_password
Ansible waits for input. Type the value without pressing Enter, then press Ctrl-D twice, exactly as the prompt says, so no newline ends up inside the secret:
Reading plaintext input from stdin. (ctrl-d to end input, twice if your content does not already have a newline)
SECRET
Encryption successful
smtp_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
38653864626539386331643839336462376262396538343233303833636161356631656234623132
3736653065623832666234626539353139333730653137660a626161643164643937323638343339
61663265633963616436333734343030306361343961396562616538393631386632626261363065
3035643032366431650a616665623635613030626464663963653865373165353262633339373962
3637
The !vault tag tells the YAML loader to hand the block to Vault at runtime. Paste everything from smtp_password: down, indentation included, into the vars file. If you’d rather not count Ctrl-D presses, --prompt asks for the variable name and then the value with input hidden; we checked that both forms stored exactly six characters for SECRET.
Piping the value in with echo is common in scripts, and it carries a trap besides the history entry. We encrypted the same word with plain echo and with echo -n into one file:
{ echo SECRET | ansible-vault encrypt_string --vault-password-file .vault_pass --stdin-name with_echo; echo -n SECRET | ansible-vault encrypt_string --vault-password-file .vault_pass --stdin-name with_echo_n; } > /tmp/strings.yml
Then we loaded both values and printed their lengths:
ansible localhost -m ansible.builtin.debug -a 'msg="{{ with_echo | length }} vs {{ with_echo_n | length }}"' -e @/tmp/strings.yml --vault-password-file .vault_pass
Plain echo stored seven characters for a six-letter secret, because the newline went into the ciphertext too. You won’t see it in any output, since a trailing newline is invisible in a terminal.
localhost | SUCCESS => {
"msg": "7 vs 6"
}
A database or an API compares that newline like any other character, so the login fails with a password that looks right in every log. In scripts use printf '%s' or echo -n, and remember that the value still lands in shell history when you type the pipe by hand. Passing it as an argument with --name smtp_password 'SECRET' has the same history problem.
Use vault secrets in a playbook
Vaulted variables load like any others, so the playbook needs no vault-specific syntax. This one writes an app config containing the password and token to the managed host. We ran it exactly as shown:
vim site.yml
Note the no_log: true on the task that handles secrets. It looks optional, because the play runs fine without it, but the next section shows what you would be printing to every log without it. Paste the playbook:
---
- name: Deploy app config with vaulted secrets
hosts: web
become: true
tasks:
- name: Create the app directory
ansible.builtin.file:
path: /opt/app
state: directory
mode: "0750"
- name: Write the config with the database password
ansible.builtin.copy:
dest: /opt/app/config.ini
mode: "0600"
content: |
[database]
user = {{ db_user }}
password = {{ db_password }}
[api]
token = {{ api_token }}
no_log: true
- name: Show which user the app connects as
ansible.builtin.debug:
msg: "App connects as {{ db_user }}"
Run it once without any password option, the way a new teammate will. Seeing this error on your own machine first means you will recognise it when they paste it into chat.
ansible-playbook site.yml
Ansible stops as soon as it loads group_vars/web/vault.yml, before any task runs, because group variables are read when the play starts. No host was touched.
PLAY [Deploy app config with vaulted secrets] **********************************
[ERROR]: Attempting to decrypt but no vault secrets found.
Give it the password file and the play completes. The config task reports changed without printing its content, which is no_log doing its job.
ansible-playbook site.yml --vault-password-file .vault_pass
Our first run created the directory and the file on the managed host. A second run reports changed=0, which you can use as a quick check that a secret did not change underneath you.
PLAY [Deploy app config with vaulted secrets] **********************************
TASK [Gathering Facts] *********************************************************
ok: [ansible01]
TASK [Create the app directory] ************************************************
changed: [ansible01]
TASK [Write the config with the database password] *****************************
changed: [ansible01]
TASK [Show which user the app connects as] *************************************
ok: [ansible01] => {
"msg": "App connects as appuser"
}
PLAY RECAP *********************************************************************
ansible01 : ok=4 changed=2 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
On the host, /opt/app/config.ini holds the decrypted values with mode 600 and owner root, because Vault only protects the copy on the control node. Whatever reads the file on the target is your responsibility, so set mode on every task that writes a secret.
For interactive runs, --ask-vault-pass prompts once per run. It’s the right choice on a shared bastion where you don’t want a password file sitting in anyone’s home directory, but it rules out cron and CI, since nothing can answer the prompt. A wrong password fails the same way a missing ID does, and the message doesn’t say whether the password or the vault ID was the problem:
ansible-playbook site.yml --ask-vault-pass
This is what we got after typing a deliberately wrong password at the prompt. You also get this text when the password for that file’s vault ID was never supplied, or when the right password is supplied under a different label while vault_id_match is on, so check the file header before you assume a typo.
Vault password:
PLAY [Deploy app config with vaulted secrets] **********************************
[ERROR]: Decryption failed (no vault secrets were found that could decrypt).
Nothing on the managed host changed in either failed run, because decryption happens on the control node before the first task is sent.
Edit vaulted files and rotate the password
ansible-vault edit decrypts into a temporary file, opens your editor and re-encrypts on save. Use it instead of decrypt, edit and encrypt, which leaves a plain-text copy on disk between steps:
ansible-vault edit group_vars/web/vault.yml --vault-password-file .vault_pass
We changed vault_db_password from Db-Pass-2026-Lab to Db-Pass-2026-Rotated and compared checksums before and after. The whole file changed, not one line, because Vault re-encrypts with a fresh salt on every save. Every edit therefore shows up in Git as a full rewrite of the file, and a reviewer cannot see which secret moved, which is one more argument for the vars.yml and vault.yml split.
Rotating the vault password itself is rekey, and you’ll need it whenever someone who held the password leaves the team. It takes several files at once, so rotate every file that shares a password in one command; doing them one by one leaves a window where half the repository needs the old password and half the new one. A recursive grep -rl '^.ANSIBLE_VAULT' --exclude-dir=.git . finds every vaulted file in a project, including non-YAML ones such as app.key; in our lab it listed all five. Generate the new password into its own file first:
openssl rand -base64 32 > .vault_pass_new
chmod 600 .vault_pass_new
ansible-vault rekey group_vars/web/vault.yml app.key --vault-password-file .vault_pass --new-vault-password-file .vault_pass_new
Vault confirms once for the batch. From this moment the old password file no longer opens either file, so swap it in before anything else runs:
Rekey successful
Move the new password into place. Until you do, every playbook run fails with the decryption error, which is a good reason to run rekey and this move together in one change.
mv .vault_pass_new .vault_pass
Rekey only handles whole vaulted files. Values created with encrypt_string live inside plain YAML, and pointing rekey at such a file fails, because the file as a whole isn’t vault data even though parts of it are. You won’t find out until rotation day unless you test it now, so we pointed rekey at the strings file from earlier:
ansible-vault rekey /tmp/strings.yml --vault-password-file .vault_pass --new-vault-password-file .vault_pass
It refuses before changing anything:
[ERROR]: Failed to rekey '/tmp/strings.yml': Input is not vault encrypted data.
Those values have to be re-encrypted one by one with encrypt_string and pasted back. That chore is the main reason we keep secrets in whole vault.yml files and use encrypt_string only for the odd value in a shared file. Also remember that rekey changes the lock, not the secrets: if a password leaked, rotate the database password and the API token themselves as well.
Keep decrypted values out of logs
Vault doesn’t mask anything at runtime. Once a value is decrypted it’s an ordinary string, and any module that echoes its arguments will print it, so the protection ends at the moment the play loads the file. Plenty of Vault tutorials claim the opposite, so we tested it rather than take either side on trust. We checked the two most common leaks against the same vault. First, a debug call:
ansible web -m ansible.builtin.debug -a var=db_password --vault-password-file .vault_pass
The password prints in full. Any teammate who reads the job log or the terminal scrollback now has it, and the vault did nothing to stop that.
ansible01 | SUCCESS => {
"db_password": "Db-Pass-2026-Rotated"
}
Second, --diff, which teams switch on in CI to review changes. A copy task that writes a secret shows it in the diff:
ansible web -m ansible.builtin.copy -a "dest=/tmp/leak.txt content={{ db_password }}" --diff --vault-password-file .vault_pass
The value we set with ansible-vault edit earlier sits right there in the diff output. A CI system that archives job logs now holds the current production credential.
--- before
+++ after: /tmp/leak.txt
@@ -0,0 +1 @@
+Db-Pass-2026-Rotated
\ No newline at end of file
That demo leaves a world-readable /tmp/leak.txt on the host, so remove it. The --vault-password-file is needed even here, because group_vars/web/vault.yml loads for every command against web; without it the cleanup failed with Attempting to decrypt but no vault secrets found:
ansible web -m ansible.builtin.file -a "path=/tmp/leak.txt state=absent" --vault-password-file .vault_pass
The same task in site.yml carries no_log: true. Run the playbook with the same --diff plus verbose output to compare:
ansible-playbook site.yml --diff -v --vault-password-file .vault_pass
Right after the edit, its result was replaced by a censored message instead of a diff. The task still reports changed, so you keep the signal that something happened without the content:
TASK [Write the config with the database password] *****************************
changed: [ansible01] => {"censored": "the output has been hidden due to the fact that 'no_log: true' was specified for this result", "changed": true}
So the rule we follow is simple: every task whose arguments contain a vaulted variable gets no_log: true, and nobody debugs a secret by printing it. Print its length or a hash instead. The cost is that a failing no_log task tells you almost nothing, so when you have to debug one, flip it off on a lab host, never in a CI job whose logs are kept. The Ansible debugging guide covers safer ways to inspect a failing task.
Separate environments with vault IDs
A vault ID is a label paired with a password source, written as label@source. With one ID for dev and another for prod, developers can hold the dev password without ever being able to open production secrets. Create a password file per environment and encrypt each environment’s file with its own ID:
openssl rand -base64 32 > .vault_pass_dev
openssl rand -base64 32 > .vault_pass_prod
chmod 600 .vault_pass_dev .vault_pass_prod
printf '%s\n' '---' 'vault_env_secret: dev-only-value' > vars/dev_vault.yml
printf '%s\n' '---' 'vault_env_secret: prod-only-value' > vars/prod_vault.yml
ansible-vault encrypt vars/dev_vault.yml --vault-id [email protected]_pass_dev
ansible-vault encrypt vars/prod_vault.yml --vault-id [email protected]_pass_prod
Each file holds one variable with the same name, which makes it easy to see later which one won. Typing secrets into printf is fine for this dummy data; for real values use ansible-vault create --vault-id [email protected]_pass_dev instead. The label goes into the header in plain text, and the format version moves to 1.2:
$ANSIBLE_VAULT;1.2;AES256;dev
$ANSIBLE_VAULT;1.2;AES256;prod
A small playbook loads both files, so you can see what happens when one password is missing. Create it in the project root:
vim envs.yml
It only prints the length of the secret, following the rule from the logging section. A length proves the file decrypted without putting the value anywhere you can’t take back.
---
- name: Read secrets from two vault IDs
hosts: localhost
gather_facts: false
vars_files:
- vars/dev_vault.yml
- vars/prod_vault.yml
tasks:
- name: Prove both files decrypted
ansible.builtin.debug:
msg: "secret length is {{ vault_env_secret | length }}"
With only the dev ID, the run fails on the prod file and names it, which is more useful than the generic message from earlier. The dev file loaded fine, so the problem is clearly the missing prod password.
ansible-playbook envs.yml --vault-id [email protected]_pass_dev
The error shows exactly which file needed a secret you did not supply. Compare that with the bare message from the single-password case: once files carry vault IDs, you can tell which password is missing from the path alone.
[ERROR]: Invalid vars_files file '/home/josphat/vault-lab/vars/prod_vault.yml': Decryption failed (no vault secrets were found that could decrypt).
Pass both IDs and the play prints secret length is 15, the length of prod-only-value, because the later file in vars_files wins when two files define the same variable. Give each environment’s variables distinct names if a play ever loads both.
ansible-playbook envs.yml --vault-id [email protected]_pass_dev --vault-id [email protected]_pass_prod
Three behaviours of IDs caught us out, and all of them come straight from how Vault treats labels. Each one is easy to check on your own files.
- Labels are hints by default:
--vault-id [email protected]_pass_devstill opened the dev file ANSIBLE_VAULT_ID_MATCH=truemakes the same command fail, as the label must match- With two IDs loaded, encrypting needs
--encrypt-vault-idto pick one
The first point is documented: Ansible tries the matching label first, then every other secret you supplied, so a label never stops a password from being tried. Set vault_id_match = True under [defaults] in ansible.cfg if you want labels enforced. The third point produces this error when you forget the flag. Name the target ID, for example --encrypt-vault-id prod, and the command goes through.
[ERROR]: The vault-ids dev,prod are available to encrypt. Specify the vault-id to encrypt with --encrypt-vault-id
Setting vault_encrypt_identity in the project config, shown next, removes the need for the flag on every encrypt.
Load vault passwords from a script or ansible.cfg
Typing --vault-id twice on every command does not survive contact with a team. Ansible accepts three other password sources, and which one fits depends on where the playbook runs:
- Laptop or bastion:
vault_identity_listin the project’sansible.cfg - CI runner:
ANSIBLE_VAULT_PASSWORD_FILEor a script reading a CI secret - Secret manager or keyring: a client script named
*-client
A password source can be an executable script instead of a file. Ansible runs it and reads the password from stdout, which lets CI inject the password through an environment variable without ever writing it to disk. That matters on shared runners, because a password file left behind by a failed job is readable by the next job on the same machine unless the cleanup step ran. The script itself contains no secret, so it’s safe to commit next to the playbooks. Create the script:
vim vault-pass.sh
The :? expansion makes the script fail loudly when the variable is unset, instead of handing Ansible an empty password. An empty password produces the generic decryption error, which would send you hunting in the wrong place.
#!/bin/sh
printf "%s\n" "${VAULT_PASSWORD:?VAULT_PASSWORD is not set}"
Make it executable, then point Ansible at it. Mode 700 matters here because the executable bit is what tells Ansible to run the file rather than read it.
chmod 700 vault-pass.sh
ansible-vault view app.key --vault-password-file ./vault-pass.sh
With VAULT_PASSWORD unset, the guard fires and Ansible reports the script’s exit code. This is the failure with the variable unset:
/home/josphat/vault-lab/vault-pass.sh: 2: VAULT_PASSWORD: VAULT_PASSWORD is not set
[WARNING]: Error in vault password file loading (default): Vault password script /home/josphat/vault-lab/vault-pass.sh returned non-zero (2): None
[ERROR]: Vault password script /home/josphat/vault-lab/vault-pass.sh returned non-zero (2): None
Export the variable and the same command prints the key. In the lab we filled it from the password file; a CI job would fill it from its secret store instead:
export VAULT_PASSWORD="$(cat .vault_pass)"
ansible-vault view app.key --vault-password-file ./vault-pass.sh
The executable bit is the whole switch between file and script, and it bites the other way too. A plain password file that picked up chmod +x (common after copying files from a Windows share or a zip) gets executed, and fails:
[WARNING]: Error in vault password file loading (default): Problem running vault password script /tmp/plain-pass ([Errno 8] Exec format error: '/tmp/plain-pass'). If this is not a script, remove the executable bit from the file.
[ERROR]: Problem running vault password script /tmp/plain-pass ([Errno 8] Exec format error: '/tmp/plain-pass'). If this is not a script, remove the executable bit from the file.
For a script that fetches different passwords per vault ID from a secret manager, name it with a -client suffix (for example vault-keyring-client.py). Ansible then calls it with --vault-id <label> so one script can serve dev and prod; the script prints the password to stdout and should exit with code 2 when it does not know the requested ID. We did not test a client script, so check the vault password management docs for the exact contract.
On a workstation, the cleanest option is to list every ID in the project’s ansible.cfg. You stop typing flags, and the config documents which password files the project expects, so open it again.
vim ansible.cfg
This is the final file we ran. vault_identity_list loads all three password files on every command. The paths resolve against your current directory, not the config file, so running from a subdirectory with ANSIBLE_CONFIG=../ansible.cfg failed with The vault password file /home/josphat/vault-lab/sub/.vault_pass was not found; use absolute paths such as ~/vault-lab/.vault_pass if you work from subdirectories. vault_encrypt_identity settles which one new encryptions use, which also stops the --encrypt-vault-id error:
[defaults]
inventory = inventory.ini
interpreter_python = auto_silent
vault_identity_list = [email protected]_pass, [email protected]_pass_dev, [email protected]_pass_prod
vault_encrypt_identity = default
With that config, ansible-playbook site.yml and ansible-playbook envs.yml both ran with no vault flags, and ansible-config dump --only-changed confirms which settings came from the file:

The default label is special: a file encrypted under it gets the plain 1.1 header without a label, which is why the first line in the capture differs from the dev and prod files. In CI, skip the config entries and export ANSIBLE_VAULT_PASSWORD_FILE to a path the job writes from its secret store, since that variable is read by every ansible-* command just like --vault-password-file. Whatever you choose, keep the password files out of the repository. Minimal Debian 13 images lack git, so install it first if needed:
sudo apt install -y git
git init
echo ".vault_pass*" >> .gitignore
git add -A
git status --short
Our status listed the vaulted files, ansible.cfg and the script, and none of the three .vault_pass* files. Run that check before the first commit of any vault project, because a password file that reaches a remote is a rekey of every file it protects.
Decode Ansible Vault error messages
Every error below came from our Debian 13 lab on ansible-core 2.21. Most vault errors trace back to one of two causes, a password that was never supplied or one that does not match, and the message wording tells you which:
| Error | Cause | Fix |
|---|---|---|
Attempting to decrypt but no vault secrets found | No password source given at all | Add --ask-vault-pass, --vault-id or vault_identity_list |
Decryption failed (no vault secrets were found that could decrypt) | Wrong password, or that file’s vault ID never supplied | Check the file header’s label and pass that ID’s password |
input is already encrypted | encrypt run on a vaulted file | Use rekey to change the password |
Input is not vault encrypted data | rekey on a file holding inline !vault values | Re-run encrypt_string for each value |
The vault-ids dev,prod are available to encrypt | Several IDs loaded, none chosen for encryption | --encrypt-vault-id or vault_encrypt_identity |
Exec format error on a password file | Plain password file is executable | chmod 600 the file |
Vault password script ... returned non-zero | The script failed, often a missing variable | Run the script by hand and read its stderr |
The two decryption messages are the ones that cost time, because the second covers both a mistyped password and a missing ID. Read the file’s first line before anything else: a 1.2 header with a label tells you which password Ansible needed, while a bare 1.1 header carries no label at all. Unless you set vault_id_match = True under [defaults], Ansible tries every supplied password against either kind. For the full command list in one place, the Ansible cheat sheet has the vault flags next to the rest of the CLI, and the next chapter of this series moves on to dynamic inventories.