Ansible Vault
Here I’ll setup Ansible Vault which will include multiple vault IDs, encrypted variable files, the plaintext/vault naming pattern, and a pre-commit framework to catch secrets before they reach Git.
What I Will Be Completing In This Part
- Design a multi-vault ID strategy separating network credentials from service credentials
- Create the
.vault/directory with per-ID password files - Configure
ansible.cfgto reference vault password files automatically - Create encrypted variable files using the plaintext/vault naming pattern
- Install the
pre-commitframework and configure hooks to block plaintext secrets - Verify the entire secrets workflow end to end
Ansible Vault is Ansible’s built-in mechanism for encrypting sensitve data so they can be stored in Git without exposing them in plaintext. The encrypted files are committed alongside playbooks and roles, which means the entire project is version controlled. The secrets are only readable by someone who has the vault password.
If I were to not use Vault, I would need to keep credentials out of the repo entirely which breaks reproducibility.
01 Vault ID
I set up multiple vault IDs instead of just using a single vault password. Vault IDs let me use different encryption passwords for different categories of secrets.
For this project I defined 3 vault IDs:
- network
- Device credentials: SSH passwords, enable secrets, SNMP strings
- services
- Infrastructure service credentials: DB passwords, API tokens, admin accounts
- pki
- Certificates and private keys
Further explanation. The network vault holds credentials that Ansible uses to connect to and configure network devices. The services vault holds credentials for infrastucture platforms (e.g. Gitea, Netbox, Graylog). The pki vault will hold TLS certificates and private keys when step-ca is deployed.
02 Directory Structure
I created a .vault/ directory in the project root tohold the vault password files.
| |
- Line 3:
- Set permissions to
700that way only the owner can read, write, or list the contents of this directory.
02 Password Files
I created one password file per vault ID. Each file contains a single line which is the vault password for that ID.
| |
Then locked down permissions:
| |
Each file should report exactly 1 line:
(.venv) $ wc -l .vault/*.pass03 Configuring Ansible.CFG
Next, I updated the ansible.cfg file to tell Ansible where to find the vault password files for each vault ID. This eliminates having to type --vault-id flags.
I added the following under the [defaults] section:
vault_identity_list = [email protected]/network.pass, [email protected]/services.pass, [email protected]/pki.passThe [defaults] section afterwards:
[defaults]
inventory = inventory/
roles_path = roles/
collections_path = ~/.ansible/collections
remote_user = admin
timeout = 30
forks = 10
host_key_checking = False
retry_files_enabled = False
stdout_callback = yaml
callbacks_enabled = ansible.posix.timer
vault_identity_list = [email protected]/network.pass, [email protected]/services.pass, [email protected]/pki.pass
[persistent_connection]
connect_timeout = 60
command_timeout = 60The file is committed to Git and containers the paths to the vault password files, but not the passwords themselves.
04 Encrypted Variables
Next, I created encrypted variable files for my network device credentials. These files will be added to the inventory/group_vars/ directory.
I then created the encrypted vault file for the ios group:
(.venv) $ ansible-vault create --encrypt-vault-id network inventory/group_vars/ios/vault.yml- create:
- Opens a new file in the default editor and encrypts the contents on save.
- –encrypt-vault-id network:
- Specifies which vault ID to use for encrypting the file.
Running this command will open my default editor, which is nano.
---
vault_ansible_user: admin
vault_ansible_password: superpassword2026
vault_ansible_become_password: supersecretpassword2026After saving the file will be fully encrypted.
(.venv) $ cat inventory/group_vars/ios/vault.ymlANSIBLE_VAULT;1.2;AES256;network
33363237326239373033313861363034373166333039626436646432613835383733613738313930
6232363866663332333866396638613136383337626138650a373431303830333164393835396534I did the same for NX-OS and PAN-OS groups:
(.venv) $ mkdir -p inventory/group_vars/nxos
(.venv) $ ansible-vault create --encrypt-vault-id network inventory/group_vars/nxos/vault.yml
(.venv) $ mkdir -p inventory/group_vars/panos
(.venv) $ ansible-vault create --encrypt-vault-id network inventory/group_vars/panos/vault.ymlThen created a services vault file for infrastructure credentials:
(.venv) $ mkdir -p inventory/group_vars/all
(.venv) $ ansible-vault create --encrypt-vault-id network inventory/group_vars/all/vault.ymlThe all group is a built-in Ansible group that includes every host in the inventory.
05 Plaintext/Vault Variable Pattern
This is the most important convention in the entire secrets management setup. I used the Ansible best practice of separating vault encrypted variables from the plaintext variables that reference them.
In each group’s directory there are 2 files.
The encrypted vault.yml defines variables with a vault_ prefix:
---
vault_ansible_user: admin
vault_ansible_password: superpassword123!
vault_ansible_become_password: supersecretpassword123!The plaintext vars.yml references those vault variables using Jinja2:
| |
- Line2 2-4:
- The connection variables are set to Jinja2 expressions that pull their values from the vault encrypted variables.
- Lines 5-8:
- Non-sensitive connection settings live in the plaintext file alongside the vault references.
I created the same for NX-OS and PAN-OS groups.
---
ansible_user: "{{ vault_ansible_user }}"
ansible_password: "{{ vault_ansible_password }}"
ansible_become_password: "{{ vault_ansible_become_password }}"
ansible_connection: ansible.netcommon.network_cli
ansible_network_os: cisco.nxos.nxos
ansible_become: true
ansible_become_method: enable---
ansible_user: "{{ vault_ansible_user }}"
ansible_password: "{{ vault_ansible_password }}"
ansible_connection: local
ansible_network_os: paloaltonetworks.panos.panos- Line 4:
- PAN-OS uses
localconnection and notnetwork_cli. The PAN-OS Ansible collection communicates with the firewall over its XML API (HTTPS).
06 Encrypt, Decrypt, and Edit
These are useful commands that I use daily while working with vault encrypted files.
Editing an existing vault file:
(.venv) $ ansible-vault edit inventory/group_vars/ios/vault.ymlThis decrypts the file, opens it in the editor, and re-encrypts it on save.
Viewing contents without editing:
(.venv) $ ansible-vault view inventory/group_vars/ios/vault.ymlThis prints the decrypted contents to stdout without opening an editor.
Encrypting an existing plaintext file:
(.venv) $ ansible-vault encrypt --encrypt-vault-id network somefile.ymlThis encrypts a file in place. The original plaintext is overwritten with the encrypted version.
Encrypting a single string:
(.venv) $ ansible-vault encrypt_string --encrypt-vault-id services 'MySecretToken' --name 'vault_netbox_api_token'07 Pre-Commit
I then installed the pre-commit framework and configured hooks that prevent plaintext secrets from being committed to Git. This runs checks automatically before every git commit. If the check fails then the commit blocked and an error message appears.
pre-commit supports hundreds of community maintained hooks. I’ll extend its usage by using it with YAML lint and Ansible syntax validation.
(.venv) $ pip install pre-commit
(.venv) $ pip freeze > requirements.txt- Line 1:
- Installed
pre-commit. - Line 2:
- Updated
requirements.txtto includepre-commit.
Then I created the .pre-commit-config.yml configuration file:
| |
- Lines 4-14:
- The
pre-commit-hooksrepo provides common checks. - Lines 17-24:
- A custom local hook that checks every staged file named
vault.yml. It reads the first line and verifies it starts with$ANSIBLE_VAULT. If the header is missing the file is plaintext and the commit is blocked. - Lines 26:32
- A second custom hook that scans all staged YAML files (ecluding
vault.ymlfiles and the pre-commit config itself) for patterns that look like plaintext secrets.
I then installed the hooks into the local Git repo:
(.venv) $ pre-commit installpre-commit installed at .git/hooks/pre-commitpre-commit install modifies the .git/hooks/pre-commit file, which is local to this clone and not tracked by Git. This means the hooks need to be reinstalled after cloning the repo on a new machine. Running pre-commit install should be part of the setup steps whenever the repo is cloned fresh.I ran the hooks against all existing files to make sure nothing in the current project triggers a failure:
(.venv) $ pre-commit run --all-filesTrim Trailing Whitespace.............................Passed
Fix End of Files.....................................Passed
Check Yaml...........................................Passed
Check for added large files..........................Passed
Detect Private Key...................................Passed
Don't commit to branch...............................Passed
Check for unencrypted vault files....................Passed
Check for plaintext password patterns................Passed08 Testing Hooks
I tested the hooks just to see what errors I would get and familiarlize myself with them.
1. Unencrypted vault file
I created a plaintext file named vault.yml to simulate a decrypted vault file being staged:
(.venv) $ echo "vault_test_password: plaintext_oops" > /tmp/test-vault.yml
(.venv) $ cp /tmp/test-vault.yml inventory/group_vars/ios/vault.yml
(.venv) $ git add inventory/group_vars/ios/vault.yml
(.venv) $ git commit -m "test: should be blocked"Check for unencrypted vault files....................Failed
- hook id: check-ansible-vault
- exit code: 1
ERROR: inventory/group_vars/ios/vault.yml is not encrypted!The commit was blocked. I restored the encrypted file:
(.venv) $ git checkout -- inventory/group_vars/ios.vault.yml2. Plaintext password in a YAML file
(.venv) $ echo "snmp_community: public123" > test-secret.yml
(.venv) $ git add test-secret.yml
(.venv) $ git commit -m "test: should be blocked"Check for plaintext password patterns................Failed
- hook id: check-no-plaintext-passwords
- exit code: 1
ERROR: Possible plaintext secret detected!Blocked again. I cleaned up:
(.venv) $ git reset HEAD test-secret.yml
(.venv) $ rm test-secret.yml3. Direct commit to main
(.venv) $ git checkout main
(.venv) $ echo "test" >> README.md
(.venv) $ git add README.md
(.venv) $ git commit -m "test: should be blocked"Don't commit to branch...............................Failed
- hook id: no-commit-to-branch
- exit code: 1I then reset.
(.venv) $ git checkout -- README.md09 Commit & Push
After testing everything I committed the vault setup and pre-commit configurationg using the feature branch workflow.
(.venv) $ git checkout -b feat/vault-and-precommit
(.venv) $ git add -A
(.venv) $ git statusThen reviewed the status output to see if anything needed to be corrected:
new file: .pre-commit-config.yaml
modified: ansible.cfg
modified: requirements.txt
new file: inventory/group_vars/all/vault.yml
new file: inventory/group_vars/ios/vars.yml
new file: inventory/group_vars/ios/vault.yml
new file: inventory/group_vars/nxos/vars.yml
new file: inventory/group_vars/nxos/vault.yml
new file: inventory/group_vars/panos/vars.yml
new file: inventory/group_vars/panos/vault.ymlThen committed and pushed"
(.venv) $ git commit -m "feat: add ansible vault with multi-ID and pre-commit hooks"
(.venv) $ git push -u origin feat/vault-and-precommitThen went to Gitea to review the Pull Request and approve it then merged it with main.
After merging:
(.venv) $ git checkout main
(.venv) $ git pull origin mainSecrets are now a solved problem for this project.