Every production Ansible project eventually reaches the same inflection point: you need to store a database password, an API token, an SSH private key, or a TLS certificate alongside your playbooks — but you cannot commit plaintext secrets to version control. Ansible Vault is the built-in answer. It provides AES-256-GCM encryption for individual variables, entire variable files, or arbitrary binary files, and integrates cleanly with CI/CD pipelines through a flexible vault-ID system. At Google, Netflix, and Stripe scale, teams layer Ansible Vault with a secrets manager (AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager) — but even then, Vault-encrypted files remain the last line of defence when external secrets stores are unreachable at apply time. Understand Vault thoroughly before you reach for more complex tooling.
The Encryption Model
Ansible Vault encrypts data at rest using AES-256-GCM with a key derived from your vault password via PBKDF2-SHA256 (600,000 iterations as of Ansible 2.17). The ciphertext is base64-encoded and wrapped in a recognisable header so Ansible knows which files to decrypt at runtime. Two granularities exist:
Encrypted files — an entire YAML variable file or any arbitrary file (private key, PKCS#12 bundle) is encrypted as a single blob. The file is unreadable in the repo; Ansible decrypts it into memory at play time.
Inline encrypted strings (!vault tagged YAML strings) — only the secret value is encrypted; the variable name remains readable. This is almost always the right choice: it allows meaningful git diff output and lets reviewers see which variables changed without seeing their values.
Key idea — prefer inline encrypted strings over encrypted files. Encrypting an entire group_vars/production/vault.yml file hides everything — including the variable names — making code review painful. Encrypt only the secret values with ansible-vault encrypt_string, keep them in a plaintext-skeleton YAML file, and your PRs remain reviewable.
Basic Vault Operations
The ansible-vault CLI is your primary interface. Every operation accepts either an interactive password prompt or a password file (a plain text file containing the password, permission-restricted to 0600). In pipelines, always use a password file or a vault-ID script — never interactive prompts.
# --- CREATE A NEW ENCRYPTED FILE ---
ansible-vault create group_vars/production/vault.yml
# --- ENCRYPT AN EXISTING PLAINTEXT FILE ---
ansible-vault encrypt group_vars/production/secrets.yml
# --- DECRYPT IN PLACE (NEVER DO THIS IN PROD REPOS) ---
ansible-vault decrypt group_vars/production/secrets.yml
# --- VIEW AN ENCRYPTED FILE WITHOUT DECRYPTING ON DISK ---
ansible-vault view group_vars/production/vault.yml
# --- EDIT AN ENCRYPTED FILE (opens $EDITOR, re-encrypts on save) ---
ansible-vault edit group_vars/production/vault.yml
# --- REKEY (rotate the encryption password) ---
ansible-vault rekey group_vars/production/vault.yml
# --- ENCRYPT A SINGLE STRING (inline secret) ---
# The output is pasted directly into a YAML vars file as a !vault block
ansible-vault encrypt_string 'S3cr3tP@ssw0rd!' --name 'db_password'
# Example output (paste into vars file as-is):
# db_password: !vault |
# $ANSIBLE_VAULT;1.1;AES256
# 38623930336162366631...
# ...
# --- RUNNING A PLAYBOOK WITH VAULT ---
# Interactive prompt:
ansible-playbook site.yml --ask-vault-pass
# Password file (preferred in automation):
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Multiple vault IDs:
ansible-playbook site.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prod
Vault IDs — The Right Way to Handle Multiple Environments
A vault ID is a label attached to an encrypted payload, introduced in Ansible 2.4. When you encrypt a secret with --vault-id dev@password_file, Ansible embeds the label dev in the ciphertext header. At runtime, Ansible matches each encrypted value to the correct password automatically — you can supply a dozen vault IDs and Ansible will try each until one succeeds.
This solves the multi-environment secret problem elegantly: dev, staging, and prod secrets live in the same repository, encrypted with separate passwords. A developer has the dev password; your CI pipeline has the prod password; neither can read the other's secrets.
# Encrypt with a vault ID label (the @ separates label from password source)
ansible-vault encrypt_string 'prod-db-secret' \
--vault-id prod@~/.vault_pass_prod \
--name 'db_password'
# Encrypt dev secret with dev vault ID
ansible-vault encrypt_string 'dev-db-secret' \
--vault-id dev@~/.vault_pass_dev \
--name 'db_password'
# Run playbook with both IDs — Ansible matches automatically
ansible-playbook site.yml \
--vault-id dev@~/.vault_pass_dev \
--vault-id prod@~/.vault_pass_prod
# In ansible.cfg, set default vault IDs so you don't need CLI flags:
# [defaults]
# vault_identity_list = dev@~/.vault_pass_dev, prod@~/.vault_pass_prod
Vault ID flow: each environment's secrets are encrypted with a separate vault ID; Ansible matches the correct password at runtime and decrypts only in memory — the plaintext never touches disk.
Using Vault Secrets in Playbooks and Templates
Once a vault-encrypted variable is defined (in group_vars/, host_vars/, or an included vars file), you use it exactly like any other Ansible variable. Ansible decrypts it transparently at the start of the play. In Jinja2 templates, the variable expands to its plaintext value on the managed node — but the plaintext is never written to the control node's disk.
# group_vars/production/vars.yml (plaintext skeleton — committed to git)
db_host: db.prod.internal
db_port: 5432
db_name: appdb
db_user: appuser
db_password: !vault |
$ANSIBLE_VAULT;1.2;AES256;prod
61616262333130336637...
...
# templates/database.conf.j2 (Jinja2 template — committed to git)
[database]
host = {{ db_host }}
port = {{ db_port }}
name = {{ db_name }}
user = {{ db_user }}
password = {{ db_password }}
# playbook task that deploys the config
- name: Deploy database config
ansible.builtin.template:
src: templates/database.conf.j2
dest: /etc/app/database.conf
owner: appuser
group: appuser
mode: '0600' # CRITICAL: config files with secrets must be 0600
no_log: true # suppress task output so the secret is not logged
Production pitfall — always set no_log: true on tasks that handle secret variables. Without it, Ansible's verbose output (-v) and callback plugins (AWX, Semaphore, Datadog) will log the decrypted secret value to stdout, log files, and your SIEM. One engineer running -vvv in a shared terminal screen is enough to expose a prod database password. Add no_log: true to every task whose parameters or registered output may contain a secret — it is the single most common Vault-related production incident.
Vault in CI/CD Pipelines
The canonical pattern for pipelines: store the vault password as a CI secret (GitHub Actions secret, GitLab CI variable masked, Jenkins credential), write it to a temporary file at job start, pass --vault-password-file to Ansible, and shred the file at job end. Never pass the password directly as a CLI argument — it appears in ps aux and shell history.
Pro tip — use a vault password script for dynamic secrets. The --vault-password-file argument accepts any executable script, not just a static file. Point it at a script that fetches the password from AWS Secrets Manager, HashiCorp Vault, or GCP Secret Manager at runtime. This eliminates long-lived vault password files entirely and lets you rotate the root password in your secrets manager without touching any Ansible files:
Vault is not limited to YAML variables. You can encrypt TLS private keys, SSH private keys, PKCS#12 bundles, or any binary file. Ansible decrypts these into a temporary location on the managed node, uses them (for example, in the copy module), and removes the temporary file. The workflow is identical to encrypted YAML files:
# Encrypt a TLS private key for storage in git
ansible-vault encrypt --vault-id prod@~/.vault_pass_prod \
files/tls/api.prod.internal.key
# Deploy it with the copy module — Ansible decrypts transparently
- name: Deploy TLS private key
ansible.builtin.copy:
src: files/tls/api.prod.internal.key # vault-encrypted on disk
dest: /etc/ssl/private/api.key
owner: root
group: ssl-cert
mode: '0640'
no_log: true
# Verify the file is still encrypted in your git repo:
# head -1 files/tls/api.prod.internal.key
# $ANSIBLE_VAULT;1.2;AES256;prod
Key Rotation and Rekeying
Vault passwords must be rotated periodically and immediately after any suspected compromise. The rekey command re-encrypts all content with a new password without decrypting to disk:
# Rekey a single file
ansible-vault rekey \
--vault-id prod@~/.vault_pass_prod_old \
--new-vault-id prod@~/.vault_pass_prod_new \
group_vars/production/vault.yml
# Rekey all vault files in a directory tree
find . -name "vault*.yml" -exec \
ansible-vault rekey \
--vault-id prod@~/.vault_pass_prod_old \
--new-vault-id prod@~/.vault_pass_prod_new \
{} \;
# After rekeying: update the password in your secrets manager,
# revoke the old password, and delete the old password file.
shred -u ~/.vault_pass_prod_old
Big-tech operational standard — treat Vault passwords as PAM credentials. They should be: stored only in your secrets manager (never in password managers or email), rotated at least every 90 days and on every team member departure, tied to a service identity with audit logging, and never shared over Slack or email. At scale, the vault password itself is the highest-value secret in your Ansible deployment — a compromised vault password exposes every secret across every environment that uses that vault ID.
Ansible Vault is a strong foundation for secrets-at-rest, but it is only one layer of a complete secrets strategy. Pair it with short-lived credentials from your cloud provider (AWS IAM roles, GCP Workload Identity), rotate database passwords through Vault dynamic secrets or AWS Secrets Manager rotation, and audit access to vault password files as rigorously as you audit root SSH keys. The engineers who get paged at 2 AM over a secret leak are almost always the ones who skipped one of these layers.