Ansible Vault #
Every serious Ansible project stores sensitive data: database passwords, API keys, SSL private keys, authentication tokens, and third-party service credentials. Storing these values in plaintext in a Git repository is an unacceptable security risk — one careless commit, one clone to a lost laptop, or one careless contributor, and the entire infrastructure could be exposed to the public. Ansible Vault is Ansible’s built-in solution to this problem: it encrypts individual files or strings using AES-256, so sensitive data can be stored safely in Git without the risk of leaking to unauthorized people. This article discusses how Vault works behind the scenes, the two encryption modes available, strategies for managing vault passwords, integration with roles and CI/CD, and the patterns you must avoid so your Vault implementation truly provides the expected protection.
Why Vault Matters #
Before discussing syntax and commands, it’s important to understand what threats Vault actually mitigates. There are three common scenarios that cause secrets to leak from Ansible repositories, and each scenario requires different protection.
Scenario one: clone to an unauthorized developer. Ansible repositories are cloned by many people — not just engineers handling production deployments, but also data analysts who need log access, project managers who want to read documentation, or new contributors onboarding. Without encryption, everyone with read access to the repository sees all the secrets.
Scenario two: leaking automated backups. Many teams back up Git repositories to cloud services like GitHub, GitLab, Bitbucket, or internal mirrors. These backups often have looser access controls than the main repository — anyone with access to the GitHub organization can see all branches, including branches containing unrotated secrets.
Scenario three: history that can’t be deleted. Git stores the entire change history. If a secrets.yml file was ever committed in plaintext, deleting it in the next commit doesn’t remove the record from history. Anyone with repository access before the fix commit can recover that file with git log -p.
Vault answers all three scenarios with one approach: sensitive files or strings never exist in plaintext in Git. Files encrypted by Vault only contain ciphertext that can’t be read without the vault password. Anyone cloning the repository without the vault password sees useless ciphertext.
Vault isn’t a replacement for a secret manager — it’s a complement. Vault encrypts data at rest in the repository, but it doesn’t prevent the vault password from leaking, doesn’t record detailed audit trails, and doesn’t support automatic rotation. For very sensitive secrets like database root passwords or production private keys, consider using Vault for local caching and an external secret manager (HashiCorp Vault, AWS Secrets Manager) as the primary source. The encryption article discusses this integration pattern in depth.
Architecture: How Vault Works Behind the Scenes #
Understanding Vault’s workflow helps you make the right decisions when troubleshooting problems or designing new workflows. The following diagram shows the complete flow from plaintext to ciphertext and back.
flowchart TD
A["Plaintext file<br/>secrets.yml"] --> B{"ansible-vault<br/>encrypt"}
B --> C["Vault password<br/>from prompt / file / env"]
C --> D["Key derivation<br/>PBKDF2 + salt"]
D --> E["AES-256-CTR<br/>encryption"]
E --> F["Ciphertext file<br/>$ANSIBLE_VAULT;1.1;AES256"]
F --> G["Commit to Git<br/>safe because it's ciphertext"]
H["Playbook run"] --> I["Ansible reads the vault file"]
I --> J["Vault password<br/>supplied to ansible-playbook"]
J --> K["Decrypt on-the-fly<br/>in memory"]
K --> L["Variables available<br/>at runtime"]
L --> M["Tasks run<br/>with the real secret"]
G -. commit .-> H
style B stroke:#ff9800,stroke-width:2px
style E stroke:#ff9800,stroke-width:2px
style F stroke:#2196f3,stroke-width:2px
style K stroke:#2196f3,stroke-width:2pxThree important things from this diagram:
1. Salt and key derivation. Ansible Vault doesn’t use the vault password directly as the AES key. It passes the vault password through a PBKDF2-based key derivation function with a random salt. This means two files encrypted with the same password produce different ciphertext, and rainbow table attacks become ineffective.
2. The $ANSIBLE_VAULT;1.1;AES256 header. Every Vault-encrypted file starts with this header. The header tells Ansible which Vault format version and algorithm were used. Currently Ansible supports format 1.1 (standard) and 1.2 (a new format with additional features). Version 1.1 is sufficient for most cases.
3. Decrypt on-the-fly. Vault files are never decrypted to disk while a playbook runs. Ansible decrypts in memory, uses the plaintext values only for the currently running task, then releases them when the playbook finishes. This minimizes the window where secrets exist in an extractable form.
Vault Format 1.2 (introduced in Ansible 2.12) brings several improvements: better key wrapping support, a more compact binary format, and the ability to encrypt with multiple vault IDs in one file. For new projects, use format 1.2 unless you need compatibility with older Ansible versions. Set the default format with ANSIBLE_VAULT_FORMAT=1.2 in the environment.Two Modes: File Encryption vs String Encryption #
Vault supports two different encryption granularities. Each has its right place in an Ansible security strategy.
Mode 1: Full file encryption #
The entire file contents are encrypted into ciphertext. This mode suits files whose contents are entirely sensitive — for example a vault.yml containing all production secrets.
# Encrypt an existing file (the plaintext file is overwritten with ciphertext)
ansible-vault encrypt group_vars/production/vault.yml
# Create a new encrypted file directly from the editor
ansible-vault create group_vars/production/vault.yml
# Edit an encrypted file — Ansible decrypts, opens the editor, encrypts again on save
ansible-vault edit group_vars/production/vault.yml
# View the contents without opening the editor
ansible-vault view group_vars/production/vault.yml
# Permanently decrypt to disk (be careful — this writes plaintext!)
ansible-vault decrypt group_vars/production/vault.yml
When to use this mode: when a file only contains sensitive data and there are no regular values that need reviewing in Git. The vault.yml file in group_vars/production/ is a classic example — its contents are 100% passwords and tokens.
Mode 2: Individual string encryption #
Only specific values are encrypted, while the YAML file structure stays in plaintext. This mode suits files that are mostly regular content but have one or two sensitive values.
# Encrypt a string and display it on stdout
ansible-vault encrypt_string 'SuperSecret123' --name 'db_password'
The resulting output:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653339316533393634313636353535623135623
3365663636343365623062323438353563373832663430640a633138623430383
...
This encrypted string can be pasted directly into any YAML file, mixed with regular plaintext variables. When Ansible runs the playbook, it recognizes the !vault tag and decrypts that value automatically.
When to use this mode: when a file contains a mix of regular variables (that need reviewing in Git, like hostnames, ports, environment names) and sensitive values (that shouldn’t be visible). Example: the group_vars/all/vars.yml file containing common configuration for all servers.
Comparison Table of the Two Modes #
| Aspect | Full File Encryption | Individual String Encryption |
|---|---|---|
| Granularity | Entire file is ciphertext | Only specific values are ciphertext |
| File reviewable in Git | No (all ciphertext) | Yes (plaintext structure, only sensitive values hidden) |
| Main command | ansible-vault encrypt | ansible-vault encrypt_string |
| Use case | Dedicated vault.yml | Mixed vars files |
| Edit overhead | Must decrypt → edit → encrypt each time | Normal editing, only !vault values are decoded |
| Good for large teams | Less (hard code review) | More (structure stays visible) |
| Runtime performance | Same | Same |
Decision Tree for Choosing the Mode #
flowchart TD
A["Does the file contain sensitive data?"] -- "No" --> B["No Vault needed"]
A -- "Yes" --> C{"Is the file 100%<br/>sensitive data?"}
C -- "Yes" --> D["Mode: Full File Encryption<br/>ansible-vault encrypt"]
C -- "No" --> E{"Are there regular values<br/>that need reviewing?"}
E -- "Yes" --> F["Mode: String Encryption<br/>ansible-vault encrypt_string"]
E -- "No" --> G["Split into 2 files:<br/>vars.yml + vault.yml"]
G --> D
style D stroke:#2196f3,stroke-width:2px
style F stroke:#2196f3,stroke-width:2px
style G stroke:#2196f3,stroke-width:2pxComplete Vault Operations Table #
Every ansible-vault subcommand has a specific purpose. This table summarizes them all with usage examples and important notes.
| Command | Purpose | Example | Important Note |
|---|---|---|---|
encrypt | Encrypt an existing file | ansible-vault encrypt vars.yml | The original file is overwritten with ciphertext |
create | Create a new file and encrypt it directly | ansible-vault create vault.yml | Opens the editor, then encrypts on save |
decrypt | Permanently decrypt to disk | ansible-vault decrypt vault.yml | Be careful — the file becomes plaintext |
view | View contents without changing the file | ansible-vault view vault.yml | Read-only, exits without saving |
edit | Decrypt → open editor → encrypt again | ansible-vault edit vault.yml | Editor from the EDITOR env var |
rekey | Change the vault password without decrypting | ansible-vault rekey vault.yml | Needs the old and new passwords |
encrypt_string | Encrypt a single string | ansible-vault encrypt_string 'secret' | Output to stdout, manual copy-paste |
encrypt_string --stdin-name | Encrypt a string from stdin | echo "secret" | ansible-vault encrypt_string --stdin-name | Useful for scripts |
Important points from this table:
decryptis rarely used in normal workflows — it removes the protection. If you need to “see” a file’s contents, useview. If you need to “edit”, useedit(the file gets encrypted again on save).rekeyis the official way to change the vault password without the risk of plaintext being exposed on disk. The rekey operation only accepts ciphertext input/output; there’s no intermediate stage where the file exists in plaintext.encrypt_stringdoesn’t write files — you must copy the output and paste it manually into the target file. This is a security feature (no plaintext written to disk), but also a common source of errors (forgetting to copy the output).
Vault Password Management: Sources and Strategies #
The vault password is the most critical encryption key — anyone who has this password can decrypt every file encrypted with it. There are several ways to provide it to Ansible, and the right choice depends on the execution context: run manually by an engineer, run by CI/CD, or run by an orchestrator like AWX.
Vault Password Sources #
# Method 1: Interactive prompt — Ansible asks when the playbook starts
ansible-playbook site.yml --ask-vault-pass
# Method 2: Password file — Ansible reads from a file
ansible-playbook site.yml --vault-password-file ~/.vault_pass
# Method 3: Password generator script — stdout output becomes the password
ansible-playbook site.yml --vault-password-file ~/bin/get-vault-pass.sh
# Method 4: Environment variable
export ANSIBLE_VAULT_PASSWORD_FILE=~/.vault_pass
ansible-playbook site.yml
# ansible.cfg — set the default so you don't need the flag every time
[defaults]
vault_password_file = ~/.vault_pass
The vault password file must not be committed to Git. Add it to
.gitignoreimmediately after creating it. This file contains the encryption key for all secrets — leaking one file means leaking every vault using that password.# .gitignore .vault_pass *.vault_pass vault_pass.txt ansible_vault_password
ANTI-PATTERN: vault password in the repository #
# ANTI-PATTERN: password inside a playbook or vars file
# File: group_vars/all/vault.yml
vault_db_password: "MyVaultPassword123!" # ← DON'T! This isn't how Vault works
# CORRECT: password in a separate file, outside the repository
# File: ~/.vault_pass (chmod 600)
MyVaultPassword123!
# ansible.cfg references this file
[defaults]
vault_password_file = ~/.vault_pass
# CORRECT (alternative): password from a secret manager at runtime
# File: ~/bin/get-vault-pass.sh
#!/bin/bash
aws secretsmanager get-secret-value \
--secret-id ansible/vault-password \
--query SecretString --output text
Vault Password Resolution Sequence Diagram #
sequenceDiagram
participant Dev as "Developer / CI"
participant ANS as "ansible-playbook"
participant CFG as "ansible.cfg"
participant SRC as "Password Source"
participant VLT as "Vault File"
participant TSK as "Task"
Dev->>ANS: "ansible-playbook site.yml"
ANS->>CFG: "read default config"
CFG-->>ANS: "vault_password_file = ~/bin/get-vault-pass.sh"
alt "--ask-vault-pass passed"
ANS->>Dev: "prompt Vault password:"
Dev-->>ANS: "type password (hidden)"
else "--vault-password-file passed"
ANS->>SRC: "read file / run script"
SRC-->>ANS: "password from file / script stdout"
else "ANSIBLE_VAULT_PASSWORD_FILE env"
ANS->>SRC: "read path from env var"
SRC-->>ANS: "password"
end
ANS->>VLT: "decrypt group_vars/production/vault.yml"
VLT-->>ANS: "plaintext in memory"
ANS->>TSK: "run task with the secret"
TSK-->>ANS: "execution result"
ANS-->>Dev: "playbook output (with no_log where appropriate)"Important from the sequence diagram: the password only exists in memory while the playbook runs, and Ansible doesn’t write it to logs. But if a task echoes the password without no_log: true, the password can appear in output — always combine Vault with no_log on sensitive tasks.
Vault IDs: Per-Environment Isolation #
For projects with several environments (development, staging, production), using the same vault password for all environments is an unnecessary risk. If the staging vault password leaks, an attacker can decrypt the production vault files. Vault IDs solve this problem.
A Vault ID is a label identifying a specific vault. Files encrypted with a specific vault ID can only be decrypted with the vault password matching that ID.
# Encrypt a string with the 'production' vault ID
ansible-vault encrypt_string 'ProdSecret' \
--name 'db_password' \
--vault-id production@~/.vault_pass_prod
# Encrypt a string with the 'staging' vault ID
ansible-vault encrypt_string 'StagingSecret' \
--name 'db_password' \
--vault-id staging@~/.vault_pass_staging
Output for production:
db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
production # ← the vault ID is visible in the header
66386439653236336462626566653339...
When running a playbook with multiple vault IDs:
ansible-playbook site.yml \
--vault-id production@~/.vault_pass_prod \
--vault-id staging@~/.vault_pass_staging
Vault ID Usage Patterns Table #
| Pattern | Suitable for | Advantages | Disadvantages |
|---|---|---|---|
| Single global vault ID | Small project, one environment | Simple, one password | Leak = all secrets leak |
| Per environment (prod, staging, dev) | Multi-environment project | Per-environment isolation | Need to manage several passwords |
| Per team (backend, frontend, data) | Large organizations with dedicated teams | Per-team isolation | Can explode into many passwords |
| Per data type (db, api, ssl) | High compliance requirements | Granular, independent rotation | Needs a good vault catalog |
| Hybrid: per environment + per type | Enterprise | Maximum isolation | High complexity |
Recommendation: start with per environment (prod/staging/dev). Add granularity only if there’s a clear compliance or security need.
Don’t use the ‘default’ vault ID. Thedefaultvault ID is an alias for an unlabeled vault. Usingdefaultmakes it easy to accidentally decrypt files with the wrong password. Always use explicit labels likeproduction@pathorstaging@path.
Recommended Vault File Structure #
The best pattern for managing Vault is separating regular variables from sensitive variables into different files. This pattern makes code review easier (reviewers can read regular variables in Git without needing the vault password) and reduces the risk of accidentally committing secrets.
Directory Structure #
inventory/
production/
hosts.ini
group_vars/
all/
vars.yml # Regular variables — plaintext, safe in Git
vault.yml # Sensitive variables — ENCRYPTED
dbservers/
vars.yml # DB-specific configuration — plaintext
vault.yml # DB passwords — ENCRYPTED
webservers/
vars.yml
vault.yml
staging/
hosts.ini
group_vars/
all/
vars.yml
vault.yml
...
Example Contents #
# inventory/production/group_vars/dbservers/vars.yml — plaintext
db_host: prod-db.internal
db_port: 5432
db_name: app_production
db_user: app_user
db_pool_size: 20
db_password: "{{ vault_db_password }}" # Reference the variable from the vault
# inventory/production/group_vars/dbservers/vault.yml — ENCRYPTED
# ansible-vault encrypt_string 'SuperSecret123' --name 'vault_db_password'
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653339...
vault_db_admin_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
66386439653236336462626566653340...
With this pattern:
vars.ymlcan be reviewed in pull requests without the vault password — reviewers can read the configuration and judge whether it’s appropriate.vault.ymlonly contains secrets and is entirely encrypted.- Playbooks use
db_password(fromvars.yml) whose value is resolved fromvault_db_password(fromvault.yml). - When deploying to a different environment (staging), just swap the entire
group_vars/directory — the reference variables stay the same, only the values differ.
ANTI-PATTERN: one vault for all environments #
# ANTI-PATTERN: one vault.yml for all environments
inventory/
group_vars/
all/
vault.yml # ← contains BOTH production and staging secrets
# Contents of vault.yml — all secrets in one file
vault_prod_db_password: SuperSecret123
vault_staging_db_password: LessSecret456
# Problem: leaking one file = leaking ALL environments
# Problem: rotating the production password requires editing the same file as staging
# CORRECT: vault per environment
inventory/
production/
group_vars/all/vault.yml # only production secrets
staging/
group_vars/all/vault.yml # only staging secrets
development/
group_vars/all/vault.yml # only development secrets
# CORRECT (more granular): vault per environment per group
inventory/
production/
group_vars/
dbservers/vault.yml # production DB-specific secrets
webservers/vault.yml # production web-specific secrets
Vault in CI/CD Pipelines #
Using Vault in CI/CD requires special attention because CI runners aren’t engineers who can type a password at a prompt. There are several patterns for providing the vault password in pipelines.
Pattern 1: Secret on the CI platform #
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
environment: production # Requires manual approval
steps:
- uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Ansible
run: pip install ansible
- name: Create the vault password file
run: |
echo "${{ secrets.VAULT_PASSWORD }}" > /tmp/.vault_pass
chmod 600 /tmp/.vault_pass
- name: Run the playbook
run: |
ansible-playbook -i inventory/production/ site.yml \
--vault-password-file /tmp/.vault_pass
- name: Clean up the vault password
if: always()
run: rm -f /tmp/.vault_pass
Critical points:
chmod 600— the password file must only be readable by the owner.if: always()— cleanup must run even if the playbook fails, so the password doesn’t linger on the runner.environment: production— GitHub Actions provides an approval gate before a job runs against a protected environment.
Pattern 2: Vault password from a cloud secret manager #
# .github/workflows/deploy.yml
- name: Fetch the vault password from AWS Secrets Manager
run: |
aws secretsmanager get-secret-value \
--secret-id ansible/vault-password \
--query SecretString \
--output text > /tmp/.vault_pass
chmod 600 /tmp/.vault_pass
env:
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
This pattern is safer because the vault password itself is rotated by AWS, and CI doesn’t need to know the plaintext value — only access to Secrets Manager.
Pattern 3: AWX / Ansible Tower as the secret store #
For teams already using AWX or Ansible Tower, the Vault Credential in AWX is the best approach. AWX stores vault passwords in its internal database encrypted with Fernet, and these credentials can only be accessed by Job Templates with the right permissions.
# ansible.cfg on the CI runner — no vault_password_file needed
[defaults]
# AWX handles the vault password, not the runner
The CI runner triggers an AWX Job Template via API or webhook; AWX handles the vault decryption using securely stored credentials.
AWX Vault Credential vs Vault Password File — AWX Vault Credential is the safer approach for organizations. The vault password is stored encrypted in the AWX database, audit logs record who used the credential, and rotation only needs to happen once in AWX. CI runners never see the vault password. For small teams not yet using AWX, secrets on the CI platform are sufficient.
ANTI-PATTERN: vault password in pipeline logs #
# ANTI-PATTERN: echoing the password in a step that fails during debugging
- name: Debug vault issue
run: |
cat /tmp/.vault_pass # ← DON'T! The password will appear in GitHub Actions logs
# CORRECT: use masking and delete the file before debugging
- name: Debug vault issue
run: |
if [ ! -s /tmp/.vault_pass ]; then
echo "Vault file is empty or missing"
fi
# DON'T cat /tmp/.vault_pass in logs
# Use 'echo "::add-mask::$(cat /tmp/.vault_pass)"' to mask it in logs
GitHub Actions automatically detects strings that look like secrets and masks them in logs. But don’t rely on auto-masking — always avoid echoing secret values in commands visible in logs.
Rekey: Vault Password Rotation #
Vault passwords should be rotated periodically — just like any other account password. If there’s any chance a password leaked (lost laptop, contributor leaving the team, exposed repository mirror), change the password immediately. ansible-vault rekey enables rotation without manually decrypting and re-encrypting files.
# Rekey a single file
ansible-vault rekey group_vars/production/vault.yml
# Prompt: Vault password (old)
# Prompt: New Vault password (new)
# Prompt: Confirm New Vault password
# Rekey all vault files in a directory
find group_vars/ -name 'vault.yml' -exec ansible-vault rekey {} \;
# Rekey with a specific vault ID
ansible-vault rekey group_vars/production/vault.yml --vault-id production@prompt
Rekey accepts the old and new passwords in one operation, and only produces new ciphertext — there’s never a stage where the plaintext file is written to disk. After rekeying, commit the rekeyed files to Git.
When to Rekey #
| Situation | Action | Priority |
|---|---|---|
| A developer who knew the vault password leaves the team | Rekey all vaults they knew | Urgent |
| A laptop/device containing the vault password is lost | Rekey all vaults on that device | Urgent |
| A security audit finds the password in logs/backups | Rekey the affected vaults | Urgent |
| Periodic rotation (e.g. every 90 days) | Rekey all vaults | Routine |
| No incident, just best practice | No rekey needed | — |
Rekey only changes the password, not the ciphertext salt. The salt is generated once per file and stays the same after rekeying. For maximum security, decrypt all files, remove them from Git history, then re-encrypt with a new password. But for most cases, rekey is sufficient.Vault Integration with Roles and Playbooks #
Vault doesn’t stand alone — it works with roles and playbooks to provide secrets to the tasks that need them. A good understanding of this integration prevents common mistakes like unresolved variables or secrets leaking into logs.
Pattern 1: Vault Variables Referenced in vars.yml
#
# roles/webserver/defaults/main.yml
db_host: localhost
db_port: 5432
# roles/webserver/vars/main.yml
# (vars/main.yml has higher priority than defaults)
# inventory/production/group_vars/webservers/vars.yml
db_host: prod-db.internal
db_password: "{{ vault_db_password }}" # Reference the variable from vault.yml
# inventory/production/group_vars/webservers/vault.yml (ENCRYPTED)
vault_db_password: !vault |
$ANSIBLE_VAULT;1.1;AES256
...
# playbook site.yml
- hosts: webservers
roles:
- role: webserver
When the playbook runs, Ansible loads variables in order: defaults → inventory vars → play vars → role vars → host_vars → group_vars. The vault_db_password variable from vault.yml is resolved first, then db_password in vars.yml takes its value. The webserver role receives the resolved db_password when its tasks run.
The role article discusses variable precedence in more detail. For context on how roles are composed into larger playbooks, see automation.
Pattern 2: no_log on Sensitive Tasks
#
Variables resolved from the vault can appear in Ansible output if the task using those variables is logged. Add no_log: true to prevent this:
# CORRECT: hide the output of tasks using secrets
- name: Set the database password for the application user
postgresql_user:
name: app_user
password: "{{ db_password }}"
state: present
no_log: true # Ansible won't display this task's command or output
no_log: true hides the entire task output — including error messages. This is a trade-off between security and debugging ease. For development, you can disable no_log; for production, always enable it.
When NOT to useno_log: when you need to debug a failed task and that task’s output isn’t sensitive. But remember, if a task uses variables from the vault, its output is most likely sensitive. The safe default:no_log: trueon every task using vault variables.
Pattern 3: Tasks That Write Secrets to Files on Managed Nodes #
Sometimes secrets need to be written to files on the destination server (for example database configuration used by applications). This pattern needs special attention:
- name: Deploy the database configuration
template:
src: db_config.j2
dest: /etc/myapp/db.conf
owner: myapp
group: myapp
mode: '0600' # Only the owner can read
no_log: true
# db_config.j2
host={{ db_host }}
port={{ db_port }}
user={{ db_user }}
password={{ db_password }}
Note the mode: '0600' — configuration files containing passwords must only be readable by the user running the application. 0600 means only the owner has read and write access.
When Not to Use Vault / Alternatives #
Vault is an excellent solution for most cases, but it isn’t the only one. There are situations where other approaches are more appropriate.
Keep Using Vault #
STILL use Ansible Vault if:
✓ Secrets are stored in the Git repository (config files, inventory, role defaults)
✓ Small-to-medium teams without a dedicated secret manager
✓ Open-source projects or public repositories (Vault enables sharing
encrypted secrets without exposing values)
✓ Compliance doesn't require granular per-secret-access audit trails
✓ Secret rotation is done manually per release (no real-time rotation needed)
Consider an External Secret Manager #
CONSIDER HashiCorp Vault / AWS Secrets Manager if:
✗ Secrets change in real time (automatic rotation every hour/minute)
✗ Compliance requires detailed per-access audit trails
✗ Large teams with many applications needing different secrets
✗ Secrets are shared between many systems (Ansible + Kubernetes + applications)
✗ There's a need for dynamic secrets (e.g. database credentials changing per connection)
Comparison Table: Vault vs Alternatives #
| Aspect | Ansible Vault | HashiCorp Vault | AWS Secrets Manager |
|---|---|---|---|
| Setup complexity | Low (built-in) | High (needs a server) | Medium (needs an AWS account) |
| Audit trail | Git history | Detailed per-access logs | CloudTrail + access logs |
| Automatic rotation | No (manual rekey) | Yes (dynamic secrets) | Yes (Lambda rotation) |
| Cost | Free (part of Ansible) | Free (open source) + infra | Per secret per month |
| Suitable for | Static secrets in repos | Dynamic, high-security secrets | Projects in the AWS ecosystem |
| Offline support | Yes (local encrypted files) | No (needs a running server) | No (needs an AWS connection) |
You can also combine Vault with a secret manager: use Vault for configuration files stored in the repo, and the secret manager for very sensitive secrets (like database root passwords). Ansible pulls secrets from both sources at runtime, and the secret manager becomes the authoritative source for the most critical secrets.
The encryption article discusses encryption layers and external secret manager integration in more detail. For workflow and approval gate patterns combining Vault with access control, see workflow.
Summary #
- Ansible Vault encrypts sensitive data using AES-256 with PBKDF2 key derivation — encrypted files are safe to store in Git without the risk of leaking to unauthorized people.
- Two modes are available: full file encryption (
ansible-vault encrypt) for entirely sensitive files, and string encryption (ansible-vault encrypt_string) for individual values in YAML files that are mostly regular content.- Separate regular variables from the vault —
vars.yml(plaintext, safe for review) referencesvault.yml(encrypted) — this pattern makes code review easier and reduces the risk of accidental commits.- The vault password must never be committed to Git — store it in a local file with
chmod 600in.gitignore, or as a secret on the CI/CD platform, or in an AWX Vault Credential for larger organizations.- Use different vault IDs per environment (
production@path,staging@path) — a leaked staging vault password doesn’t compromise the production vault, and per-environment rotation can be done independently.- Rotate passwords with
ansible-vault rekey— there’s never a stage where the plaintext file is written to disk, so rekeying is safe to do routinely without risk.- Integrate with
no_log: trueon tasks using vault variables — withoutno_log, secrets can appear in Ansible output when tasks fail and run in verbose mode.- Vault isn’t the only solution — for very sensitive secrets or real-time rotation needs, consider an external secret manager (HashiCorp Vault, AWS Secrets Manager) and fetch secrets at runtime using lookup plugins.