Skip to content

Ansible Vault: A Practical Deep Dive

Ansible

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:

$ANSIBLE_VAULT;1.1;AES256

When a vault ID label is used, the header can include that label:

$ANSIBLE_VAULT;1.2;AES256;prod

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.

db_password: correct-horse-battery-staple

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:

db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  363438316439316461613732...

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:

python3 -m pip install --user ansible

Or install ansible-core if you only need the core command-line tools:

python3 -m pip install --user ansible-core

Verify ansible-vault is available:

ansible-vault --version

Check the help output:

ansible-vault --help

The basic syntax is:

ansible-vault <action> [options] <file>

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:

ansible-vault view group_vars/all/vault.yml --ask-vault-pass

You can also store it outside the repository in a password file:

ansible-vault view group_vars/all/vault.yml \
  --vault-password-file ~/.ansible/vault-password

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:

  • dev
  • stage
  • prod
  • shared

Use a vault ID with a prompt:

ansible-vault create group_vars/prod/vault.yml --vault-id prod@prompt

Use a vault ID with a password file:

ansible-vault view group_vars/prod/vault.yml \
  --vault-id prod@~/.ansible/prod-vault-password

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
db_password: !vault |
  $ANSIBLE_VAULT;1.1;AES256
  336335363736...

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:

mkdir ansible-vault-demo
cd ansible-vault-demo

Create a place for inventory variables:

mkdir -p group_vars/all

Creating an Encrypted File

Create a new encrypted file:

ansible-vault create group_vars/all/vault.yml

Ansible prompts for a vault password, opens your editor, and encrypts the file when you save and close it.

Add this content:

db_user: app_user
db_password: correct-horse-battery-staple
api_token: demo-token-value

After saving, inspect the file with sed or head:

head group_vars/all/vault.yml

You should see encrypted vault text, not the YAML values.

Editing Encrypted Content

Edit the file safely:

ansible-vault edit group_vars/all/vault.yml

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:

ansible-vault view group_vars/all/vault.yml

This is safer than decrypting the file just to read it.

Encrypting an Existing File

Create a plaintext file:

cat > group_vars/all/plaintext.yml <<'EOF'
service_account_password: temporary-secret
EOF

Encrypt it:

ansible-vault encrypt group_vars/all/plaintext.yml

The file is rewritten as encrypted content.

Decrypting a File

Decrypt only when you truly need plaintext:

ansible-vault decrypt group_vars/all/plaintext.yml

In normal workflows, prefer view or edit.

If you need a decrypted copy without replacing the encrypted original:

ansible-vault decrypt group_vars/all/vault.yml --output /tmp/vault-cleartext.yml

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:

ansible-vault encrypt_string --name db_password --prompt

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:

ansible-vault encrypt_string 'correct-horse-battery-staple' --name db_password

That's convenient, but the value may appear in shell history. Prefer --prompt for real secrets.

Use Encrypted Variables in Playbooks

Create inventory.yml:

all:
  hosts:
    localhost:
      ansible_connection: local

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:

ansible-playbook -i inventory.yml site.yml --ask-vault-pass

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:

ansible-playbook -i inventory.yml site.yml --ask-vault-pass

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:

ansible-playbook -i inventory.yml site.yml \
  --vault-password-file ~/.ansible/dev-vault-password

Add local vault password paths to .gitignore if your project has any local helper files:

.vault-password
.vault-password-*
vault-password*

Use Multiple Vault IDs

Create environment directories:

mkdir -p group_vars/dev group_vars/prod

Create a dev vault:

ansible-vault create group_vars/dev/vault.yml --vault-id dev@prompt

Create a prod vault:

ansible-vault create group_vars/prod/vault.yml --vault-id prod@prompt

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:

[defaults]
vault_password_file = ~/.ansible/dev-vault-password

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:

ansible-vault create group_vars/all/vault.yml

Add:

db_password: replace-with-real-secret

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:

ansible-playbook -i inventory.yml site.yml --ask-vault-pass

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:

inventory/
  dev.yml
  prod.yml
group_vars/
  dev/
    main.yml
    vault.yml
  prod/
    main.yml
    vault.yml
site.yml

Create group_vars/dev/main.yml:

app_environment: dev
db_host: dev-db.internal.example.com
db_user: dev_app

Create group_vars/prod/main.yml:

app_environment: prod
db_host: prod-db.internal.example.com
db_user: prod_app

Create the dev vault:

ansible-vault create group_vars/dev/vault.yml --vault-id dev@prompt

Create the prod vault:

ansible-vault create group_vars/prod/vault.yml --vault-id prod@prompt

Each file can define the same variable name:

db_password: environment-specific-secret

Run dev:

ansible-playbook -i inventory/dev.yml site.yml \
  --vault-id dev@~/.ansible/dev-vault-password

Run prod:

ansible-playbook -i inventory/prod.yml site.yml \
  --vault-id prod@~/.ansible/prod-vault-password

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.

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: true on 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:

ansible-vault rekey group_vars/prod/vault.yml

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:

rg -n "password|passwd|token|secret|api_key|private_key|credential" group_vars host_vars roles

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:

ansible-vault decrypt group_vars/prod/vault.yml

Prefer:

ansible-vault view group_vars/prod/vault.yml

or:

ansible-vault edit group_vars/prod/vault.yml

Printing Secrets During Debugging

This is easy to do:

- name: Debug all variables
  ansible.builtin.debug:
    var: vars

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:

  1. Add ansible.cfg vault settings for local consistency.
  2. Add .gitignore entries for local password files.
  3. Split secrets by environment with vault IDs.
  4. Use no_log: true in roles that handle secrets.
  5. Add CI checks that search for likely plaintext secrets.
  6. Store CI vault passwords in your CI platform's secret store.
  7. Combine Vault with roles and collections cleanly.
  8. Evaluate an external secret manager when rotation, audit, or dynamic credentials become required.

In CI, the pattern usually looks like this:

ansible-playbook -i inventory/prod.yml site.yml \
  --vault-id prod@./ci/read-prod-vault-password

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.

References