The vars/vault Split
Scenario: Every time anyone needs to check which database user the application connects as, they have to decrypt a vault file - so the whole team has the production vault passphrase on their laptops.
New words, in plain English
vars.yml- readable, reviewable, non-secret settings.vault.yml- the encrypted file, containing only secret values.- Indirection -
vars.ymlreferences avault_-prefixed variable that lives invault.yml. - Least privilege - only the people and systems that need the passphrase have it.
Encrypting a whole variables file works, and it has a cost: everything in it becomes invisible to code review. A pull request that changes a vault file shows two blocks of ciphertext, and no reviewer can tell whether it changed a password or a port number.
The standard answer is the vars/vault split. Make group_vars/<group>/ a directory containing two files. vars.yml holds everything readable and references secrets indirectly. vault.yml holds only the secret values, each named with a vault_ prefix. Ansible loads both and merges them.
The result is that a diff on vars.yml is fully reviewable - you can see that the database user changed - while the secrets stay encrypted. It also makes the naming convention do real work: a vault_-prefixed name is a reliable signal that a value comes from the encrypted file, so grep vault_ finds every secret reference in the repository.
And it enables least privilege. Most engineers can read and review vars.yml without ever needing the passphrase; only deployment automation and a small group actually need the key.
Analogy: A contract with the commercially sensitive figures redacted. Everyone can read the terms, check the parties and review the clauses; only the people who need the numbers hold the unredacted copy.
A worked example
# group_vars/dbservers/vars.yml - READABLE and reviewable
db_host: dbprod.internal
db_user: checkout_app
db_port: 5432
db_password: "{{ vault_db_password }}" # indirection
# group_vars/dbservers/vault.yml - ENCRYPTED, secrets only
vault_db_password: "Pa55w0rd-rotate-2026"
# On disk:
# group_vars/dbservers/
# ├── vars.yml (plain text, in every diff)
# └── vault.yml ($ANSIBLE_VAULT;1.1;AES256...)
# Using it - nothing in the play knows or cares it was encrypted
- ansible.builtin.template:
src: db.conf.j2
dest: /etc/app/db.conf
mode: "0600"
no_log: true
Two habits complete the pattern. First, add a repository check - a pre-commit hook or a CI step - that greps for anything looking like a credential in unencrypted files. It is crude and it catches the mistake that matters: a secret pasted into vars.yml "just for testing".
Second, remember that git remembers. A secret committed in clear text is in the history even after you encrypt the file, and every clone has it. Rotating that credential is not optional cleanup - it is the actual remediation, and rewriting history is at best a supplement to it.
Tip: Prefix every vaulted variable withvault_. It makesgrep -r vault_an accurate inventory of every secret your automation consumes.
Goal: Put this to work in the ansible-vault-secrets lab. Open/labs/ansible, pickansible-vault-secrets, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.