Ansible Vault: Encrypting Secrets in Playbooks and Storing Them Safely in Git

Security tutorial - IT technology blog
Security tutorial - IT technology blog

3 AM. I had just finished pushing a bunch of Ansible playbooks to GitHub to sync with a new server. Five minutes later, my phone buzzed — an email from GitHub Security Alert: “We found a potential secret exposed in your repository.” Database password, WordPress auth key, monitoring tool API key — all sitting right inside vars/main.yml, plain text, in a public repo. That was the first time I learned the lesson about Ansible Vault the hard way.

If you’re using Ansible to manage infrastructure and storing playbooks on Git — even in a private repo — this is a problem you need to solve before it becomes a real incident.

The Problem: Secrets in Ansible Playbooks and Git

Ansible playbooks often require a lot of sensitive information: database passwords, SSH private keys, cloud provider API tokens, Telegram bot tokens, SMTP passwords… These things must exist in the codebase for Ansible to use them. Commit to Git and you expose secrets. Don’t commit and the team can’t share them, new servers won’t have the vars they need to run.

This is a classic conflict: infrastructure-as-code wants everything in Git, but secrets are not allowed in Git.

Comparing 4 Approaches to Managing Secrets in Ansible

Approach 1: Hardcode directly in the vars file

# vars/main.yml — DON'T DO THIS
db_password: "MyP@ssw0rd123"
api_key: "sk-ant-api03-xxxxxxxxxxxxx"
wp_secret_key: "abcdefghijklmnop"

Convenient in the moment, but once committed to Git — even if you delete it later — it’s still in the git history. git log -p will find it. GitHub Secret Scanner will find it. Bots scanning public repos will find it within minutes of your push.

Approach 2: Environment variables

# vars/main.yml
db_password: "{{ lookup('env', 'DB_PASSWORD') }}"
api_key: "{{ lookup('env', 'API_KEY') }}"

Better for security — nothing sensitive in git. But when deploying to 10 servers, you have to set env vars on each one individually. When onboarding a new team member, you have to share them via Slack DM or email — also not secure. When rotating passwords, you have to update every location manually.

Approach 3: External Secret Manager (HashiCorp Vault, AWS Secrets Manager)

# vars/main.yml with HashiCorp Vault lookup
db_password: "{{ lookup('hashi_vault', 'secret=secret/db/password:value') }}"

An enterprise solution — full audit logs, fine-grained access control, auto-rotation. But to run HashiCorp Vault you need a dedicated server cluster, you need to configure authentication, and you need to maintain it. For a small team or side project, this is overkill. AWS Secrets Manager is great but locks you into AWS and costs extra money every month.

Approach 4: Ansible Vault (built-in, free)

Ansible Vault is a built-in feature of Ansible — no additional installation required. It encrypts var files with AES-256, and you can commit the encrypted file to Git completely safely. Only those with the vault password can decrypt it.

Pros and Cons Analysis

Approach Security Convenience Team sharing Cost
Hardcode Very bad Very easy Easy Free
Env vars Good Complex Difficult Free
External Vault Best Very complex Good Paid / infra
Ansible Vault Good Moderate Good Free

Ansible Vault sits at the sweet spot: secure enough, convenient enough, shareable across the team via git, and completely free. For 80% of use cases — personal VPS, small to mid-sized teams, startups — it’s the most sensible choice.

When Should You Choose Ansible Vault?

  • You’re already using Ansible to deploy or configure servers
  • Team size is 1–20 people, not yet needing enterprise secret management
  • You want to store all infrastructure config on Git, including secrets
  • You don’t want to set up additional infrastructure just to manage secrets
  • Compliance doesn’t require detailed audit logs for every secret access

If you need fine-grained role-based access control, a full audit trail, or you’re operating at enterprise scale — that’s when to consider HashiCorp Vault.

Implementing Ansible Vault: Step-by-Step Guide

Step 1: Create a strong vault password

The vault password is the key to decrypting all your secrets — it needs to be strong and stored securely (in a team password manager, not a sticky note or Slack message).

I use the password generator at toolcraft.app to create vault passwords — the tool runs 100% in the browser so there’s no risk of the password being sent to any server. After generating, save it immediately to your team’s Bitwarden or 1Password.

# Create vault password file in project (remember to add to .gitignore)
echo "your-very-strong-vault-password-here" > .vault_pass
chmod 600 .vault_pass

Step 2: Create and encrypt the secrets file

Separate secrets into their own file — a common convention is vars/vault.yml or group_vars/all/vault.yml. Use a vault_ prefix for variable names to make them easy to identify:

