Vault with group_vars and host_vars is the practical sweet spot for most Ansible repos: you keep your normal inventory variable layout, but encrypt only the secret pieces. That lets teams review non-sensitive defaults in Git while protecting credentials and tokens.
This guide focuses on safe structure and runtime usage for vaulted group and host variable files. It does not re-teach every ansible-vault command; for full command walkthroughs (create, edit, view, rekey, encrypt_string), use the main Vault tutorial.
Tested on: Rocky Linux 10.2 (Red Quartz); kernel 6.12.0-211.16.1.el10_2.0.1.x86_64; ansible-core 2.16.16.
NOTE
This chapter is part of the GoLinuxCloud Ansible tutorial (RHCE EX294). Follow along from ~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
~/ansible-project, inventory group lab, and playbooks in playbooks/. Use your own host names and paths if yours differ.
Why Use Vault with group_vars and host_vars?
You already use group_vars and host_vars to keep variables near inventory. Common naming and split-file patterns are in group_vars and host_vars patterns. Vault lets you keep that same model while encrypting only sensitive values.
Benefits:
- no plain-text passwords in repository history
- readable non-secret defaults for easier review
- consistent inventory loading model for playbooks and ad hoc runs
- clearer environment separation with vault IDs (
dev,staging,prod)
Ansible inventory documentation also supports splitting variable data into separate files, which fits Vault usage naturally.
What This Article Covers and What It Does Not Cover
This article covers:
- secret and non-secret file split for groups and hosts
- naming and mapping strategy for secret variables
- vault ID usage for environment separation
- Git-safe commit boundaries and runtime checks
This article does not cover:
- full beginner intro to every
ansible-vaultsubcommand - deep precedence walkthrough beyond “host is more specific than group”
- complete org-wide secrets governance policies
Plain Variables vs Secret Variables
Keep non-secret values readable, and encrypt only secrets.
| Type | Examples | Recommended file |
|---|---|---|
| Plain values | ports, usernames, feature flags, paths | vars.yml |
| Secret values | passwords, tokens, private keys, API secrets | vault.yml (encrypted) |
This split reduces accidental leaks and avoids turning every review diff into unreadable ciphertext.
Recommended Secret Variable Layout
A practical production-style inventory layout:
inventory/
├── prod/
│ ├── hosts
│ ├── group_vars/
│ │ ├── all/
│ │ │ ├── vars.yml
│ │ │ └── vault.yml
│ │ ├── web/
│ │ │ ├── vars.yml
│ │ │ └── vault.yml
│ │ └── db/
│ │ ├── vars.yml
│ │ └── vault.yml
│ └── host_vars/
│ └── db1/
│ ├── vars.yml
│ └── vault.ymlIn the tested lab, the same pattern appears as:
find inventory/prod -maxdepth 4 -type f | sortSample output:
inventory/prod/group_vars/all/vars.yml
inventory/prod/group_vars/db/vars.yml
inventory/prod/group_vars/db/vault.yml
inventory/prod/group_vars/web/vars.yml
inventory/prod/group_vars/web/vault.yml
inventory/prod/hosts
inventory/prod/host_vars/db1/vars.ymlvars.yml stays readable; vault.yml is committed only after encryption.
group_vars Vault Layout
Plain group variables
group_vars/web/vars.yml for non-secret defaults:
web_workers: 4
web_secret_token: "{{ vault_web_secret_token }}"Secret group variables
group_vars/web/vault.yml for secret values:
vault_web_secret_token: !vault |
...Keep secret variable names explicit (vault_*) so reviewers know which values are sensitive.
Environment-specific group secrets
Use separate inventory roots or vault IDs per environment. In a multi-env repo, prod/group_vars/web/vault.yml should not share the same vault secret as dev/group_vars/web/vault.yml.
host_vars Vault Layout
Plain host variables
Use host_vars/<host>/vars.yml for host-specific but non-secret values:
db_port: 5432Secret host variables
Use host_vars/<host>/vault.yml when one host needs unique secret material:
vault_db_admin_password: !vault |
...Host-specific exceptions
Host-level secret files are for real exceptions, not defaults copied everywhere. Group-level vault files should still hold shared secrets for that role.
Naming Secret Variables Safely
Prefix encrypted secret variables with vault_ so reviewers, linters, and ansible-lint rules can spot sensitive keys at a glance. Map them into readable playbook variables in plain vars.yml files:
| Convention | Example | Purpose |
|---|---|---|
| Encrypted secret | vault_db_password in vault.yml |
Holds the !vault ciphertext |
| Plain alias | db_password: "{{ vault_db_password }}" in vars.yml |
Playbooks and templates use the short name |
| Never | db_password: !vault | in a file named vars.yml |
Hides secrets in a filename that looks non-secret |
Use readable names and map secret values through clear keys:
# vars.yml
db_user: app
db_password: "{{ vault_db_password }}"# vault.yml (encrypted)
vault_db_password: !vault |
...This preserves readable playbook logic (db_password) while isolating encrypted content (vault_db_password). Apply the same vault_* prefix to tokens, API keys, and private key material—vault_api_token, vault_tls_key, and so on.
Use Vault Files with Existing group_vars and host_vars
You do not need to redesign inventory to add Vault. Start by splitting existing files:
- keep current readable values in
vars.yml - move secret keys into
vault.yml - encrypt
vault.yml - run playbook with a vault password source
This incremental migration is safer than mass-encrypting every variable file in one pass.
Use Vault IDs for dev, staging and production
Vault IDs let you label encrypted files by environment and pass multiple vault secrets at runtime.
Example encryption:
ansible-vault encrypt inventory/prod/group_vars/web/vault.yml --vault-id [email protected]ansible-vault encrypt inventory/prod/group_vars/db/vault.yml --vault-id [email protected]Host-specific secret file in the tested lab:
ansible-vault encrypt inventory/prod/host_vars/db1/vault.yml --vault-id [email protected]Sample output:
$ANSIBLE_VAULT;1.2;AES256;prodLabeling helps Ansible choose the right secret first. For strict ID matching, set vault_id_match = true in ansible.cfg.
Git-Safe Ansible Vault Structure
Vault is useful only when password material stays outside version control.
Recommended local files:
project/
├── inventory/.../vault.yml # encrypted and committed
├── .vault-dev.pass # local only
├── .vault-prod.pass # local only
└── .gitignore # excludes .vault*.passMinimal .gitignore snippet:
.vault*.pass
*.retryWhat to Commit and What Not to Commit
Commit:
- encrypted
vault.ymlfiles - readable non-secret
vars.yml - playbooks and templates
Do not commit:
- vault password files (
.vault-*.pass) - decrypted temporary files
- debug logs containing secret values
- editor backups/swap files with plain text
Run Playbooks with Vault-Protected Variables
Runtime options are the same as other Vault workflows:
ansible-playbook check-vars.yml --vault-id [email protected]Sample output:
PLAY [all] *********************************************************************
TASK [Show non-secret checks only] *********************************************
ok: [db1] => {
"msg": {
"env": "production",
"has_db_password": true,
"has_web_token": false,
"host": "db1"
}
}
ok: [web1] => {
"msg": {
"env": "production",
"has_db_password": false,
"has_web_token": true,
"host": "web1"
}
}For manual runs you can also use:
ansible-playbook check-vars.yml --ask-vault-passVerify That Vault Variables Are Loaded
Verify with safe checks, not raw secret output:
- name: Show non-secret checks only
ansible.builtin.debug:
msg:
host: "{{ inventory_hostname }}"
env: "{{ app_env }}"
has_db_password: "{{ db_password is defined }}"
has_web_token: "{{ web_secret_token is defined }}"This confirms variable availability without printing the secret itself.
Avoid Leaking Secrets in Output
Vault protects secrets at rest. Once decrypted during a run, tasks can still leak values via debug output, failed command text, or CI logs.
Use no_log: true on sensitive tasks:
- name: Use secret safely
ansible.builtin.command: /bin/true
no_log: true
when: db_password is defined or web_secret_token is definedAvoid debug: var=db_password in shared logs.
Common Mistakes with Vault, group_vars and host_vars
| Mistake | Symptom | Fix |
|---|---|---|
Keep secrets in plain vars.yml |
Secrets visible in Git | Move to encrypted vault.yml |
| Wrong host alias directory | Host vars not loaded | Match host_vars/<inventory_hostname>/ exactly |
| Commit password files | Vault compromised | Keep .vault*.pass outside Git |
| Print secrets in debug output | Secret in logs/CI | Use safe booleans + no_log: true |
| Overuse host-level secrets | Duplicated secret sprawl | Keep shared secrets in group-level vault files |
| Mix vault IDs without naming discipline | Wrong secret source at runtime | Use clear env labels (dev, staging, prod) |
Recommended Structure for This Course
For the course lab flow:
- Keep reusable defaults in
group_vars/all/vars.yml. - Put group secrets in
group_vars/<group>/vault.ymlencrypted with Vault. - Use
host_vars/<host>/vault.ymlonly for host-specific secret exceptions. - Use one vault password file for single-env labs; add vault IDs when splitting dev/stage/prod.
- Validate loaded variables with safe checks, then protect sensitive tasks with
no_log: true.
This keeps layouts predictable while preventing accidental secret exposure.
Summary
Using Ansible Vault with group_vars and host_vars lets you keep the familiar variable layout while encrypting only what is sensitive. Split readable and secret values into separate files, encrypt vault.yml, keep password files out of Git, and run playbooks with vault password options or vault IDs. Verify secret loading with safe boolean checks, not raw debug prints, and use no_log: true anywhere decrypted secrets pass through task execution.

