Use Ansible Vault with group_vars and host_vars Safely

Learn a safe Ansible Vault layout for group_vars and host_vars, including vault IDs, Git-safe structure, secret naming, and runtime patterns that avoid leaking credentials.

Published

Updated

Read time 7 min read

Reviewed byDeepak Prasad

Ansible Vault layout for group_vars and host_vars with environment vault files

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.

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-vault subcommand
  • 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.


A practical production-style inventory layout:

text
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.yml

In the tested lab, the same pattern appears as:

bash
find inventory/prod -maxdepth 4 -type f | sort

Sample output:

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.yml

vars.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:

yaml
web_workers: 4
web_secret_token: "{{ vault_web_secret_token }}"

Secret group variables

group_vars/web/vault.yml for secret values:

yaml
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:

yaml
db_port: 5432

Secret host variables

Use host_vars/<host>/vault.yml when one host needs unique secret material:

yaml
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:

yaml
# vars.yml
db_user: app
db_password: "{{ vault_db_password }}"
yaml
# 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:

  1. keep current readable values in vars.yml
  2. move secret keys into vault.yml
  3. encrypt vault.yml
  4. 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:

bash
ansible-vault encrypt inventory/prod/group_vars/web/vault.yml --vault-id [email protected]
bash
ansible-vault encrypt inventory/prod/group_vars/db/vault.yml --vault-id [email protected]

Host-specific secret file in the tested lab:

bash
ansible-vault encrypt inventory/prod/host_vars/db1/vault.yml --vault-id [email protected]

Sample output:

output
$ANSIBLE_VAULT;1.2;AES256;prod

Labeling 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:

text
project/
├── inventory/.../vault.yml          # encrypted and committed
├── .vault-dev.pass                  # local only
├── .vault-prod.pass                 # local only
└── .gitignore                       # excludes .vault*.pass

Minimal .gitignore snippet:

output
.vault*.pass
*.retry

What to Commit and What Not to Commit

Commit:

  • encrypted vault.yml files
  • 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:

bash
ansible-playbook check-vars.yml --vault-id [email protected]

Sample output:

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:

bash
ansible-playbook check-vars.yml --ask-vault-pass

Verify That Vault Variables Are Loaded

Verify with safe checks, not raw secret output:

yaml
- 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:

yaml
- name: Use secret safely
  ansible.builtin.command: /bin/true
  no_log: true
  when: db_password is defined or web_secret_token is defined

Avoid 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)

For the course lab flow:

  1. Keep reusable defaults in group_vars/all/vars.yml.
  2. Put group secrets in group_vars/<group>/vault.yml encrypted with Vault.
  3. Use host_vars/<host>/vault.yml only for host-specific secret exceptions.
  4. Use one vault password file for single-env labs; add vault IDs when splitting dev/stage/prod.
  5. 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.


Frequently Asked Questions

1. Should I encrypt every file in group_vars and host_vars?

No. Keep non-secret values in readable vars files and encrypt only secret values in separate vault files for each group or host.

2. Where should vaulted group variables go?

A common pattern is inventory/group_vars//vault.yml with readable defaults in vars.yml in the same folder.

3. Can host_vars override group_vars secrets?

Yes. host_vars are more specific for that host, so host-level secret exceptions are a practical fit for host_vars//vault.yml.

4. What should never be committed to Git?

Never commit vault password files, decrypted temporary copies, editor swap files, or logs that print secret values.

5. How do I run a playbook with vaulted files?

Use --ask-vault-pass, --vault-password-file, or --vault-id at runtime so Ansible can decrypt vault.yml files while loading vars.

6. How can I avoid leaking secrets in task output?

Do not debug-print secret values and use no_log: true on tasks that handle credentials, tokens, private keys, or secret payloads.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …