Overview
In automated operations, Ansible Playbooks frequently handle sensitive information such as database passwords, API keys, and SSH private keys. If this data is stored in plaintext in code repositories, a single repository leak exposes all credentials. Ansible Vault, Ansible’s built-in encryption tool, protects sensitive data using the AES-256 symmetric encryption algorithm, ensuring that only authorized users can access it.
This article will systematically cover Ansible Vault usage, password management strategies, and CI/CD pipeline integration—from basic concepts to production practices.
Why You Need Ansible Vault
Risks of Plaintext Storage
In real-world operations, sensitive information is scattered across multiple locations:
| Location | Common Sensitive Data | Risk Level |
|---|---|---|
group_vars/all.yml | Database passwords, Redis passwords | High |
host_vars/web01.yml | SSH connection passwords, BECOME passwords | High |
| Inventory files | ansible_password, ansible_ssh_pass | High |
| Playbook variables | API tokens, third-party keys | Medium |
| Jinja2 templates | Certificate private keys, JWT secrets | High |
Risks of plaintext storage include:
- Repository leaks: Git history permanently retains plaintext passwords; even if subsequently deleted, they can be recovered from history
- Compliance audit failures: Security standards like PCI-DSS and ISO 27001 require encrypted storage of sensitive data
- Team collaboration risks: Anyone with repository access can see all passwords
- Log leaks: Ansible execution logs may output variable values, causing passwords to appear in log files
Ansible Vault’s Position
Ansible Vault is not the only secret management solution, but it is the most direct choice within the Ansible ecosystem:
| Solution | Pros | Cons | Use Case |
|---|---|---|---|
| Ansible Vault | Built-in, zero dependencies, YAML-native | Single password encryption, no fine-grained permissions | Small to medium teams, quick start |
| HashiCorp Vault | Dynamic secrets, lease management, audit logs | Requires additional deployment and maintenance | Large enterprises, high security requirements |
| AWS Secrets Manager | Cloud-native, auto-rotation | Vendor lock-in, usage-based pricing | AWS cloud environments |
| SOPS + age | Multi-key encryption, Git-friendly | Requires additional tooling | Multi-person collaboration, GitOps scenarios |
Refer to the Ansible Vault official documentation for the complete feature list.
Core Concepts and Encryption Mechanism
Encryption Principle
Ansible Vault uses the AES-256 symmetric encryption algorithm (in earlier versions, possibly AES-128). The encryption flow:
Plaintext YAML → AES-256 encryption (Vault password as key) → Encrypted YAML ($ANSIBLE_VAULT header)
Encrypted files begin with $ANSIBLE_VAULT;1.1;AES256, followed by base64-encoded ciphertext. For example:
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653033393664633164636136383765393730353066386230336230
363366323737366336663737333635303462653066333637356436653066333763350a6366373134
37633733336236393030303237373332643761306635316631393963363033663638366363313061
...
Encryptable File Types
Ansible Vault can encrypt any data file used by Ansible:
| File Type | Example | Encryptable |
|---|---|---|
| Variable files | group_vars/all.yml | Yes |
| Inventory files | inventory.ini | Yes |
| Playbook files | site.yml | Yes (but not recommended) |
| Jinja2 templates | nginx.conf.j2 | Yes |
| Standalone key files | db_creds.yml | Yes |
| Certificate/key files | server.key | Yes |
ansible.cfg | ansible.cfg | No |
Note:
ansible.cfgcannot be encrypted because Ansible must read this file at startup. If the configuration contains sensitive information, move it to an encrypted variable file and reference it fromansible.cfg.
Basic Operations
Creating Encrypted Files
# Interactive creation (will prompt for password)
ansible-vault create secrets.yml
# Create using a password file (recommended for automation)
ansible-vault create --vault-password-file=~/.vault_pass secrets.yml
Example file content after creation:
# secrets.yml (encrypted)
db_password: "prod-db-2026!"
admin_api_key: "sk_live_abc123def456"
ssh_private_key: |
-----BEGIN RSA PRIVATE KEY-----
MIIEowIBAAKCAQEA...
-----END RSA PRIVATE KEY-----
Encrypting Existing Files
# Encrypt a single file
ansible-vault encrypt existing_vars.yml
# Encrypt multiple files
ansible-vault encrypt group_vars/all.yml host_vars/web01.yml
# Specify output file (does not overwrite the original)
ansible-vault encrypt --output=encrypted.yml plaintext.yml
Viewing, Editing, and Rekeying
# View encrypted file content (does not leave plaintext on disk)
ansible-vault view secrets.yml
# Edit encrypted file (auto decrypt → edit → re-encrypt on save)
ansible-vault edit secrets.yml
# Change the encryption password (rekey)
ansible-vault rekey secrets.yml
# Permanently decrypt a file (removes encryption, use with caution!)
ansible-vault decrypt secrets.yml
Common Command Reference
| Command | Function | Safety Level |
|---|---|---|
ansible-vault create | Create and encrypt a new file | Safe |
ansible-vault encrypt | Encrypt an existing file | Safe |
ansible-vault edit | Edit an encrypted file | Safe (temporary in-memory decryption) |
ansible-vault view | View encrypted content | Safe (no disk paging) |
ansible-vault rekey | Change encryption password | Safe |
ansible-vault decrypt | Permanently decrypt | Dangerous (plaintext on disk) |
Password Management Strategies
Password File Approach
Create a file containing only the password string:
# Create password file
echo "MyVaultPassword2026!" > ~/.vault_pass
# Set strict permissions (mandatory!)
chmod 600 ~/.vault_pass
Configure the default password file path in ansible.cfg:
[defaults]
vault_password_file = ~/.vault_pass
Afterward, Ansible commands will automatically use the configured password file:
# Automatically uses the password file configured in ansible.cfg
ansible-playbook site.yml
Multiple Passwords (Vault ID) Approach
In production environments, different environments (development, staging, production) should use different Vault passwords. Ansible 2.4+ introduced the Vault ID concept:
# Create encrypted files with Vault IDs
ansible-vault create --vault-id dev@~/.vault_dev_pass dev_secrets.yml
ansible-vault create --vault-id prod@~/.vault_prod_pass prod_secrets.yml
Specify multiple Vault IDs when running Playbooks:
ansible-playbook site.yml \
--vault-id dev@~/.vault_dev_pass \
--vault-id prod@~/.vault_prod_pass
Ansible automatically selects the correct password for decryption based on the Vault ID used during file encryption.
Configure multiple Vault IDs in ansible.cfg:
[defaults]
vault_identity_list = dev@~/.vault_dev_pass, prod@~/.vault_prod_pass, staging@~/.vault_staging_pass
Vault ID Naming Conventions
Recommended practice is to use environment names as Vault ID identifiers:
| Vault ID | Purpose | Password File |
|---|---|---|
dev | Development environment | ~/.vault_dev_pass |
staging | Staging environment | ~/.vault_staging_pass |
prod | Production environment | ~/.vault_prod_pass |
db | Database-specific password | ~/.vault_db_pass |
network | Network device passwords | ~/.vault_network_pass |
Best practice: Production environment password files should be stored in restricted locations (such as password managers or HSMs), not in user home directories.
Using Encrypted Variables in Playbooks
Method 1: vars_files Reference
# site.yml
---
- name: Deploy Application
hosts: prod_servers
vars_files:
- secrets.yml # Automatically decrypts and loads variables
tasks:
- name: Configure database connection
template:
src: db.conf.j2
dest: /etc/app/db.conf
# {{ db_password }} is automatically decrypted and filled at runtime
Method 2: Dynamic Loading with include_vars
---
- name: Load secrets based on environment
hosts: all
tasks:
- name: Include encrypted vars for current environment
include_vars:
file: "{{ env }}_secrets.yml"
no_log: true # Prevent variable content from appearing in logs
Method 3: Command-Line Input
# Interactive input with --ask-vault-pass
ansible-playbook site.yml --ask-vault-pass
# Specify password file with --vault-password-file
ansible-playbook site.yml --vault-password-file=~/.vault_pass
# Specify labeled password with --vault-id
ansible-playbook site.yml --vault-id prod@~/.vault_prod_pass
Preventing Log Leaks
Even with Vault encryption, Ansible may output variable values to logs during execution. Use the no_log attribute to prevent leaks:
---
- name: Create database user
community.mysql.mysql_user:
name: "{{ db_user }}"
password: "{{ db_password }}"
priv: "*.*:ALL"
state: present
no_log: true # Critical! Prevents password from appearing in logs
You can also set no_log at the play level:
---
- name: Sensitive operations
hosts: all
no_log: true # All tasks in this play will not log
tasks:
- name: Deploy with secrets
...
Project Structure Design
Recommended Directory Structure
project/
├── ansible.cfg
├── inventory/
│ ├── production/
│ │ ├── hosts.ini
│ │ ├── group_vars/
│ │ │ ├── all/
│ │ │ │ ├── common.yml # Common variables (plaintext)
│ │ │ │ └── vault.yml # Encrypted variables
│ │ │ └── webservers/
│ │ │ ├── vars.yml # Plaintext variables
│ │ │ └── vault.yml # Encrypted variables
│ │ └── host_vars/
│ │ └── web01/
│ │ ├── vars.yml
│ │ └── vault.yml
│ └── staging/
│ ├── hosts.ini
│ └── group_vars/
│ └── all/
│ └── vault.yml
├── playbooks/
│ ├── site.yml
│ ├── deploy.yml
│ └── db_setup.yml
├── roles/
│ ├── nginx/
│ ├── mysql/
│ └── app/
├── files/
│ └── ssl/
│ └── server.key # Encrypted SSL private key
└── templates/
└── db.conf.j2
Plaintext-Encrypted Separation Principle
Store plaintext and encrypted variables in separate files for easier management and auditing:
# group_vars/all/common.yml (plaintext, can be committed to Git)
---
app_name: "myapp"
app_port: 8080
db_host: "db.internal"
db_port: 3306
# group_vars/all/vault.yml (encrypted, can be committed to Git but content is unreadable)
$ANSIBLE_VAULT;1.1;AES256
6638643965323633646262656665303339366463316463613638376539373035...
# ansible.cfg
[defaults]
inventory = inventory/
vault_identity_list = dev@~/.vault_dev_pass, staging@~/.vault_staging_pass, prod@~/.vault_prod_pass
host_key_checking = False
CI/CD Integration
GitLab CI Integration
In CI/CD environments, Vault passwords cannot be entered interactively. Inject via CI variables:
# .gitlab-ci.yml
stages:
- deploy
deploy_production:
stage: deploy
image: ansible/ansible-runner:latest
variables:
# Vault password injected via CI variable (set as Masked variable in GitLab UI)
VAULT_PASS_PROD: $VAULT_PASS_PROD
before_script:
# Write CI variable to temporary password file
- echo "$VAULT_PASS_PROD" > /tmp/vault_pass
- chmod 600 /tmp/vault_pass
script:
- ansible-playbook -i inventory/production playbooks/deploy.yml
--vault-id prod@/tmp/vault_pass
after_script:
# Clean up password file
- rm -f /tmp/vault_pass
only:
- main
GitHub Actions Integration
# .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-core
- name: Create vault password file
run: |
echo "${{ secrets.VAULT_PASS_PROD }}" > ~/.vault_pass
chmod 600 ~/.vault_pass
- name: Run playbook
run: |
ansible-playbook -i inventory/production playbooks/deploy.yml \
--vault-id prod@~/.vault_pass
- name: Cleanup
if: always()
run: rm -f ~/.vault_pass
Jenkins Pipeline Integration
pipeline {
agent any
environment {
VAULT_PASS = credentials('ansible-vault-prod')
}
stages {
stage('Deploy') {
steps {
sh '''
echo "${VAULT_PASS}" > /tmp/vault_pass
chmod 600 /tmp/vault_pass
ansible-playbook -i inventory/production playbooks/deploy.yml \
--vault-id prod@/tmp/vault_pass
rm -f /tmp/vault_pass
'''
}
}
}
}
Security tip: Password files in CI/CD should be cleaned up in
after_scriptorpoststeps to ensure no residue even if the task fails.
Password Rotation
Regular Rotation Strategy
Security compliance typically requires regular password changes. Ansible Vault’s rekey command supports password rotation:
# Interactive rotation (enter old password → set new password)
ansible-vault rekey secrets.yml
# Rotation using password files
ansible-vault rekey \
--vault-password-file=~/.vault_old_pass \
--new-vault-password-file=~/.vault_new_pass \
secrets.yml
# Rotation using Vault ID
ansible-vault rekey \
--vault-id prod@~/.vault_prod_old \
--new-vault-id prod@~/.vault_prod_new \
prod_secrets.yml
Batch Rotation Script
#!/bin/bash
# rotate-vault-passwords.sh
# Batch rotate Vault passwords for all encrypted files
set -euo pipefail
OLD_PASS_FILE="$1"
NEW_PASS_FILE="$2"
VAULT_DIR="${3:-group_vars}"
if [ ! -f "$OLD_PASS_FILE" ] || [ ! -f "$NEW_PASS_FILE" ]; then
echo "Usage: $0 <old_pass_file> <new_pass_file> [vault_dir]"
exit 1
fi
# Find all encrypted files
find "$VAULT_DIR" -type f -name "*.yml" -exec grep -l '^\$ANSIBLE_VAULT' {} \; | while read -r file; do
echo "Rekeying: $file"
ansible-vault rekey \
--vault-password-file="$OLD_PASS_FILE" \
--new-vault-password-file="$NEW_PASS_FILE" \
"$file"
done
echo "All vault files rekeyed successfully."
Rotation Automation
Combine with cron or CI/CD for regular automatic rotation:
# /etc/cron.d/vault-rotate
# Execute password rotation at 2 AM on the first day of each quarter
0 2 1 */3 * /opt/scripts/rotate-vault-passwords.sh \
/etc/ansible/.vault_pass \
/etc/ansible/.vault_pass_new \
/etc/ansible/group_vars \
&& mv /etc/ansible/.vault_pass_new /etc/ansible/.vault_pass
Security Auditing
Encrypted File Scanning
Regularly scan repositories for plaintext sensitive data:
#!/bin/bash
# scan-plaintext-secrets.sh
# Scan for files that may contain plaintext passwords
PATTERNS=(
"password.*=.*['\"].\{8,\}['\"]"
"secret.*=.*['\"].\{8,\}['\"]"
"api_key.*=.*['\"].\{20,\}['\"]"
"token.*=.*['\"].\{20,\}['\"]"
"private_key"
)
SCAN_DIR="${1:-.}"
ISSUES=0
for pattern in "${PATTERNS[@]}"; do
while IFS= read -r file; do
# Skip already encrypted files
if head -1 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
continue
fi
# Skip .git directory
if echo "$file" | grep -q '\.git/'; then
continue
fi
echo "[WARNING] Potential plaintext secret in: $file"
grep -n "$pattern" "$file" | head -5
ISSUES=$((ISSUES + 1))
done < <(grep -rl "$pattern" "$SCAN_DIR" 2>/dev/null)
done
if [ "$ISSUES" -gt 0 ]; then
echo ""
echo "Found $ISSUES files with potential plaintext secrets."
echo "Consider encrypting them with: ansible-vault encrypt <file>"
exit 1
else
echo "No plaintext secrets found."
exit 0
fi
Audit Logging
In team collaboration, record who accessed encrypted data and when:
# Record vault operation audit logs
vault_audit() {
local action="$1"
local file="$2"
local user="$(whoami)"
local timestamp="$(date '+%Y-%m-%d %H:%M:%S')"
local log_file="/var/log/ansible-vault-audit.log"
echo "[$timestamp] user=$user action=$action file=$file" >> "$log_file"
}
# Usage
vault_audit "view" "group_vars/all/vault.yml"
ansible-vault view group_vars/all/vault.yml
Advanced Techniques
Encrypting Individual Variables
Sometimes you only need to encrypt a few sensitive fields rather than an entire file. Use the !vault tag to encrypt individual variables:
# group_vars/all.yml
---
db_host: "db.internal"
db_port: 3306
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653033393664633164636136383765393730353066386230336230
363366323737366336663737333635303462653066333637356436653066333763350a6366373134
...
api_key: !vault |
$ANSIBLE_VAULT;1.1;AES256
37323438366638343536373633336630333134356238393630353436393866636330303037343030
...
# Non-sensitive variables remain in plaintext
app_name: "myapp"
app_port: 8080
Generate encrypted variable values:
# Using the encrypt_string command
ansible-vault encrypt_string --vault-id prod@~/.vault_prod_pass 'my_secret_password'
Output can be pasted directly into a YAML file:
# Specify variable name
ansible-vault encrypt_string --vault-id prod@~/.vault_prod_pass \
--name 'db_password' \
'prod-db-2026!'
Integration with HashiCorp Vault
For scenarios requiring higher security levels, integrate with HashiCorp Vault:
# playbook.yml
---
- name: Get secrets from HashiCorp Vault
hosts: localhost
vars:
vault_url: "http://vault:8200"
tasks:
- name: Read database credentials from Vault
hashivault_read:
url: "{{ vault_url }}"
token: "{{ lookup('env', 'VAULT_TOKEN') }}"
secret: "secret/data/database"
key: "password"
register: db_secret
no_log: true
- name: Use the secret
debug:
msg: "DB password retrieved from Vault (masked)"
no_log: true
Encrypting an Entire Directory
#!/bin/bash
# encrypt-directory.sh
# Encrypt all YAML files in a directory
DIR="${1:-group_vars}"
find "$DIR" -type f \( -name "*.yml" -o -name "*.yaml" \) | while read -r file; do
# Skip already encrypted files
if head -1 "$file" | grep -q '^\$ANSIBLE_VAULT'; then
echo "[SKIP] Already encrypted: $file"
continue
fi
echo "[ENCRYPT] $file"
ansible-vault encrypt --vault-id prod@~/.vault_prod_pass "$file"
done
Common Issues and Troubleshooting
Forgotten Vault Password
Ansible Vault uses symmetric encryption with no backdoor. If you forget the password, the data cannot be recovered.
Preventive measures:
- Store passwords in an enterprise password manager (e.g., 1Password, Bitwarden)
- Have multiple team members hold password copies
- Regularly back up encrypted files
Decryption Failures
# Example error message
ERROR! Attempting to decrypt but no valid secrets found
# Troubleshooting steps
# 1. Check if password file exists and is readable
ls -la ~/.vault_pass
# 2. Check password file content (ensure no extra newlines or spaces)
cat -A ~/.vault_pass
# 3. Check vault_password_file path in ansible.cfg
grep vault ansible.cfg
# 4. Check if Vault ID matches
ansible-vault view --vault-id prod@~/.vault_prod_pass secrets.yml
Git Diff for Encrypted Files
Encrypted files change entirely even when only one character is modified. This makes Git diff unable to show actual changes.
Solution—temporarily decrypt for viewing diffs:
# Configure in .gitattributes
echo "*.yml diff=ansible_vault" >> .gitattributes
# Configure diff tool in .git/config
git config diff.ansible_vault.textconv "ansible-vault view"
After configuration, git diff will automatically decrypt and show plaintext differences.
Common Error Reference
| Error Message | Cause | Solution |
|---|---|---|
Attempting to decrypt but no valid secrets found | No valid Vault password provided | Check password file or --vault-id parameter |
Vault format unhandled | File format incompatible | Check Ansible version, upgrade to latest |
Decryption failed | Wrong password or corrupted file | Try correct password or restore from backup |
value must be valid YAML | Encrypted file content corrupted | Restore from Git history or recreate |
ansible.cfg not found | Configuration file accidentally encrypted | ansible.cfg cannot be encrypted; restore from backup |
Summary
Ansible Vault provides a lightweight sensitive data protection solution for Ansible automated operations. Key points to focus on when using it:
- Plaintext-encrypted separation: Store sensitive and regular variables in separate files; only encrypt sensitive files for easier management and auditing
- Multi-password management: Use Vault IDs to set different passwords for different environments, reducing single-point leak risk
- CI/CD security: Inject passwords through Secret variables in pipelines; clean up temporary files immediately after use
- Regular rotation: Combine with cron or CI/CD for periodic password rotation to meet security compliance requirements
- Log protection: Set
no_log: trueon tasks handling sensitive variables to prevent password leaks in logs - Backup and recovery: Encrypted files need backups too; passwords also need to be securely stored in enterprise password managers
Ansible Vault’s design philosophy is “good enough”—it doesn’t pursue the enterprise-grade key management capabilities of HashiCorp Vault, but instead solves the most common sensitive data protection problem in Ansible scenarios with minimal learning curve and zero additional dependencies. For small to medium teams and rapid-iteration projects, it is the most cost-effective choice.