Skip to content

Ansible Vault Best Practices


This chapter revolves around best practice and common patterns of using Ansible Vault — genuinely common, practical ways Vault gets used in real projects, plus a direct return to the exact question this whole section opened with.

Pattern: Never Committing The Password — .gitignore

# .gitignore
.vault_pass.txt
*.vault_pass

Simple, but a genuine safety net rather than just documentation of intent — even with the best of intentions, a careless git add . shouldn’t be able to accidentally stage a password file. Let .gitignore enforce what good habits alone shouldn’t have to.

Also the best thing even above .gitignore is simply not putting secret file on your repository directory — keep it elsewhere with restrictive 400 or 600 permission.

Pattern: Choosing Whole-File vs. encrypt_string

  • Whole-file encryption (ansible-vault create/encrypt) fits a file that’s mostly or entirely sensitive — a dedicated secrets.yaml with nothing else in it. Simpler workflow, one password unlocks everything inside.
  • encrypt_string fits a file that’s primarily plain configuration, with just one or a few genuinely sensitive values mixed in. Keeps the rest of the file readable and diffable in git, rather than hiding non-sensitive settings behind encryption for no real benefit.

Pattern: Rebuilding The env-Based Secret, Properly

This is the direct payoff promised at the very start of this section. Recall the lookups section’s pattern:

# From the lookups section — depends on the control node's own environment
vars:
  db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
tasks:
  - name: Fail if the secret is missing
    ansible.builtin.fail:
      msg: "DB_PASSWORD is not set on the control node"
    when: db_password | length == 0

Rebuilt properly with Vault:

vars_files:
  - secrets.yaml  # encrypted, containing db_password
tasks:
  - name: Use the secret
    ansible.builtin.template:
      src: db.conf.j2
      dest: /etc/myapp/db.conf

No dependency on someone having manually exported DB_PASSWORD on whatever machine happens to run this playbook, and no emptiness check needed at all — the secret genuinely lives in the repository, encrypted, reproducible by anyone with the vault password from a completely clean clone, with nothing separate to set up or remember.

Pattern: Separate Encrypted Files Per Environment

Combining vars_files with --vault-id, for genuinely different secrets per environment:

vars_files:
  - "secrets_{{ deployment_env }}.yaml"
ansible-vault encrypt secrets_dev.yaml --vault-id dev@prompt
ansible-vault encrypt secrets_prod.yaml --vault-id prod@prompt
ansible-playbook site.yaml -e "deployment_env=prod" \
  --vault-id dev@~/.vault_pass_dev.txt \
  --vault-id prod@~/.vault_pass_prod.txt

Each environment’s secrets are protected by their own password, loaded based on which environment the run actually targets.

Pattern: An Encrypted Default In A Role

Combining the roles section’s defaults/main.yml with !vault directly:

roles/myapp/defaults/main.yml
app_port: 8080
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          ...

A role can ship with a sensible, encrypted default password — for a development or testing environment, say — while remaining genuinely overridable by anyone applying it who needs a different value for production, exactly per the sensitivity-versus-configurability distinction from earlier in this section.

Pattern: A Secret Flowing Into A Template

Combining directly with template from the common modules section:

templates/db.conf.j2
password = {{ db_password }}
- name: Deploy db config with secret
  ansible.builtin.template:
    src: db.conf.j2
    dest: /etc/myapp/db.conf
    mode: '0600'

mode: '0600' here matters for exactly the reason permissions have mattered throughout this course — the rendered file, sitting on the remote host, now contains a genuine decrypted secret. Protecting the vault password locally means nothing if the file it ultimately produces is left world-readable on the server it was deployed to.

Pattern: Vault Passwords From A Real Secrets Manager

Expanding on the executable-script option from earlier in this section — the standard way Vault integrates with CI/CD:

#!/usr/bin/env bash
# get_vault_pass.sh
curl -s https://secrets.example.com/api/vault-password
chmod +x get_vault_pass.sh
ansible-playbook site.yaml --vault-password-file ./get_vault_pass.sh

Ansible recognizes an executable file passed to --vault-password-file and runs it, using its output as the password, rather than reading it as static content. This is how Vault genuinely integrates with a real secrets platform in an automated pipeline — the actual vault password is never stored at rest on the CI runner at all.

A Pre-Commit Safety Checklist

  • Does git diff show a $ANSIBLE_VAULT header — not plaintext — for every secrets file about to be committed?
  • Is the vault password file itself covered by .gitignore, and confirmed absent from git status?
  • If ansible-vault decrypt was used for any reason, was the file re-encrypted before committing?
  • Is the correct --vault-id label being used for the environment this specific secret actually belongs to?

Best Practices

  • Never commit a vault password, only vault-encrypted content — enforce this with .gitignore, not just discipline alone.
  • Choose whole-file encryption for dedicated secrets files, encrypt_string for mostly-plaintext files with a few sensitive values — match the tool to how much of the file actually needs protecting.
  • Reach for Vault, not env, the moment a secret needs to be part of your reproducible, version-controlled repository — this was this section’s entire opening argument, now fully demonstrated in practice.
  • Set restrictive permissions on any file that ends up containing a decrypted secret, on the remote host as much as the control node — protecting the vault password means little if the deployed result is left exposed.
Last updated on