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

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 with vault_. It makes grep -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, pick ansible-vault-secrets, and fix the real broken project - Ansible really does SSH into four managed hosts and converge them.