๐ŸŽ New User? Get 20% off your first purchase with code NEWUSER20 ยท โšก Instant download ยท ๐Ÿ”’ Secure checkout Register Now โ†’
Menu

Categories

Ansible Vault in Production: Encrypting Secrets Without Git History

Ansible Vault in Production: Encrypting Secrets Without Git History
You have playbooks managing production servers. Database passwords, API keys and SSL private keys sit in YAML files. Before you push to the shared repository or hand the automation to CI/CD, those secrets must be encrypted. Ansible Vault is the built-in answer for ansible vault production workflows, but the first time you encrypt a file that already has Git history, you need to rewrite commits or the plaintext stays searchable forever. This article shows the exact commands to encrypt secrets, scrub Git history, and structure vault files for production use. Vault uses AES256 symmetric encryption. You supply a password, Ansible encrypts the entire YAML file, and you check in the ciphertext. When you run the playbook, you provide the vault password again and Ansible decrypts in memory. The workflow keeps secrets out of logs and diffs while letting you version-control the encrypted blobs alongside your infrastructure code.

How Ansible Vault Encrypts YAML Files

Encrypt a variables file with ansible-vault encrypt:

ansible-vault encrypt group_vars/prod/vault.yml
New Vault password: 
Confirm New Vault password: 
Encryption successful

The file is now AES256-encrypted. Open it and you see a header followed by hex-encoded ciphertext:

$ANSIBLE_VAULT;1.1;AES256
66386439653034323432343032626435653863643936363966613330656430313735646335376239
3765373836363837316564346437653964383432626231350a626462616566316434653335613237
39353866613062376562336662653862363437626334373464633738383239393732376135616537
6236653232663230650a353564346462653762323836396538643264653531343436363964343637
3835

Ansible recognizes the $ANSIBLE_VAULT;1.1;AES256 magic string and decrypts the file at runtime when you supply the password. The encrypted file is still valid YAML from Ansible's parser perspective. Encryption does not compress: a 10 KB plaintext file becomes roughly 10 KB of base64-encoded ciphertext plus overhead. The official Ansible Vault documentation describes cipher details and password handling.

Encrypting Existing Files Without Leaking Plaintext to Git

If you already committed group_vars/prod/vault.yml in plaintext, every historical commit still contains the secrets. A colleague running git log or a CI pipeline cloning the repository can retrieve old versions. Encryption today does not erase yesterday's mistakes.

Checking Git History for Plaintext Secrets

Search the entire history of a file, including deleted branches and reflog entries:

git log --all --full-history -p -- group_vars/prod/vault.yml

The -p flag shows diffs. If you see db_password: MyS3cret in any commit, that plaintext is in the repository forever unless you rewrite history. You can also use a Git secret scanning tool to automate the search across all files and commits.

Rewriting History with git filter-repo

Remove the file from all commits with git filter-repo, the current best practice for history rewriting. Install it with pip install git-filter-repo. Then run:

git filter-repo --path group_vars/prod/vault.yml --invert-paths

This deletes every version of group_vars/prod/vault.yml from the repository's history. Commit hashes change because Git recalculates them after removing the file. After rewriting, encrypt the file and commit it fresh:

ansible-vault encrypt group_vars/prod/vault.yml
git add group_vars/prod/vault.yml
git commit -m "Add encrypted vault file"

Force-push the rewritten history to the remote:

git push origin main --force

Notify your team immediately. Everyone must delete their local clone and re-clone the repository. Rebasing or pulling will fail because the history diverged. An alternative tool, BFG Repo-Cleaner, also works, but git filter-repo is faster and handles edge cases better.

Verifying the Plaintext Is Gone

Run the log command again to confirm:

git log --all --full-history -p -- group_vars/prod/vault.yml

You should see only the commit adding the encrypted file. No plaintext secrets appear. Check the reflog as well:

git reflog expire --expire=now --all
git gc --prune=now --aggressive

This prunes dangling objects from your local repository. Remote hosting services like GitHub and GitLab cache refs for a period; contact support if you need them purged immediately.

Structuring Vault Files: Inline vs Dedicated vault.yml

You have two patterns for organizing secrets. Encrypt individual variables inline and embed them in a regular YAML file, or keep all secrets in a dedicated vault.yml that you encrypt as a whole. Pick based on how many secrets you manage and how often they change.

Inline Encrypted Variables with !vault Tag

Encrypt a single string and paste the output into your variables file:

ansible-vault encrypt_string 'MyS3cretPassword' --name 'db_password'
New Vault password: 
Confirm New Vault password: 
db_password: !vault |
          $ANSIBLE_VAULT;1.1;AES256
          36363931343766393464616339636632646334613436663937383932613039613039653661626130
          6432643261383733353037653561343334636134383438620a343937643766346264383533373765
          ...
Encryption successful

Copy the db_password: !vault | block into group_vars/prod/vars.yml. The !vault tag tells Ansible to decrypt that value. This works well for a handful of secrets mixed with plaintext configuration, but diffs become noisy when you change a password because the entire ciphertext block changes.

Dedicated vault.yml in group_vars

Create group_vars/prod/vault.yml with all secrets in plaintext:

vault_db_password: MyS3cretPassword
vault_api_key: abc123xyz
vault_ssl_key: |
  -----BEGIN PRIVATE KEY-----
  MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC...
  -----END PRIVATE KEY-----

Encrypt the entire file:

ansible-vault encrypt group_vars/prod/vault.yml

In group_vars/prod/vars.yml, reference the vault variables:

db_password: "{{ vault_db_password }}"
api_key: "{{ vault_api_key }}"

This pattern scales better. When you rotate one password, only vault.yml changes. Diffs on vars.yml stay readable. The directory structure looks like this:

group_vars/
โ””โ”€โ”€ prod/
    โ”œโ”€โ”€ vars.yml      # plaintext, references {{ vault_* }}
    โ””โ”€โ”€ vault.yml     # encrypted, contains vault_* definitions

Ansible automatically loads both files for the prod group. For a complete production setup, see Ansible Automation: From Zero to Production, which covers inventory structure, role organization, and vault integration in CI/CD pipelines.

Managing Vault Passwords: Files, Environment Variables, and CI/CD

You need the vault password every time you run a playbook or edit an encrypted file. Typing it interactively breaks automation. Store the password in a file, environment variable, or CI/CD secret store depending on where the playbook runs.

Using a Vault Password File

Create a file containing only the vault password:

echo 'MyVaultPassword123' > .vault-pass
chmod 600 .vault-pass

Add .vault-pass to .gitignore immediately:

echo '.vault-pass' >> .gitignore
git add .gitignore
git commit -m "Ignore vault password file"

Run playbooks with --vault-password-file:

ansible-playbook site.yml --vault-password-file .vault-pass

The file must contain only the password, no whitespace, no comments. Permissions should be 600 or 400 to prevent other users from reading it. This works for local development and on bastion hosts where you manually place the password file.

Passing the Password via Environment Variable

Set the ANSIBLE_VAULT_PASSWORD_FILE environment variable to the path of your password file:

export ANSIBLE_VAULT_PASSWORD_FILE=.vault-pass
ansible-playbook site.yml

Ansible reads the file automatically. You can also pipe the password from an environment variable directly:

echo "$VAULT_PASS" | ansible-playbook site.yml --vault-password-file /dev/stdin

This pattern fits CI/CD pipelines where the password comes from a secret store. The password never touches disk.

Storing Vault Passwords in CI/CD Secret Stores

In GitLab CI, store the vault password as a masked variable in Settings โ†’ CI/CD โ†’ Variables. Name it VAULT_PASSWORD. Then write it to a temporary file in your .gitlab-ci.yml:

deploy:
  script:
    - echo "$VAULT_PASSWORD" > /tmp/.vault-pass
    - chmod 600 /tmp/.vault-pass
    - ansible-playbook site.yml --vault-password-file /tmp/.vault-pass
    - rm /tmp/.vault-pass