# vars/vault.yml — content BEFORE encrypting
vault_db_password: "MySecretDbPass123!"
vault_api_key: "sk-ant-api03-xxxxxxxxxxxxxxxxx"
vault_wp_secret_key: "random-64-char-string-here"
vault_telegram_bot_token: "1234567890:ABCDEFxxxxx"
vault_smtp_password: "smtp-password-here"
# Encrypt file (if file doesn't exist yet, use 'create' instead of 'encrypt')
ansible-vault encrypt vars/vault.yml --vault-password-file .vault_pass

# Create a new file and open editor to enter content immediately
ansible-vault create vars/vault.yml --vault-password-file .vault_pass

After encrypting, the file looks like this — completely safe to commit to git:

$ANSIBLE_VAULT;1.1;AES256
38663266623431313530356635663438656665383066346433383836326438633165303832666562
3566356334363931313630353962373061386564326266310a623539353531313135356235363033
...

Step 3: Reference vault vars in the playbook

The vars/main.yml file (unencrypted, committed normally) references vault vars:

# vars/main.yml — no secrets, only references
db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"
wp_secret_key: "{{ vault_wp_secret_key }}"
# deploy.yml
---
- hosts: webservers
  vars_files:
    - vars/main.yml
    - vars/vault.yml
  tasks:
    - name: Configure database
      template:
        src: templates/db.conf.j2
        dest: /etc/myapp/db.conf
      # Template uses {{ db_password }} as normal

Step 4: Run the playbook with vault

# Run with vault password file
ansible-playbook deploy.yml --vault-password-file .vault_pass

# Set environment variable (convenient for CI/CD)
export ANSIBLE_VAULT_PASSWORD_FILE=.vault_pass
ansible-playbook deploy.yml

# Enter password manually (for dev environment)
ansible-playbook deploy.yml --ask-vault-pass

Step 5: Configure .gitignore

# .gitignore
.vault_pass
*.vault_pass
.ansible_vault_password

# DO NOT add vars/vault.yml — the file is encrypted, safe to commit

Step 6: View and edit the vault file

# View decrypted content (without saving to a plain text file)
ansible-vault view vars/vault.yml --vault-password-file .vault_pass

# Edit directly (opens editor, re-encrypts on save)
ansible-vault edit vars/vault.yml --vault-password-file .vault_pass

Step 7: Rotate the vault password when needed

When a team member leaves or the vault password is suspected to be compromised — rotate immediately:

# Rekey with new password
ansible-vault rekey vars/vault.yml \
  --vault-password-file .vault_pass \
  --new-vault-password-file .vault_pass_new

# Then replace old password file with new one
mv .vault_pass_new .vault_pass

CI/CD Integration (GitHub Actions)

Store the vault password in a GitHub repository secret, then use it in your workflow:

# .github/workflows/deploy.yml
- name: Deploy with Ansible
  env:
    ANSIBLE_VAULT_PASSWORD: ${{ secrets.ANSIBLE_VAULT_PASSWORD }}
  run: |
    echo "$ANSIBLE_VAULT_PASSWORD" > /tmp/.vault_pass
    chmod 600 /tmp/.vault_pass
    ansible-playbook deploy.yml --vault-password-file /tmp/.vault_pass
    rm /tmp/.vault_pass

Common Errors

Error: “Decryption failed (no vault secrets would decrypt)” — Wrong vault password or the vault file is corrupt. Check your .vault_pass file, particularly for trailing spaces or Windows line endings:

cat -A .vault_pass
# Line ending must be $ (Unix newline)
# If you see ^M$ it's a Windows line ending — fix it:
sed -i 's/\r//' .vault_pass

Accidentally committed an unencrypted file: Use BFG Repo Cleaner to remove it from git history, then rotate all exposed secrets immediately — no delays.

Need to encrypt just a single value: Ansible Vault supports inline encryption — paste the output directly into your vars file:

ansible-vault encrypt_string 'my-secret-value' \
  --name 'vault_db_password' \
  --vault-password-file .vault_pass

After That Incident

After that 3 AM wake-up call, I migrated all my Ansible playbooks over to Vault in about 2 hours. The pattern is straightforward: separate secrets into vars/vault.yml, encrypt it, commit to git, share the vault password with the team via a password manager. For the vault password itself, I generate it at toolcraft.app — it runs entirely client-side so nothing gets sent to any third-party server.

This isn’t a perfect solution — if the vault password leaks, all secrets leak with it. But compared to hardcoding plain text in git or sharing secrets over Slack, Ansible Vault is a significant improvement with minimal setup effort. For most small and medium teams using Ansible, this is enough — and far better than doing nothing at all.

Share: