How to Encrypt Secrets in Ansible Playbooks With Ansible Vault
A hands-on tutorial that installs Ansible, encrypts secrets with ansible-vault, wires them into a working playbook, and covers vault IDs and password rotation.
It is easy to find a playbook on GitHub with a database password, an API token, or a TLS private key sitting in plain text inside a variables file. It usually is not malicious: someone was moving fast, the repository started private, and encrypting the file felt like a step for later. Later rarely comes, and once a secret is in git history, it is effectively permanent even if you delete the file in a following commit. Ansible Vault exists to remove that excuse. It is a built-in feature of Ansible, the open source automation tool that runs playbooks (YAML files describing tasks) against servers, and it encrypts sensitive variables or entire files so you can commit them to source control right alongside your normal, readable configuration.
Table Of Content
- What Ansible Vault actually protects, and what it does not
- Prerequisites
- Step 1: Install Ansible in an isolated environment
- Confirm the install and check the version
- Step 2: See the problem Ansible Vault solves
- Step 3: Encrypt an existing file with ansible-vault encrypt
- View the encrypted content without leaving plaintext on disk
- Step 4: Encrypt a single value inline with encrypt_string
- Step 5: Edit, decrypt, and a real gotcha with interactive editors
- Step 6: Wire an encrypted file into a real playbook
- Three more ways to supply the vault password
- Step 7: Keep dev and production secrets apart with vault IDs
- Step 8: Rotate a vault password with rekey
- Common mistakes and gotchas
- How to verify everything works end to end
- Next steps
This tutorial installs Ansible from scratch, creates a real secrets file, encrypts it, and wires it into a working playbook that decrypts the values automatically at run time. Along the way you will encrypt a single inline value, edit and rotate encrypted content, supply the vault password four different ways, and keep development and production secrets apart with separate vault IDs. Every command below was run against a live installation of ansible-core 2.19.11 on Ubuntu 26.04 LTS with Python 3.11.15 while writing this tutorial, and every mechanism described was cross-checked against the official Ansible Vault documentation. Nothing here needs a remote server: everything targets your own machine over a local connection, so there is no infrastructure to provision and nothing to clean up afterward except a scratch directory.
What Ansible Vault actually protects, and what it does not
Ansible’s own documentation states this warning plainly, and it is worth understanding before you type a single command: encryption with Ansible Vault only protects “data at rest.” A vault-encrypted file sitting on disk, or checked into git, is unreadable without the password. The moment Ansible decrypts that value to actually use it, whether to log into a database or print a debug message, it becomes “data in use,” plain text again, at least for that moment. If a playbook prints a decrypted password to your terminal or to a CI log, Vault never had any way to prevent that; that responsibility belongs to whoever wrote the playbook (Ansible’s no_log task option exists for exactly this reason, to suppress a task’s output when it might contain something sensitive). Keep that distinction in mind throughout this tutorial: Vault protects the file, not every place the secret might end up once decrypted.
Ansible can encrypt two different kinds of content, and picking between them matters:
- Whole files. Every line becomes unreadable ciphertext. This is simple and thorough, but it means you cannot see at a glance what variables a file defines, and diffing changes in git shows only that “the encrypted blob changed,” not what changed inside it.
- Individual variables, inline. A single value is encrypted in place with
ansible-vault encrypt_string, while the rest of the YAML file stays readable. This keeps files diff-friendly and lets you mix plain and encrypted values, at the cost of one extra command each time you add a new secret.
This tutorial covers both, starting with whole-file encryption because it is what you will reach for most often.
Prerequisites
- A Linux machine or VM with terminal access. This tutorial was verified on Ubuntu 26.04 LTS, but the commands are identical on any modern Linux distribution, since Ansible and its vault tooling are pure Python and do not depend on distribution-specific packaging.
- Python 3.11 or newer and
pip, since that is the minimum Python versionansible-core 2.19.11(the version this tutorial uses) requires; both ship with current Ubuntu releases. - Basic comfort reading YAML and running shell commands. No prior Ansible experience is assumed; every Ansible-specific concept (playbooks, inventories, variables) is explained the first time it appears.
- No remote servers, cloud account, or special permissions are required. Every example targets
localhost.
Step 1: Install Ansible in an isolated environment
Installing into a Python virtual environment keeps this tutorial’s dependencies away from any system Python packages, and makes the whole thing trivial to remove afterward by deleting one directory.
# Context: Ubuntu 26.04 LTS, any working directory you want to experiment in.
# Purpose: create an isolated Python environment and install the Ansible engine into it.
mkdir ansible-vault-tutorial && cd ansible-vault-tutorial
python3 -m venv venv
source venv/bin/activate
pip install --upgrade pip
pip install ansible-core
Notice this installs ansible-core, not the plain ansible package. ansible-core is the engine itself: the ansible, ansible-playbook, and ansible-vault commands, plus a small set of built-in modules. The larger ansible package additionally bundles a large set of community-maintained collections for specific platforms (cloud providers, network devices, and so on). None of that bundle is needed here, so installing just ansible-core is faster and keeps the environment smaller.
Confirm the install and check the version
ansible-vault --version
Expected output (abbreviated): ansible-vault [core 2.19.11], followed by details about the config file, module search path, and Python interpreter in use. If you instead see command not found, confirm the virtual environment is active; you should see (venv) at the start of your shell prompt. Every command from here on assumes that prompt is present.
Step 2: See the problem Ansible Vault solves
Create a small variables file the way most people do it before they learn better, as plain, readable YAML:
cat > secrets.yml << 'EOF'
db_password: "S3cur3DbP@ss!"
api_key: "sk-test-1234567890abcdef"
EOF
cat secrets.yml
Expected output: the two lines above, printed back exactly as written. If this file were committed to a git repository right now, anyone with read access to that repository, including anyone who forks it, downloads a released archive, or gains access later even after the file is deleted in a newer commit, could read both values straight out of git history. That is the exact problem the rest of this tutorial solves.
Step 3: Encrypt an existing file with ansible-vault encrypt
First, create a password to protect the file, and store it somewhere Ansible can read it from:
echo "supersecretvaultpassword123" > .vault_pass.txt
chmod 600 .vault_pass.txt
The chmod 600 restricts the file to your own user account only. This password file must never be committed to git; add it to .gitignore immediately (echo ".vault_pass.txt" >> .gitignore) before you forget. Now encrypt the secrets file in place:
ansible-vault encrypt secrets.yml --vault-password-file .vault_pass.txt
Expected output: Encryption successful. Look at the file now:
head -c 200 secrets.yml
Expected output (your ciphertext will differ, encryption is randomized): a first line reading $ANSIBLE_VAULT;1.1;AES256, followed by wrapped hexadecimal text. That header is not part of the secret; it identifies the vault format version (1.1) and the cipher used to encrypt the content, AES256 per the official Ansible documentation. The file is now safe to commit to git exactly as it sits on disk.
View the encrypted content without leaving plaintext on disk
You rarely want to fully decrypt a file just to check what is inside it. ansible-vault view decrypts to your terminal only, never writing plaintext back to disk:
ansible-vault view secrets.yml --vault-password-file .vault_pass.txt
Expected output: the original two lines, db_password and api_key, printed in plaintext to your terminal. Try it again with the wrong password to see how Ansible responds to a mismatch:
echo "wrongpassword" > .wrong_pass.txt
ansible-vault view secrets.yml --vault-password-file .wrong_pass.txt
Expected output: [ERROR]: Decryption failed (no vault secrets were found that could decrypt). for secrets.yml, and a nonzero exit code. Remember that exact message; you will see it again any time a playbook run is missing the correct password for something it needs to decrypt.
Step 4: Encrypt a single value inline with encrypt_string
Sometimes encrypting an entire file is overkill, especially in a variables file that mixes ordinary settings with just one or two sensitive values. ansible-vault encrypt_string encrypts a single value and prints YAML you can paste directly into an existing file:
ansible-vault encrypt_string 'MySrvPassw0rd!' --name 'service_password' --vault-password-file .vault_pass.txt
Expected output: a block that starts with service_password: !vault |, followed by an indented $ANSIBLE_VAULT;1.1;AES256 block. The !vault tag is what tells Ansible's YAML parser that this particular value needs decrypting before use, even though the rest of the file around it is ordinary plaintext YAML. Paste that output straight into any variables file, right alongside plaintext keys, and Ansible will decrypt only that one value at run time.
Step 5: Edit, decrypt, and a real gotcha with interactive editors
To modify an encrypted file's contents without manually decrypting and re-encrypting it, use ansible-vault edit. It decrypts the file to a temporary location, opens your $EDITOR, and re-encrypts the result on save, deleting the temporary file afterward.
ansible-vault edit secrets.yml --vault-password-file .vault_pass.txt
This opens your configured terminal editor (vim by default on most Ubuntu installs) with the decrypted content loaded. Make a change, save, and quit, and Ansible re-encrypts automatically. Here is a gotcha worth knowing about, confirmed by testing both commands directly outside of a real terminal session: ansible-vault create performs an explicit check and refuses outright without a real terminal, failing immediately with [ERROR]: not a tty, editor cannot be opened (exit code 5) before it ever tries to open an editor. ansible-vault edit does not perform that same upfront check; instead it goes ahead and launches $EDITOR regardless, and what happens next depends entirely on the editor itself. With vim as the default, it printed its own warnings ("Output is not to a terminal," "Input is not from a terminal") and exited without saving, leaving the vault file safely untouched. Either way, the practical lesson is the same: neither command is suitable for scripts or CI. For anything you need to automate or run in CI, encrypt a plaintext file with ansible-vault encrypt (Step 3) or generate a value with encrypt_string (Step 4) instead; neither requires a terminal.
There is a second, easy-to-miss risk buried in "opens your editor": most editors create swap files or backups while you work, as an autosave safety net, and those temporary files land on disk as plain text, completely outside Ansible's control. The official documentation dedicates an entire section to this under "Steps to secure your editor," and for vim specifically recommends adding set noswapfile, set nobackup, set nowritebackup, and set viminfo= to your vim configuration before editing vault files with it. If you use a different editor as your default, check whether it has an equivalent autosave or backup feature and disable it the same way.
Finally, to permanently remove encryption from a file (for example, if you are retiring Vault in favor of a different secrets tool), use ansible-vault decrypt:
ansible-vault decrypt secrets.yml --vault-password-file .vault_pass.txt
Expected output: Decryption successful, and the file is now back to plain, readable YAML on disk. Re-encrypt it before moving on to the next step, since the rest of this tutorial assumes secrets.yml is encrypted: ansible-vault encrypt secrets.yml --vault-password-file .vault_pass.txt.
Step 6: Wire an encrypted file into a real playbook
Encryption only matters if Ansible can actually use the secret when it runs. Build a minimal project structure: an inventory (the list of hosts Ansible manages, here just your own machine), a group_vars directory (variables automatically loaded for a group of hosts), and a playbook (the YAML file describing what to do).
mkdir -p group_vars/local
cat > inventory.ini << EOF
[local]
localhost ansible_connection=local ansible_python_interpreter=$(readlink -f venv/bin/python3)
EOF
cp secrets.yml group_vars/local/vault.yml
cat > site.yml << 'EOF'
---
- name: Demonstrate reading vault-encrypted variables
hosts: local
gather_facts: false
tasks:
- name: Show the decrypted database password
ansible.builtin.debug:
msg: "Connecting to the database with password: {{ db_password }}"
- name: Show the decrypted API key
ansible.builtin.debug:
msg: "Using API key: {{ api_key }}"
EOF
The [local] group in inventory.ini tells Ansible to manage your own machine directly over a local connection instead of SSH, which is why ansible_connection=local is set. Any file placed in group_vars/<group-name>/, such as group_vars/local/vault.yml here, is loaded automatically for every host in that group; Ansible does not need to be told to read it. Try running the playbook without supplying a password first, to see how Ansible fails when it cannot decrypt something it needs:
ansible-playbook -i inventory.ini site.yml
Expected output: [ERROR]: Attempting to decrypt but no vault secrets found. Ansible correctly detected that group_vars/local/vault.yml is encrypted and refused to guess. Now supply the password:
ansible-playbook -i inventory.ini site.yml --vault-password-file .vault_pass.txt
Expected output:
TASK [Show the decrypted database password] ***********************************
ok: [localhost] => {
"msg": "Connecting to the database with password: S3cur3DbP@ss!"
}
TASK [Show the decrypted API key] **********************************************
ok: [localhost] => {
"msg": "Using API key: sk-test-1234567890abcdef"
}
PLAY RECAP **********************************************************************
localhost : ok=2 changed=0 unreachable=0 failed=0 skipped=0 rescued=0 ignored=0
Ansible found the encrypted file, decrypted it using the supplied password, and substituted the real values into both debug messages. This is also a good moment to internalize the "data in use" warning from earlier: those two values are now sitting in plain text in your terminal scrollback, exactly as intended for this demo, but it is a real illustration of how a careless debug task in a production playbook could leak a secret to a CI log even though the file on disk stayed encrypted the entire time.
Three more ways to supply the vault password
--vault-password-file is convenient for a tutorial, but typing it on every command gets old fast. Ansible supports three alternatives, all verified working here:
# Option 1: environment variable, useful in CI pipelines
ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass.txt ansible-playbook -i inventory.ini site.yml
# Option 2: a project-level default in ansible.cfg
cat > ansible.cfg << 'EOF'
[defaults]
inventory = ./inventory.ini
vault_password_file = ./.vault_pass.txt
EOF
ansible-playbook site.yml
# Option 3: interactive prompt, no password file at all
ansible-playbook -i inventory.ini site.yml --ask-vault-pass
All three produce identical output to the explicit-flag version above. Option 2 is the most convenient for solo, local work, since it removes the need to pass anything on the command line at all, once ansible.cfg is in place, plain ansible-playbook site.yml just works. Option 3 prompts for the password interactively and is the only option here that involves no file on disk at all, useful when you would rather type a password than store it anywhere, even briefly.
Step 7: Keep dev and production secrets apart with vault IDs
A single shared vault password works for a personal project, but it breaks down the moment you have separate secrets for different environments, for example a development database password everyone on the team can see, and a production one only a few people should ever decrypt. Vault IDs solve this by letting you tag encrypted content with a label and pair each label with its own password.
echo "devpassword111" > .vault_pass_dev.txt
echo "prodpassword999" > .vault_pass_prod.txt
cat > dev_secrets.yml << 'EOF'
env_name: "development"
db_password: "dev-password-not-real"
EOF
cat > prod_secrets.yml << 'EOF'
env_name: "production"
db_password: "Pr0d-P@ssw0rd-Rotate-Me"
EOF
ansible-vault encrypt dev_secrets.yml --vault-id [email protected]_pass_dev.txt --encrypt-vault-id dev
ansible-vault encrypt prod_secrets.yml --vault-id [email protected]_pass_prod.txt --encrypt-vault-id prod
The --vault-id label@source pattern pairs a label (dev, prod, anything you choose) with a password source, here a file. A real gotcha surfaced while verifying this: since this project's ansible.cfg from Step 6 already sets a default vault_password_file, running the encrypt command above without an explicit --encrypt-vault-id fails with [ERROR]: The vault-ids dev,default are available to encrypt. Specify the vault-id to encrypt with --encrypt-vault-id, because Ansible then sees two candidate vault IDs and refuses to guess which one you meant. Adding --encrypt-vault-id dev (or prod) resolves the ambiguity, as shown above. Check the header Ansible wrote to confirm the label was stored:
head -1 dev_secrets.yml
Expected output: $ANSIBLE_VAULT;1.2;AES256;dev. Notice the format version is now 1.2, not 1.1; format 1.2 is what adds room for the vault ID label in the header, and Ansible switches to it automatically whenever you encrypt with --vault-id. Now update the playbook to load only the dev secrets, and run it supplying just the dev vault ID:
cat > playbook_multi_vault.yml << 'EOF'
---
- name: Demonstrate multiple vault IDs
hosts: local
gather_facts: false
vars_files:
- dev_secrets.yml
tasks:
- name: Show which environment this is
ansible.builtin.debug:
msg: "Environment {{ env_name }} password is {{ db_password }}"
EOF
ansible-playbook -i inventory.ini playbook_multi_vault.yml --vault-id [email protected]_pass_dev.txt
Expected output: "msg": "Environment development password is dev-password-not-real". Only the dev password was needed, and only the dev password was supplied.
Here is a subtlety worth understanding rather than assuming: the label you attach with --vault-id is, by default, just a hint for humans and tooling, not a strict cryptographic boundary. Ansible's own documentation confirms this directly: "Ansible does not enforce using the same password every time you use a particular vault ID label," and by default Ansible tries every vault password you supply against every piece of encrypted content it needs to decrypt, regardless of whether the labels match. Verifying this directly: running ansible-playbook -i inventory.ini playbook_multi_vault.yml --vault-id [email protected]_pass_prod.txt (the correct label, dev, paired with the wrong password file, the production one) still correctly fails to decrypt dev_secrets.yml, because the actual password content does not match, proving that the real security boundary is the password itself, not the label attached to it. If you want Ansible to strictly enforce that a label only ever decrypts content encrypted under that same label, set the DEFAULT_VAULT_ID_MATCH configuration option; without it, labels are for organization and clarity, and the passwords are what actually gate access.
Step 8: Rotate a vault password with rekey
Passwords should not live forever. ansible-vault rekey re-encrypts a file under a new password without needing to manually decrypt and re-encrypt it yourself:
echo "brandnewvaultpassword456" > .new_vault_pass.txt
ansible-vault rekey secrets.yml --vault-password-file .vault_pass.txt --new-vault-password-file .new_vault_pass.txt
Expected output: Rekey successful. Confirm the old password no longer works, and the new one does:
ansible-vault view secrets.yml --vault-password-file .vault_pass.txt
# [ERROR]: Decryption failed (no vault secrets were found that could decrypt). for secrets.yml
ansible-vault view secrets.yml --vault-password-file .new_vault_pass.txt
# db_password: "S3cur3DbP@ss!"
# api_key: "sk-test-1234567890abcdef"
Rotate the password back if you are following along and want the rest of your files consistent: ansible-vault rekey secrets.yml --vault-password-file .new_vault_pass.txt --new-vault-password-file .vault_pass.txt.
Common mistakes and gotchas
- Trying to script
ansible-vault createoredit. Both are meant for a human at a real terminal.createchecks upfront and fails immediately withnot a tty, editor cannot be opened;editskips that check and launches your editor anyway, which then fails in whatever way that editor fails without a real terminal. Useencrypton a plaintext file, orencrypt_stringfor a single value, in any automated or CI context. - Forgetting
--encrypt-vault-idwhen a default password is already configured. Ifansible.cfgsets a defaultvault_password_fileand you also pass--vault-id label@sourceto encrypt something, Ansible sees two candidate vault IDs and refuses to guess; pass--encrypt-vault-id <label>to resolve it. - Assuming a vault ID label is a security boundary. By default it is not. Ansible tries every supplied password against every piece of encrypted content regardless of label, and only the password content itself determines whether decryption succeeds. Set
DEFAULT_VAULT_ID_MATCHif you specifically need strict label enforcement. - Leaving your editor's autosave or backup files enabled.
ansible-vault editdecrypts to a temporary file for your editor to open; if that editor also writes its own swap or backup file during the session, the plaintext can end up on disk somewhere Ansible never manages or cleans up. Disable swap and backup files for vault editing sessions, as the official docs recommend for vim. - Committing the vault password file itself. The password file protects everything encrypted with it; treat it with the same care as the secrets inside your vault files, keep it out of git with
.gitignore, and restrict its permissions withchmod 600. - Forgetting that a playbook run decrypts everything it references, not just what a given task uses. If a playbook's
vars_filesorgroup_varspull in several encrypted files, Ansible needs a working password for all of them before the play starts, even ones only used deep inside a role you are not actively testing.
How to verify everything works end to end
- Run
ansible-vault view secrets.yml --vault-password-file .vault_pass.txtand confirm you see the plaintextdb_passwordandapi_keyvalues, proving the file is both encrypted on disk and readable with the right password. - Run
ansible-playbook -i inventory.ini site.yml --vault-password-file .vault_pass.txtand confirm both debug tasks reportokwith the real decrypted values in the message, and thatPLAY RECAPshows zero failures. - Run the same playbook with no vault password supplied at all and confirm it fails cleanly with
Attempting to decrypt but no vault secrets foundrather than silently skipping the encrypted variables. - Run
ansible-playbook -i inventory.ini playbook_multi_vault.yml --vault-id [email protected]_pass_dev.txtand confirm it succeeds using only the dev password, then try substituting the prod password file under the dev label and confirm it correctly fails to decrypt. - Rekey
secrets.ymlto a new password and confirm the old password file now fails while the new one succeeds, proving the rotation actually took effect rather than silently keeping the old password valid.
Next steps
Once this workflow feels natural, the next gap to close is making sure a secret never reaches a plaintext file in the first place, even briefly during editing; pairing Ansible Vault with a pre-commit secret scanner like the one covered in this Gitleaks tutorial catches the case where someone forgets to encrypt a file before committing it. For teams that have outgrown password files shared over chat or email, look into Ansible's support for vault password client scripts, which can pull a password from a proper secrets manager at run time instead of a file on disk, or, at real organizational scale, Red Hat's Ansible Automation Platform, which replaces vault password files entirely with a managed credential store (a topic covered from a different angle in this Ansible Automation Platform BYOK checklist). Whichever direction you take next, the core habit from this tutorial does not change: secrets belong in an encrypted, version-controlled file, never a plaintext one, and the password that protects them deserves exactly as much care as the secrets themselves.








No Comment! Be the first one.