The file exists only during the job and disappears when the runner finishes. For GitHub Actions, store the password in repository secrets and use ${{ secrets.VAULT_PASSWORD }}. Jenkins uses credential bindings with the Credentials plugin. Alternatively, pipe the password directly from the environment variable to /dev/stdin as shown above, which avoids writing to disk entirely.

When you rotate the vault password, use ansible-vault rekey to re-encrypt files with a new password. Update the secret in your CI/CD platform at the same time to keep automation working.

Encrypting and Decrypting at Runtime

Ansible decrypts vault files in memory when you run a playbook. You never work with plaintext on disk during normal operations. For editing, Ansible provides commands that decrypt to your editor and re-encrypt on save.

Running Playbooks with Encrypted Variables

Pass the vault password file when running any playbook that uses encrypted variables:

ansible-playbook site.yml --vault-password-file .vault-pass

Ansible decrypts group_vars/prod/vault.yml and any inline !vault strings before executing tasks. The plaintext never appears in logs or on disk. If you forget the password flag, the playbook fails immediately:

ERROR! Attempting to decrypt but no vault secrets found

For interactive password entry, use --ask-vault-pass:

ansible-playbook site.yml --ask-vault-pass
Vault password: 

This prompts for the password. It works for quick manual runs but breaks automation. Always use --vault-password-file in scripts and CI/CD.

Editing Encrypted Files with ansible-vault edit

Change a password or add a new secret with ansible-vault edit:

ansible-vault edit group_vars/prod/vault.yml --vault-password-file .vault-pass

Ansible decrypts the file into a temporary location, opens your $EDITOR (or vi by default), and re-encrypts when you save and quit. The plaintext temporary file is deleted immediately. If you exit without saving, the encrypted file remains unchanged. This is the safest way to modify secrets because the plaintext never persists on disk under the original filename.

Viewing Encrypted Content Temporarily

Print decrypted content to stdout with ansible-vault view:

ansible-vault view group_vars/prod/vault.yml --vault-password-file .vault-pass

This shows the plaintext without modifying the file. To decrypt and overwrite the file on disk, use ansible-vault decrypt:

ansible-vault decrypt group_vars/prod/vault.yml --vault-password-file .vault-pass
Decryption successful

Warning: decrypt replaces the encrypted file with plaintext. If you commit or push at this point, secrets leak into Git. Always re-encrypt immediately with ansible-vault encrypt group_vars/prod/vault.yml before committing. Use edit or view instead unless you have a specific reason to work with the plaintext file directly.

Using Multiple Vault IDs for Separation

Ansible 2.4 introduced vault IDs to manage separate passwords for different environments or teams in the same repository. Each encrypted file is tagged with a vault ID, and Ansible uses the corresponding password at runtime. This lets you run a single playbook against production and staging without sharing the same encryption key.

Defining Vault IDs for Environments

Assign a vault ID when encrypting a file with --vault-id:

ansible-vault encrypt --vault-id prod@.vault-pass-prod group_vars/prod/vault.yml
Encryption successful

The prod@.vault-pass-prod syntax means "use the vault ID named prod and read the password from .vault-pass-prod". The encrypted file header now includes the ID:

$ANSIBLE_VAULT;1.2;AES256;prod
66386439653034323432343032626435653863643936363966613330656430313735646335376239
...

Encrypt staging files with a different ID and password:

ansible-vault encrypt --vault-id staging@.vault-pass-staging group_vars/staging/vault.yml

The staging ID appears in the header. You now have two password files: .vault-pass-prod and .vault-pass-staging. Add both to .gitignore.

Encrypting with a Specific Vault ID

When encrypting individual strings, specify the vault ID:

ansible-vault encrypt_string 'StagingPassword' --vault-id staging@.vault-pass-staging --name 'db_password'
db_password: !vault |
          $ANSIBLE_VAULT;1.2;AES256;staging
          ...

