Ansible OEL 8 DevOps · OEL 8 · Secrets

AnsibleVault Best Practices

The operational rules that turn Ansible Vault from a feature into a discipline — file naming conventions, .gitignore patterns, password storage, CI integration, and the rotation playbook for a leaked password.

The most important convention: keep encrypted secrets in a file named vault.yml and reference them from a plain vars.yml using indirection. This way every secret in your playbooks is read from vars.yml, but the actual value lives encrypted in vault.yml.

YAML — group_vars/all/vault.yml (encrypted)
---
# Encrypted file — every key starts with vault_ for clarity
vault_mysql_root_password: "ChangeMeNow123!"
vault_appuser_password:    "AnotherSecret456!"
vault_api_token:           "tok_aBc123XyZ"
vault_tls_private_key: |
  -----BEGIN PRIVATE KEY-----
  MIIEvQIBADANBgkqhkiG9w0BAQEFAA...
  -----END PRIVATE KEY-----
YAML — group_vars/all/vars.yml (plain — indirection)
---
# Plain file — playbooks reference these names
mysql_root_password: "{{{{ vault_mysql_root_password }}}}"
appuser_password:    "{{{{ vault_appuser_password }}}}"
api_token:           "{{{{ vault_api_token }}}}"
tls_private_key:     "{{{{ vault_tls_private_key }}}}"

# Plain settings live here too — no encryption needed
mysql_port: 3306
backup_retention_days: 14
💡 Tip: Why the indirection? Three benefits: (1) playbooks reference one consistent name regardless of whether the source is encrypted, (2) grep'ing for usages works without decrypting, (3) you can move a value between encrypted/plain by editing only vault.yml + vars.yml — never the playbooks.
What goes in git, what stays out ✓ commit to git group_vars/all/vars.yml — plain settings group_vars/all/vault.yml — encrypted secrets .vault-id-list — vault id mappings (no keys) .gitignore ✗ NEVER in git .vault_pass — the master password file .vault_pass_dev / _prod — per-env passwords ~/.ssh/id_* — SSH private keys decrypted *.tmp files Encrypted file = safe in git · Password file = safe ONLY in CI secrets / 1Password
INI — .gitignore
# Vault password files — NEVER commit these
.vault_pass
.vault_pass_*
*.vault_pass

# Decrypted backups, accidental dumps
*.tmp
*.decrypted

# Local CI scratch
.molecule/
*.retry

# SSH private keys
id_rsa
id_ed25519
*.pem
StorageProsCons
Plain file in ~/.ansible/vault_passSimple, no authAnyone with shell access can read it
1Password / Bitwarden CLIAudit log, MFA, easy rotationRequires CLI tools on every dev box
HashiCorp Vault (different "vault")Centralised, audited, dynamic secretsInfrastructure to run
AWS Secrets ManagerIf you're already in AWSCloud lock-in
CI variable (GitLab/GitHub)Native, encrypted at restOne copy per CI provider
BASH — vault password from 1Password CLI
# Create a script that prints the password to stdout
cat > scripts/get_vault_pass.sh << 'EOF'
#!/usr/bin/env bash
op item get "Ansible Vault Production" --fields password
EOF
chmod +x scripts/get_vault_pass.sh

# Use it
ansible-playbook site.yml --vault-password-file scripts/get_vault_pass.sh

# Or in ansible.cfg
[defaults]
vault_password_file = ./scripts/get_vault_pass.sh
YAML — GitLab CI example
---
deploy_production:
  stage: deploy
  image: quay.io/ansible/creator-ee:latest
  variables:
    ANSIBLE_HOST_KEY_CHECKING: "False"
  before_script:
    # vault password comes from a CI/CD masked variable
    - echo "$VAULT_PASS_PROD" > .vault_pass_prod
    - chmod 600 .vault_pass_prod
    # SSH key likewise
    - echo "$SSH_PRIVATE_KEY" > ~/.ssh/id_ed25519
    - chmod 600 ~/.ssh/id_ed25519
  script:
    - ansible-playbook site.yml \
        -i inventories/production \
        --vault-id prod@.vault_pass_prod
  after_script:
    # Always wipe the password file at the end of the job
    - rm -f .vault_pass_prod
  only:
    - main
YAML — GitHub Actions example
---
name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Setup SSH key
        run: |
          mkdir -p ~/.ssh
          echo "${{{{ secrets.SSH_PRIVATE_KEY }}}}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519

      - name: Setup vault password
        run: |
          echo "${{{{ secrets.VAULT_PASS_PROD }}}}" > .vault_pass_prod
          chmod 600 .vault_pass_prod

      - name: Run playbook
        run: |
          ansible-playbook site.yml \
            -i inventories/production \
            --vault-id prod@.vault_pass_prod

      - name: Cleanup
        if: always()
        run: rm -f .vault_pass_prod

If a vault password leaks, here's the recovery procedure:

  1. Generate a new password. Use openssl rand -base64 32 for strong randomness.
  2. Re-encrypt every file with the new password.
  3. Update the password in your secret store (1Password, CI variable, etc.).
  4. Rotate the underlying secrets the vault contained (the leaked password may have been used to read them).
BASH — bulk rekey
# Find every vault-encrypted file in the repo
find . -type f -name "*.yml" -exec sh -c \
  'head -1 "$1" | grep -q "ANSIBLE_VAULT" && echo "$1"' _ {{}} \;

# Rekey each one
for f in $(find . -type f -name "*.yml" -exec sh -c \
    'head -1 "$1" | grep -q "ANSIBLE_VAULT" && echo "$1"' _ {{}} \;); do
  ansible-vault rekey \
    --vault-id old@~/.vault_pass_old \
    --new-vault-id new@~/.vault_pass_new \
    "$f"
done

# Commit the re-encrypted files
git add -A && git commit -m "Rotate vault password $(date +%Y-%m-%d)"
⚠ Warning: After a leak, treat every secret inside the vault as also compromised. Rotate database passwords, API keys, and TLS keys — not just the vault password itself. The cost of over-rotating is small; the cost of trusting a leaked vault is enormous.
  • Never commit a .vault_pass file or any unencrypted credentials.
  • Never echo a vault password into shell history. Use file-based access or stdin.
  • Always use different vault passwords per environment (dev/staging/prod).
  • Always wipe vault password files at the end of CI jobs (rm -f in after_script).
  • Audit who has each environment's vault password — same group of people who have prod SSH access, no more.
✅ Tip: Vault is plumbing — invisible when done right, painful when sloppy. The two-file pattern + rigorous .gitignore + per-environment passwords + CI rotation discipline covers 95% of the practical risk.