Ansible Vault vs HashiCorp Vault: which secret goes where
One encrypts files in your repo, the other serves secrets at runtime. Most teams need both — here is the split that works.
Two tools share a word and solve different problems. Ansible Vault encrypts a file so it can sit in Git next to the playbook that uses it; the playbook decrypts it in memory at run time with a passphrase. HashiCorp Vault is a server that authenticates a caller, hands out a secret (often one it just created, with a lease), and writes an audit line. Choosing between them is a question about the secret, not about the tools: is it small, static and needed before anything else exists, or should it rotate, expire and be traceable to whoever fetched it?
Which secret goes where
Decision table
| Secret | Where | Why |
|---|---|---|
| Registry pull password for the first image | Ansible Vault | Needed before the host can reach anything else; changes rarely |
| Initial token that enrols a host with Vault | Ansible Vault | Bootstraps the runtime path; short-lived by design |
| TLS key for an internal, one-time install | Ansible Vault | Static, small, tied to this play |
| Database credentials for a migration | HashiCorp Vault | Leased per run and revoked after; never in Git |
| Cloud API keys used by a role | HashiCorp Vault | Rotate on a schedule, scoped per environment |
| Certificates from an internal PKI | HashiCorp Vault | Issued on demand with short TTLs |
| The Vault passphrase itself | CI secret store / password manager | It must not live next to what it protects |
Most teams end up with both, and that is the intended shape rather than a compromise: Ansible Vault carries the handful of secrets that have to exist before the runtime path works, and HashiCorp Vault carries everything that should not be a file. The failure mode to avoid is the middle ground, where a production database password lives in an encrypted vars file for three years because moving it felt like a project.
Ansible Vault: files and variables that travel with the play
ansible-vault encrypt turns a whole vars file into ciphertext with an $ANSIBLE_VAULT;1.1;AES256 header; ansible-vault encrypt_string encrypts a single value so a group_vars file can stay mostly readable. Vault IDs label which passphrase encrypted what, so prod and staging files can use different passphrases in the same repository, and the label is recorded in the header ($ANSIBLE_VAULT;1.2;AES256;prod).
ansible-vault encrypt --vault-id prod@prompt group_vars/prod/secrets.ymlNew vault password (prod): ********Encryption successfulhead -2 group_vars/prod/secrets.yml$ANSIBLE_VAULT;1.2;AES256;prod39396264613966… ciphertext, safe to commitansible-vault encrypt_string --vault-id prod@prompt --name registry_password 's3cr3t'registry_password: !vault | $ANSIBLE_VAULT;1.2;AES256;prod# group_vars/prod/secrets.yml is vault-encrypted; nothing here changes- hosts: prodtasks:- name: write app configansible.builtin.template:src: app.conf.j2 # references {{ db_password }}dest: /etc/app.confmode: "0640" # decrypted only in memory, at run time
At run time the passphrase comes from a prompt, a file, or a script. The script form is the one that fits CI: --vault-id prod@./vault-pass-client.py runs the script and reads the passphrase from its stdout, so the passphrase can come from the CI platform's secret store or from HashiCorp Vault itself without ever being written to the runner's disk. A plain --vault-password-file works too, provided the file has mode 0600, is created by the job and removed by it, and is listed in .gitignore before the first commit.
#!/usr/bin/env python3# A vault password client: Ansible calls it with --vault-id <label># and reads the passphrase from stdout. The value never touches disk.import os, syslabel = sys.argv[sys.argv.index("--vault-id") + 1] if "--vault-id" in sys.argv else "default"print(os.environ[f"ANSIBLE_VAULT_PASS_{label.upper()}"])
# CI variable ANSIBLE_VAULT_PASS_PROD comes from the platform's masked secret store.# The script name must end in -client or -client.<ext> and be executable.chmod +x vault-pass-client.pyansible-playbook playbook.yml --vault-id prod@./vault-pass-client.py
HashiCorp Vault: look the secret up when the play runs
For anything that should rotate, differ per environment or be revoked centrally, the playbook fetches the value from HashiCorp Vault at run time and stores nothing. The community.hashi_vault.vault_read lookup returns the raw read response, so for a dynamic database credential from database/creds/app the password is under .data.password and the lease id is right next to it. The older hashi_vault lookup still exists but the collection documents a migration away from it; new playbooks should use vault_read, or vault_kv2_get for KV v2 paths.
- hosts: prodvars:ansible_hashi_vault_url: https://vault.internal:8200ansible_hashi_vault_auth_method: approle # role_id/secret_id from the CI secret storetasks:- name: lease one database credential for this playansible.builtin.set_fact:db_lease: "{{ lookup('community.hashi_vault.vault_read', 'database/creds/app') }}"no_log: true- name: run migration with that credentialansible.builtin.command: ./migrate.shenvironment:DB_USER: "{{ db_lease.data.username }}"DB_PASS: "{{ db_lease.data.password }}"no_log: true
Three details make this safe rather than merely dynamic. The lookup runs inside set_fact, once: a lookup placed under vars: is evaluated lazily on every reference, and for a dynamic secret that means db_lease.data.username and db_lease.data.password would come from two different leases. no_log: true keeps the credential out of the play output and the CI log. And the authentication is a method with an identity behind it, approle here or jwt when the CI platform can present an OIDC token, so the Vault audit log shows which pipeline read database/creds/app and the lease can be revoked without touching the playbook.
The remaining decision is who owns the passphrases and the Vault policies once the split exists. The Secrets management foundations course covers that ownership model, at rest, in transit and at run time, and the Ansible course covers the vault ID and lookup mechanics in the context of a full inventory.