The staging label in the header tells Ansible which password to use. This works for both full files and inline !vault strings.

Providing Multiple Vault Passwords at Runtime

Run a playbook that touches both environments by passing both vault IDs:

ansible-playbook site.yml --vault-id prod@.vault-pass-prod --vault-id staging@.vault-pass-staging

Ansible decrypts each file with the matching password. If a file has the prod label, it uses .vault-pass-prod; if it has staging, it uses .vault-pass-staging. You can also use --vault-id default@prompt to prompt interactively for a specific ID. For CI/CD, store each password in a separate secret and write both to temporary files before running the playbook.

Rekeying and Rotating Vault Passwords

Change the vault password with ansible-vault rekey. This re-encrypts files in place with a new password. You must coordinate the change with everyone who runs the playbooks: update password files on workstations, bastion hosts, and CI/CD secret stores at the same time.

Changing the Vault Password with ansible-vault rekey

Rekey a single file:

ansible-vault rekey group_vars/prod/vault.yml --vault-password-file .vault-pass-old --new-vault-password-file .vault-pass-new
Rekey successful

The file is decrypted with the old password from .vault-pass-old, then re-encrypted with the new password from .vault-pass-new. The file content does not change; only the encryption key changes. Verify the new password works:

ansible-vault view group_vars/prod/vault.yml --vault-password-file .vault-pass-new

If you see the plaintext secrets, rekeying succeeded. Commit the rekeyed file and update .vault-pass on every machine that runs the playbook. Until you do, anyone using the old password will see decryption errors.

Scripting Bulk Rekeying Across Multiple Files

Rekey all vault files in one pass with a shell loop:

for f in group_vars/*/vault.yml host_vars/*/vault.yml; do
  ansible-vault rekey "$f" --vault-password-file .vault-pass-old --new-vault-password-file .vault-pass-new
done

This recurses through group_vars and host_vars, rekeying every vault.yml. If you use vault IDs, specify the ID in the rekey command:

ansible-vault rekey group_vars/prod/vault.yml --vault-id prod@.vault-pass-old --new-vault-id prod@.vault-pass-new

After rekeying, replace the old password in CI/CD secret stores immediately. In GitLab CI, update the VAULT_PASSWORD masked variable. In GitHub Actions, update the repository secret. If you delay, the next pipeline run fails with Decryption failed. Test the playbook once after updating all passwords to confirm everything decrypts correctly.

Integrating Vault with External Secret Managers (HashiCorp Vault, AWS Secrets Manager)

For larger teams or compliance requirements, store secrets in HashiCorp Vault or AWS Secrets Manager instead of encrypting them in Git. Ansible fetches secrets at runtime via lookup plugins, so you never commit encrypted blobs to the repository. The vault password file becomes a HashiCorp Vault token or an AWS IAM role.

Fetch a secret from HashiCorp Vault with the hashi_vault lookup:

- name: Set database password from HashiCorp Vault
  set_fact:
    db_password: "{{ lookup('community.hashi_vault.hashi_vault', 'secret=secret/data/prod/db:password') }}"

Install the community.hashi_vault collection first:

ansible-galaxy collection install community.hashi_vault

Configure the Vault address and authentication token in environment variables or ansible.cfg. The lookup plugin connects to Vault at runtime and retrieves the secret. This requires Vault to be available during playbook execution; if the service is down, the playbook fails.

For AWS Secrets Manager, use the aws_secret lookup:

- name: Fetch RDS password from AWS Secrets Manager
  set_fact:
    db_password: "{{ lookup('amazon.aws.aws_secret', 'prod/db', region='us-east-1') }}"

The playbook must run with an IAM role or access keys that have secretsmanager:GetSecretValue permission. The official lookup plugin documentation lists all available plugins and their configuration options. This pattern eliminates encrypted files from Git entirely, but it couples your playbooks to the external secret manager's availability and access controls. For more on integrating Ansible with cloud services, see the HashiCorp Vault 1.20 update covering secretless authentication patterns.

