TL;DR — Quick Summary
Ansible Vault encrypts secrets in playbooks with AES-256. Learn vault create, encrypt_string, vault IDs, CI/CD integration, and password rotation.
Storing database passwords, API keys, and SSL private keys in plain text inside your Ansible playbooks is one of the most common security mistakes in infrastructure automation. Once a secret lands in a git repository unencrypted, it is effectively compromised — git history persists forever, and anyone with read access to the repo can retrieve it. Ansible Vault solves this by encrypting secrets directly inside your repository using AES-256, so you can commit encrypted credential files alongside your playbooks without exposing anything sensitive.
Prerequisites
Before working through this guide, you should have:
- Ansible 2.8 or later installed on your control node (
ansible --versionto verify) - Basic familiarity with Ansible playbooks and group_vars directory structure
- An existing Ansible project with inventory and at least one playbook
- A text editor configured as your
$EDITOR(nano, vim, or VS Code with the remote extension) - For CI/CD sections: a GitHub Actions, GitLab CI, or Jenkins pipeline you can modify
New to Ansible? Read our Ansible for Beginners guide first to set up your project structure before adding Vault.
Why Plaintext Secrets in Playbooks Are Dangerous
Every secret stored in plain text in a git repository carries the same risk: exposure. Consider what typically ends up in Ansible variable files without Vault:
- Database root passwords in
group_vars/dbservers.yml - AWS access keys in
host_vars/deploy.yml - SMTP credentials in
roles/notifications/defaults/main.yml - SSL private key content in a
vars:block inside a playbook
Any of the following events exposes these secrets instantly: a repository accidentally made public, a contributor’s GitHub account compromised, a leaked CI/CD log, or a disgruntled former employee. Ansible Vault prevents this by ensuring that the files stored in git contain only AES-256 encrypted ciphertext, not the actual secret values.
The vault password itself never enters your repository. It stays in your head, in a password manager, or in your CI/CD secrets store — separate from the code.
Creating Encrypted Files with ansible-vault create
The create subcommand opens your configured $EDITOR with an empty file, lets you type your secrets, and saves the file encrypted when you close the editor:
ansible-vault create group_vars/all/vault.yml
You will be prompted for a new vault password (twice for confirmation). The editor opens. Enter your secrets:
vault_db_password: "Tr0ub4dor&3"
vault_db_root_password: "R4nd0mS33d#9"
vault_api_key: "sk-live-abc123def456ghi789"
vault_smtp_password: "M@ilS3rv1c3P@ss"
vault_ssl_private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEA3cH7...
-----END RSA PRIVATE KEY-----
Save and close. The file on disk now contains only the encrypted blob:
$ANSIBLE_VAULT;1.1;AES256
66386439653763616335616230333934356262313032393132376163633335636630
34663437326664303537373561373039636131336531353364386530636431343739
...
Editing and viewing encrypted files
# Open in editor for modifications
ansible-vault edit group_vars/all/vault.yml
# Display decrypted content without editing
ansible-vault view group_vars/all/vault.yml
# Encrypt an existing plain-text file
ansible-vault encrypt secrets.yml
# Decrypt a file back to plain text (use with caution)
ansible-vault decrypt secrets.yml
Encrypting Inline Variables with encrypt_string
Sometimes you want a single secret inside a non-vault file — perhaps because you want the surrounding context visible without decrypting everything. The encrypt_string subcommand generates an encrypted value you can paste directly into any YAML file:
ansible-vault encrypt_string --name 'vault_stripe_key' 'sk_live_abc123xyz'
Output:
vault_stripe_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
66653465346564383664396631316639
37313739623731633864333736396230
...
Paste this block directly into any YAML variable file. Ansible decrypts it transparently at runtime, the same way it handles a fully encrypted vault file.
When to use encrypt_string vs encrypted files:
| Approach | Use when |
|---|---|
ansible-vault create vault.yml | Most cases — keeps secrets organized, easy to audit |
encrypt_string | Single secret in a mostly-plain file; inline secrets in roles |
| Encrypted entire playbook | Rarely needed; breaks readability |
Vault IDs: Multiple Passwords for Different Secret Sets
Vault IDs let you label encrypted content so Ansible knows which password applies. This is essential when staging and production use different passwords, or when you want to rotate one password without re-encrypting everything.
Encrypting with a vault ID
# Encrypt with a named vault ID
ansible-vault create --vault-id prod@prompt group_vars/production/vault.yml
ansible-vault create --vault-id staging@prompt group_vars/staging/vault.yml
# Or point to a password file
ansible-vault create --vault-id prod@~/.vault_prod group_vars/production/vault.yml
Decrypting with multiple vault IDs
ansible-playbook site.yml \
--vault-id prod@~/.vault_prod \
--vault-id staging@~/.vault_staging
Ansible automatically matches each encrypted block to its vault ID label and uses the correct password. Content encrypted without an explicit vault ID defaults to the label default.
CI/CD Integration with —vault-password-file
Interactive password prompts break automated pipelines. The --vault-password-file flag accepts a path to a file containing only the vault password, enabling fully non-interactive execution.
GitHub Actions example
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Ansible
run: pip install ansible
- name: Write vault password to file
run: echo "${{ secrets.ANSIBLE_VAULT_PASSWORD }}" > ~/.vault_pass
# Note: file permissions are set automatically; delete in cleanup
- name: Run playbook
run: |
ansible-playbook -i inventory/production site.yml \
--vault-password-file ~/.vault_pass
- name: Remove vault password file
if: always()
run: rm -f ~/.vault_pass
Store ANSIBLE_VAULT_PASSWORD in your repository’s GitHub Actions secrets (Settings → Secrets → Actions). The vault password never appears in logs because GitHub masks secret values.
GitLab CI example
# .gitlab-ci.yml
deploy:
script:
- echo "$ANSIBLE_VAULT_PASSWORD" > ~/.vault_pass
- ansible-playbook -i inventory/production site.yml --vault-password-file ~/.vault_pass
- rm -f ~/.vault_pass
variables:
ANSIBLE_VAULT_PASSWORD: $ANSIBLE_VAULT_PASSWORD
Using a vault password script
For dynamic vault passwords (fetched from HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault at runtime), create an executable script that outputs the password:
#!/bin/bash
# ~/.get_vault_pass.sh
aws secretsmanager get-secret-value \
--secret-id ansible/vault-password \
--query SecretString \
--output text
chmod +x ~/.get_vault_pass.sh
ansible-playbook site.yml --vault-password-file ~/.get_vault_pass.sh
Best Practices: Organizing Vaulted Variables
The most maintainable pattern separates your variable files into two parallel files per group:
group_vars/
all/
vars.yml # plain variables + references to vault_ vars
vault.yml # encrypted, only vault_ prefixed variables
production/
vars.yml
vault.yml
staging/
vars.yml
vault.yml
group_vars/all/vars.yml — plain text, safe to inspect in git:
# group_vars/all/vars.yml
db_host: "db.internal.example.com"
db_port: 5432
db_name: "myapp_prod"
db_password: "{{ vault_db_password }}" # reference, not the value
api_endpoint: "https://api.stripe.com/v1"
stripe_key: "{{ vault_stripe_key }}"
smtp_host: "smtp.sendgrid.net"
smtp_user: "apikey"
smtp_password: "{{ vault_smtp_password }}"
group_vars/all/vault.yml — encrypted, all vault_ prefixed:
# group_vars/all/vault.yml (this file is encrypted on disk)
vault_db_password: "Tr0ub4dor&3"
vault_stripe_key: "sk_live_abc123"
vault_smtp_password: "SG.abc123xyz"
Benefits of this pattern:
- Playbooks reference human-readable names (
db_password), not vault variables directly git diffshows changes tovars.ymlstructure without requiring vault password- You can audit which variables exist in the vault file by name (run
ansible-vault view) - New team members can read
vars.ymlto understand the variable structure without vault access
Rotating Vault Passwords with rekey
Rotating the vault password is a one-command operation:
ansible-vault rekey group_vars/all/vault.yml
You will be prompted for the current vault password, then asked to enter and confirm the new password. The file is re-encrypted in place. No secrets are decrypted to disk during this process.
For multiple vault files at once:
# Rekey all vault files in the project
find . -name "vault.yml" -exec ansible-vault rekey {} \;
# Or with --new-vault-password-file for non-interactive rotation
ansible-vault rekey \
--vault-password-file ~/.old_vault_pass \
--new-vault-password-file ~/.new_vault_pass \
group_vars/all/vault.yml group_vars/production/vault.yml
After rekeying, update your CI/CD secret store with the new password before the next pipeline run.
Using Ansible Vault with AWX and Semaphore
AWX / Ansible Tower
AWX has a native credential type for Ansible Vault. Create a credential of type Vault (Settings → Credentials → Add → Vault), enter the vault password, and assign it to your job template. AWX handles passing the password securely without exposing it in job output.
For multiple vault IDs in AWX, create one Vault credential per ID and assign all of them to the job template.
Semaphore UI
Semaphore stores vault passwords in its Key Store. Navigate to Key Store → New Key, select type Password, enter your vault password, and name it (e.g., vault-production). When creating a task template, select the vault key in the Vault Password field.
Semaphore passes the key to Ansible via a temporary file that is deleted after the playbook run, following the same security model as the CI/CD password-file approach.
Comparison: Ansible Vault vs Alternatives
| Tool | Encryption | Git-native | CI/CD | Dynamic secrets | Complexity |
|---|---|---|---|---|---|
| Ansible Vault | AES-256 | Yes — files in repo | Password file | Via script | Low |
| HashiCorp Vault | AES-256-GCM | No — external service | API + token | Yes (leases) | High |
| SOPS | AES-256 / PGP / KMS | Yes — encrypted values | KMS key | No | Medium |
| git-crypt | AES-256 | Yes — transparent | GPG key | No | Medium |
Choose Ansible Vault when your secrets only need to be accessible by Ansible playbooks and you want zero additional infrastructure. Choose HashiCorp Vault when you need dynamic secrets, fine-grained lease expiration, or multi-application secret access. SOPS is useful when you need encrypted files readable by tools other than Ansible (Terraform, Helm). git-crypt works well for general encrypted files in a repo but lacks Ansible-specific integrations.
Real-World Scenario: Database Credentials and API Keys Across Environments
You manage three environments (development, staging, production) for a web application. Each environment has a PostgreSQL database, a Stripe API key, and a SendGrid SMTP password. Development uses weak local passwords; production uses strong credentials managed by the security team.
Directory layout:
project/
ansible.cfg
inventory/
development/hosts
staging/hosts
production/hosts
group_vars/
all/
vars.yml # shared variable structure
development/
vars.yml # non-sensitive dev config
vault.yml # encrypted dev secrets (separate password)
staging/
vars.yml
vault.yml # encrypted staging secrets
production/
vars.yml
vault.yml # encrypted production secrets (most restricted)
playbooks/
deploy.yml
db_setup.yml
Each environment’s vault.yml uses a separate vault ID and password, managed by the appropriate team:
# Create development vault (developer team password)
ansible-vault create --vault-id dev@prompt group_vars/development/vault.yml
# Create staging vault (QA team password)
ansible-vault create --vault-id staging@prompt group_vars/staging/vault.yml
# Create production vault (ops team password)
ansible-vault create --vault-id prod@prompt group_vars/production/vault.yml
Deployment pipeline (production only):
ansible-playbook -i inventory/production playbooks/deploy.yml \
--vault-id prod@~/.vault_prod
Staging pipeline (QA engineer can run):
ansible-playbook -i inventory/staging playbooks/deploy.yml \
--vault-id staging@~/.vault_staging
The production vault password is never shared with developers or QA. If the QA vault is compromised, production secrets are unaffected because they use a completely separate password.
Gotchas and Edge Cases
Forgetting the vault password is permanent. There is no recovery mechanism. The encrypted data is gone. Store vault passwords in a password manager and a CI/CD secret immediately after creation.
Avoid --ask-vault-pass in automation. It blocks pipelines and requires interactive input. Always use --vault-password-file in any non-interactive context.
ansible.cfg can set vault defaults. Add vault_password_file = ~/.vault_pass to your ansible.cfg to avoid specifying it on every command. Be aware that this affects everyone who uses the same ansible.cfg.
vault.yml files should never be in .gitignore. The entire point is to commit them encrypted. If you are tempted to gitignore a vault file, you have probably made it plain text by accident.
Encrypted files still show as changed in git when re-encrypted identically. AES-256 in CBC mode produces different ciphertext for the same plaintext each time. Do not ansible-vault encrypt unchanged files as it pollutes git history. Only edit and re-encrypt when secrets actually change.
Binary files can also be encrypted. SSL certificates, keystore files, and SSH keys in binary format can all be encrypted with ansible-vault encrypt. Ansible decrypts them to a temp file before passing the path to the relevant module.
Troubleshooting
“Decryption failed” error:
# Verify the password is correct by viewing the file
ansible-vault view group_vars/all/vault.yml
# Check that the vault ID matches if using vault IDs
ansible-vault view --vault-id prod@prompt group_vars/production/vault.yml
Playbook runs without prompting for vault password and uses wrong value:
Check if vault_password_file is set in ansible.cfg. If the file does not exist or is empty, Ansible silently skips vault decryption. Add --ask-vault-pass temporarily to confirm.
“File is not encrypted” error when editing:
The file is stored as plain text. Encrypt it first: ansible-vault encrypt filename.yml.
Variable shows as literal string {{ vault_db_password }} in task output:
The variable is referenced but the vault file was not loaded. Verify the vault file is in a path Ansible automatically loads (group_vars/, host_vars/) or explicitly include it with vars_files: in the playbook.
CI/CD pipeline shows vault password in logs:
Never pass the vault password as a command-line argument (--vault-password-file /dev/stdin <<< "$PASSWORD"). Always write to a file first. Confirm your CI provider masks the secret variable in log output.
Summary
- Store all secrets in
group_vars/*/vault.ymlfiles encrypted withansible-vault create - Use the
vault_prefix convention to distinguish encrypted variables from plain ones - Reference vault variables from plain
vars.ymlfiles using{{ vault_varname }}so playbooks stay readable - Use
encrypt_stringonly for single inline secrets, not as your primary secrets pattern - Use vault IDs to maintain separate passwords for different environments or teams
- Integrate with CI/CD using
--vault-password-fileand store the password in your secrets manager - Rotate vault passwords with
ansible-vault rekeyand update CI/CD secrets immediately - Never store vault passwords in files that are committed to git
- AWX and Semaphore both have native vault credential support — prefer that over shell workarounds