Ansible Vault: A Practical Deep Dive
Ansible runs need secrets at runtime: passwords, API tokens, private keys, TLS material, and cloud credentials. You don't have to keep those in Git. External stores, CI variables, and untracked local files are all valid patterns. Ansible Vault is for the case where you want encrypted variables and files versioned next to the playbooks, roles, and inventory that use them, with the vault password kept out of the repository.
This post walks through ansible-vault and the runtime flags that unlock encrypted content during a run: create and edit vaulted files, wire passwords from files or prompts, use vault IDs, and avoid the mistakes that still leak secrets after decryption.
Vault is built into Ansible. It's not a centralized secrets manager, and it doesn't replace runtime discipline around logs, registers, and task output.
Why Use Ansible Vault¶
Vault commonly protects passwords, API keys, SSH private keys, TLS material, cloud credentials, service account tokens, and other environment-specific secrets.
The important phrase is at rest. Vault protects content while it's stored in a repository or on disk. Once Ansible decrypts that content during a playbook run, normal runtime discipline still matters. Don't print secrets. Don't register and debug secret-bearing output. Use no_log: true where a task could expose sensitive values.
Vault fits when you want encrypted Ansible-owned secrets versioned with the playbooks, roles, or inventory that reference them. The ciphertext can live in Git. The vault password should not.
Secure Storage in Version-Controlled Repositories¶
Encrypted group_vars, host_vars, or standalone vault files can ride along in the same pull request and review process as the automation that uses them. Decryption happens at run time when you pass --ask-vault-pass, --vault-password-file, or the equivalent wiring in CI.
Encryption at Rest¶
Vault files are encrypted and marked with an Ansible Vault header. A vaulted file usually starts with something like this:
When a vault ID label is used, the header can include that label:
That header tells Ansible the file is encrypted, which vault format is used, which cipher is used, and, when present, which vault ID label applies.
Direct Ansible Integration¶
Vault doesn't require a separate service for basic use cases. ansible-vault ships with Ansible, and ansible-playbook knows how to decrypt vaulted content when you provide the right secret.
That matters for small teams, lab environments, offline automation, and repositories that need simple encryption without operating another platform.
Plaintext Variables vs. Vault¶
Plaintext variables are easy until they aren't.
That value is now exposed to anyone who can read the repository, anyone who can read old commits, and any tool that indexes the codebase. If the repository is ever cloned to the wrong place, backed up to the wrong system, or made public by accident, the secret goes with it.
Vault changes the storage model:
The variable is still available to Ansible at runtime, but it's no longer readable from the file without the vault password.
Vault vs. External Secret Managers¶
| Use Ansible Vault when... | Use a dedicated secret manager when... |
|---|---|
| You need simple encryption for Ansible variables | Secrets must be centrally audited |
| The team is small | Access must be revoked instantly |
| Secret rotation is manual but manageable | Secrets are shared across many systems |
| Offline or disconnected automation matters | Credentials should be dynamic or short-lived |
| You don't need dynamic credentials | Rotation must be automated |
| You don't need centralized audit logs for every secret read | Policy enforcement matters across many teams |
HashiCorp Vault, AWS Secrets Manager, Azure Key Vault, Google Secret Manager, CyberArk, and even 1Password all solve problems Ansible Vault doesn't try to solve. Ansible Vault is a file encryption tool that integrates cleanly with Ansible. That's useful, but it's not a central secrets platform.
Installation and Setup¶
Ansible Vault is available when Ansible is installed.
Install Ansible with pip:
Or install ansible-core if you only need the core command-line tools:
Verify ansible-vault is available:
Check the help output:
The basic syntax is:
Common actions:
| Action | Purpose |
|---|---|
create | Create a new encrypted file |
edit | Edit an encrypted file |
view | View an encrypted file |
encrypt | Encrypt an existing plaintext file |
decrypt | Decrypt an encrypted file |
encrypt_string | Encrypt a single variable value |
rekey | Change the password or vault ID for encrypted content |
Core Concepts¶
Vault becomes much easier once you separate three ideas: vault passwords, vault IDs, and encrypted content.
Vault Passwords¶
A vault password is the secret used to encrypt and decrypt vaulted content.
You can type it when prompted:
You can also store it outside the repository in a password file:
The password file should never be committed.
Vault IDs¶
A vault ID is a label that tells Ansible which vault secret is meant for which encrypted content.
Example vault IDs:
devstageprodshared
Use a vault ID with a prompt:
Use a vault ID with a password file:
Vault IDs matter when one repository contains secrets for multiple environments. They let you avoid using one password for everything.
Encrypted Files vs. Encrypted Variables¶
Vault supports two common patterns.
| Encrypted File | Encrypted Variable (!vault) | |
|---|---|---|
| Shape | Whole file is ciphertext | One value is ciphertext; the key stays visible |
| Example | group_vars/prod/vault.yml | db_password: !vault \| in a YAML file |
| Review | Variable names hidden until decrypt | PRs can show which key changed |
| Tradeoff | Hides more context | Easier reviews; names are still exposed |
How Decryption Works During Playbook Runs¶
When Ansible reads a vaulted file or variable, it detects the Vault header. If you provided a matching vault password or vault ID, Ansible decrypts the value in memory and uses it like any other variable.
That means this task doesn't need special syntax:
- name: Connect to an application database
community.postgresql.postgresql_ping:
login_host: "{{ db_host }}"
login_user: "{{ db_user }}"
login_password: "{{ db_password }}"
no_log: true
If db_password is vaulted and Ansible has the right vault secret, the module receives the decrypted value at runtime. Install community.postgresql from Galaxy if you run this example literally (ansible-galaxy collection install community.postgresql).
Vault Isn't Output Protection
Vault protects stored data. It doesn't automatically protect task output. Use no_log: true on tasks that may display secret values.
Basic Usage Walkthrough¶
Create a small demo directory:
Create a place for inventory variables:
Creating an Encrypted File¶
Create a new encrypted file:
Ansible prompts for a vault password, opens your editor, and encrypts the file when you save and close it.
Add this content:
After saving, inspect the file with sed or head:
You should see encrypted vault text, not the YAML values.
Editing Encrypted Content¶
Edit the file safely:
Vault decrypts the content into a temporary editor session, then encrypts it again when you save.
Tip
Make sure your editor doesn't create unsafe backup, swap, or recovery files in the project directory. Editor temporary files are a common way to leak secrets by accident.
Viewing Encrypted Files¶
View without permanently decrypting:
This is safer than decrypting the file just to read it.
Encrypting an Existing File¶
Create a plaintext file:
Encrypt it:
The file is rewritten as encrypted content.
Decrypting a File¶
Decrypt only when you truly need plaintext:
In normal workflows, prefer view or edit.
If you need a decrypted copy without replacing the encrypted original:
Delete the temporary plaintext file when finished.
Working With Encrypted Variables¶
Sometimes encrypting the whole file is too blunt. If you want reviewers to see variable names and non-secret values, encrypt only the secret value.
Encrypt a Single Variable¶
Prompt for the value:
Paste the output into a variable file:
db_user: app_user
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
653863356134613964383965626534333561303762613232666439396331613939666430
6165343962633231393364343539613333613438636234340a6630376235383937316265
613366346338323339623235616630313061386265373034633538623634323762653031
3432343332313431620a363836353663643234366636613634
The encrypted text above is just an example shape. Generate your own value.
You can also pass a value directly:
That's convenient, but the value may appear in shell history. Prefer --prompt for real secrets.
Use Encrypted Variables in Playbooks¶
Create inventory.yml:
Create group_vars/all/main.yml:
db_host: localhost
db_name: app
db_user: app_user
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
653863356134613964383965626534333561303762613232666439396331613939666430
6165343962633231393364343539613333613438636234340a6630376235383937316265
613366346338323339623235616630313061386265373034633538623634323762653031
3432343332313431620a363836353663643234366636613634
Create site.yml:
- name: Use vaulted variables
hosts: localhost
gather_facts: false
tasks:
- name: Show non-secret database settings
ansible.builtin.debug:
msg: "Connecting to {{ db_name }} on {{ db_host }} as {{ db_user }}"
- name: Use the secret without printing it
ansible.builtin.debug:
msg: "The database password is available to this task"
no_log: true
Run the playbook:
The secret is decrypted for the playbook, but no_log: true prevents that task output from being displayed.
Variable Precedence Considerations¶
Vault doesn't change Ansible variable precedence.
A vaulted variable in group_vars/all/vault.yml can still be overridden by a higher-precedence variable from host vars, extra vars, role vars, or another source.
That can be useful, but it can also be dangerous. If someone passes a plaintext secret through --extra-vars, Vault is no longer protecting that value.
Keep the rule simple: encryption changes storage, not precedence.
Using Vault With Playbooks¶
| Flag | Typical Use |
|---|---|
--ask-vault-pass | Prompt at the terminal; fine locally, awkward in CI |
--vault-password-file PATH | Automation and repeatable local runs |
--vault-id LABEL@SOURCE | Multiple environments; SOURCE is prompt, a file path, or a script |
The most common playbook workflow is a vault prompt:
That's fine for a person at a terminal. It's not great for automation.
Use a Password File¶
Create a password file outside the repository:
mkdir -p ~/.ansible
chmod 700 ~/.ansible
printf '%s\n' 'replace-with-a-real-vault-password' > ~/.ansible/dev-vault-password
chmod 600 ~/.ansible/dev-vault-password
Then, to use it:
Add local vault password paths to .gitignore if your project has any local helper files:
Use Multiple Vault IDs¶
Create environment directories:
Create a dev vault:
Create a prod vault:
Run with both vault IDs:
ansible-playbook -i inventory.yml site.yml \
--vault-id dev@~/.ansible/dev-vault-password \
--vault-id prod@~/.ansible/prod-vault-password
If you provide more than one vault ID during encryption, tell Ansible which one to use:
ansible-vault encrypt group_vars/prod/vault.yml \
--vault-id dev@~/.ansible/dev-vault-password \
--vault-id prod@~/.ansible/prod-vault-password \
--encrypt-vault-id prod
That keeps environment boundaries explicit.
Configuration¶
You can reduce repeated command flags with ansible.cfg.
| Setting | Effect |
|---|---|
vault_password_file | Default password file for every run |
ask_vault_pass = true | Prompt for a vault password on every run |
vault_identity_list | Comma-separated LABEL@SOURCE entries for multiple vault IDs |
Example with multiple vault IDs:
[defaults]
inventory = inventory.yml
vault_identity_list = dev@~/.ansible/dev-vault-password, prod@~/.ansible/prod-vault-password
For a single password file:
Use password files carefully:
- Store them outside the repository.
- Set restrictive permissions.
- Prefer one file per environment.
- Don't place them in shared project folders.
- Don't bake them into container images.
- Don't print them in CI logs.
Practical Example: Database Credentials¶
A common pattern is to split non-secret defaults from vaulted secrets. Reuse the inventory.yml from the encrypted-variables walkthrough, or define the same localhost inventory there.
Create group_vars/all/main.yml:
app_name: payments-api
db_host: db.internal.example.com
db_port: 5432
db_name: payments
db_user: payments_app
Create group_vars/all/vault.yml:
Add:
Create site.yml:
- name: Configure application database connection
hosts: localhost
gather_facts: false
tasks:
- name: Render application configuration
ansible.builtin.template:
src: app.env.j2
dest: /tmp/app.env
mode: "0600"
no_log: true
Create app.env.j2:
APP_NAME={{ app_name }}
DATABASE_HOST={{ db_host }}
DATABASE_PORT={{ db_port }}
DATABASE_NAME={{ db_name }}
DATABASE_USER={{ db_user }}
DATABASE_PASSWORD={{ db_password }}
Run it:
The template task uses the decrypted secret, but no_log: true keeps the task output from exposing the rendered values.
Practical Example: Environment-Specific Secrets¶
Use different vault IDs for different environments.
Layout:
Create group_vars/dev/main.yml:
Create group_vars/prod/main.yml:
Create the dev vault:
Create the prod vault:
Each file can define the same variable name:
Run dev:
Run prod:
This is cleaner than one shared vault password for every environment. If a dev password leaks, you don't have to rotate prod secrets just because both environments shared the same vault password.
Recommended Practices¶
Practice these recomendations by default:
- Don't commit vault passwords.
- Use different vault IDs per environment.
- Keep vault password files outside the repository.
- Set vault password file permissions to
0600. - Use
no_log: trueon secret-bearing tasks. - Encrypt only the data that needs encryption.
- Keep non-secret defaults readable.
- Rotate secrets periodically.
- Rekey vault files when people leave the team.
- Limit repository access to people who need the encrypted files.
- Limit vault password access even more tightly.
Rekey a file:
Rekey from one vault ID to another:
ansible-vault rekey group_vars/prod/vault.yml \
--vault-id prod@~/.ansible/old-prod-vault-password \
--new-vault-id prod \
--new-vault-password-file ~/.ansible/new-prod-vault-password
See Vault vs. External Secret Managers for when to outgrow file encryption. Vault is a good repository encryption tool. It isn't a substitute for a mature enterprise secrets program.
Common Pitfalls¶
Losing Vault Passwords¶
If you lose the vault password, the encrypted data is effectively gone.
Store vault passwords in an approved password manager. Don't rely on one person's laptop.
Mixing Encrypted and Plaintext Secrets¶
This is the classic half-fix.
You encrypt db_password, but leave api_token in plaintext because it was added later in a hurry. Now the repository still leaks secrets.
Review variable files for secret-looking names:
That search isn't perfect, but it catches plenty of mistakes.
Overusing One Vault for Everything¶
One vault password for dev, stage, prod, and shared services is convenient. It's also a blast radius problem.
Use vault IDs by environment or trust boundary.
Decrypting Files and Forgetting Them¶
Avoid this:
Prefer:
or:
Printing Secrets During Debugging¶
This is easy to do:
Don't do that in a real playbook with decrypted secrets in scope.
Use targeted debug output, and set no_log: true where sensitive values may appear.
Next Steps¶
Once the basics are comfortable, improve the workflow:
- Add
ansible.cfgvault settings for local consistency. - Add
.gitignoreentries for local password files. - Split secrets by environment with vault IDs.
- Use
no_log: truein roles that handle secrets. - Add CI checks that search for likely plaintext secrets.
- Store CI vault passwords in your CI platform's secret store.
- Combine Vault with roles and collections cleanly.
- Evaluate an external secret manager when rotation, audit, or dynamic credentials become required.
In CI, the pattern usually looks like this:
The read-prod-vault-password script should read from the CI secret store and print the vault password to stdout. The script can be committed. The secret value shouldn't be.
Ansible Vault isn't glamorous, but it's one of the most useful safety rails in Ansible. It lets you keep automation portable and reviewable without pretending secrets are ordinary variables. Keep the workflow simple, keep the secrets encrypted, and keep the password out of Git.