Pre-Production Vault Checklist

Before deploying Ansible playbooks with vault-encrypted secrets to production, verify the following:

  • All plaintext secrets removed from Git history with git filter-repo and team re-cloned the repository.
  • Vault password files added to .gitignore and never committed.
  • Permissions on password files set to 600 or 400 to prevent unauthorized reads.
  • CI/CD pipelines configured with vault passwords stored in secret management systems, not hardcoded in scripts.
  • At least two people have access to the vault password via a secure channel (password manager, encrypted email, or secret sharing tool).
  • Vault IDs assigned for each environment if you manage multiple stages in one repository.
  • Rekeying procedure documented and tested: confirm you can rotate the password and update all systems without downtime.
  • Playbook runs successfully in a staging environment with --vault-password-file or --vault-id flags.
  • Logs reviewed to ensure no plaintext secrets leak during task execution (use no_log: true on tasks that handle secrets).
  • Backup of the vault password stored offline in case the primary copy is lost.

For a complete production deployment workflow including vault integration, role structure, and CI/CD pipeline configuration, see Ansible Automation: From Zero to Production. The book covers multi-environment setups, secret rotation strategies, and production hardening steps beyond encryption.

Frequently Asked Questions

Can I encrypt only part of a YAML file with Ansible Vault?

Yes, use ansible-vault encrypt_string to encrypt individual values and embed them in a plaintext YAML file with the !vault tag. This lets you version-control non-sensitive configuration in plaintext while encrypting passwords, API keys, and other secrets inline. The entire file does not need to be encrypted.

What happens if I lose the vault password file?

You cannot decrypt the files. Ansible Vault uses AES256 symmetric encryption with no recovery mechanism. If you lose the password, you must reset all secrets, re-encrypt with a new password, and redeploy. Store the password in at least two secure locations: a password manager and an offline backup. For critical systems, consider using HashiCorp Vault or AWS Secrets Manager as the source of truth instead of Ansible Vault files.

How do I rotate the vault password across multiple repositories?

Rekey each repository separately with ansible-vault rekey, then update the password in all CI/CD secret stores, bastion hosts, and developer workstations. Coordinate the change with the team to avoid decryption failures. For organizations with many repositories, automate rekeying with a script that clones each repo, runs ansible-vault rekey on all vault files, commits, and pushes. Update external secret stores immediately after rekeying.

Is Ansible Vault secure enough for production secrets?

Yes, for most use cases. Ansible Vault uses AES256-CBC encryption, which is strong when paired with a long, random password. The risk is password management: if you store the password in plaintext on disk or commit it to Git, encryption provides no protection. For highly sensitive environments or compliance requirements (PCI-DSS, HIPAA), integrate with HashiCorp Vault or AWS Secrets Manager to avoid storing encrypted secrets in version control entirely.

Can I use different vault passwords for different environments in the same playbook?

Yes, use vault IDs introduced in Ansible 2.4. Encrypt production files with --vault-id prod@.vault-pass-prod and staging files with --vault-id staging@.vault-pass-staging. At runtime, pass both: ansible-playbook site.yml --vault-id prod@.vault-pass-prod --vault-id staging@.vault-pass-staging. Ansible matches the vault ID in each file's header to the correct password. This lets you separate keys by environment, team, or sensitivity level within one repository.

Share this article:
Dorian Thorne
About the Author

Dorian Thorne

Cloud Infrastructure, Cloud Architecture, Infrastructure Automation, Technical Documentation

Dorian Thorne is a cloud infrastructure specialist and technical author focused on the design, deployment, and operation of scalable cloud-based systems.

He has extensive experience working with cloud platforms and modern infrastructure practices, including virtualized environments, cloud networking, identity and acces...

Cloud Computing Cloud Networking Identity and Access Management Infrastructure as Code System Reliability

Stay Updated

Subscribe to our newsletter for the latest tutorials, tips, and exclusive offers.