Ansible Vault
Security
NetApp ONTAP
Field Guide

Encrypting Ansible Variables with Ansible Vault: A Real Walkthrough, Including the First Error You Will Hit

13 min read

There is a moment in every engineer’s first week with Ansible when the tooling stops being theoretical: you encrypt your variables file, run the playbook the way you always have, and Ansible answers with ERROR! Attempting to decrypt but no vault secrets found. It reads like something broke. Nothing broke. That error is Ansible Vault doing precisely its job — and the engineers who understand why it appears handle secrets correctly for the rest of their careers.

This walkthrough is taken from a real session on a CentOS control node in a NetApp ONTAP automation lab: a variables file holding two clusters’ worth of credentials, the encryption, the error, the fix, and the day-2 commands that keep plaintext off disk permanently. It assumes the setup from our Ansible installation guide and pairs with the ONTAP playbooks guide, where Vault protects every playbook’s credentials.

What this guide covers

Encrypting an Ansible variables file with ansible-vault encrypt, proving the encryption took, understanding and fixing the no vault secrets found error, the view/edit/rekey lifecycle that never leaves plaintext on disk, the ansible.cfg setup that removes the password prompt, and the honest limits of what Vault protects.

Audience: engineers securing their first automation credentials. Examples use a NetApp lab environment; the pattern applies to any Ansible estate.

The problem Vault solves, in one sentence

Your playbooks belong in Git — that is where review, history, and rollback come from — but your passwords must never be in Git, and a variables file is how both statements stay true at once: playbooks reference "{{ PRI_CLU_PASS }}" in the clear, the file defining it is encrypted with AES-256, and the decryption key arrives only at runtime. Ansible Vault is the encryption half of that bargain — a subcommand suite (ansible-vault encrypt / view / edit / rekey) that turns YAML files into ciphertext Ansible can transparently decrypt in memory during a run.

The variables file we are protecting

The file in this session, global.vars, is the environment model for a two-cluster NetApp lab — the single place where every site-specific fact lives so the playbooks themselves never change between environments. Here it is in full, because the inventory of what needs protecting is the point:

$ cat global.vars
{
        "PRI_CLU":              "cluster1.demo.netapp.com",
        "PRI_CLU_USER":         "admin",
        "PRI_CLU_PASS":         "Netapp1!",
        "PRI_CLU_NODE1":        "cluster1-01",
        "PRI_CLU_NODE2":        "cluster1-02",
        "PRI_MGMT_PORT":        "e0c",
        "PRI_DATA_PORT":        "e0d",
        "PRI_SVM":              "san_svm",
        "PRI_SVM2":             "svm_san",
        "PRI_SVM_IP":           "192.168.0.200",
        "PRI_SVM_NETMASK":      "255.255.255.0",
        "PRI_SVM_CIFS_IP":      "192.168.0.201",
        "PRI_SVM_CIFS_NETMASK": "255.255.255.0",
        "PRI_SVM_NFS_IP":       "192.168.0.202",
        "PRI_SVM_NFS_NETMASK":  "255.255.255.0",
        "PRI_CLU_IC1_IP":       "192.168.0.121",
        "PRI_CLU_IC2_IP":       "192.168.0.122",
        "PRI_CLU_IC_NETMASK":   "255.255.255.0",
        "PRI_CLU_DEFAULT_GW":   "192.168.0.1",
        "PRI_AGGR":             "aggr1_cluster1_01_data",
        "PERF_AGGR":            "aggr1_cluster1_01_data",
        "PRI_AGGR_02":          "aggr1_cluster1_02_data",
        "PRI_DOMAIN":           "demo.netapp.com",
        "PRI_DNS1":             "192.168.0.253",
        "PRI_DNS2":             "",
        "PRI_AD_DOMAIN":        "demo.netapp.com",
        "PRI_AD_USER":          "Administrator@demo.netapp.com",
        "PRI_AD_PASS":          "Netapp1!",
        "VOL_SIZE":             "20",
        "SEC_CLU":              "cluster2.demo.netapp.com",
        "SEC_CLU_USER":         "admin",
        "SEC_CLU_PASS":         "Netapp1!",
        "SEC_CLU_NODE1":        "cluster2-01",
        "SEC_CLU_NODE2":        "",
        "SEC_MGMT_PORT":        "e0c",
        "SEC_DATA_PORT":        "e0d",
        "SEC_SVM":              "sec_svm_01",
        "SEC_SVM_IP":           "192.168.0.210",
        "SEC_SVM_NETMASK":      "255.255.255.0",
        "SEC_AGGR":             "aggr1_cluster2_01_data",
        "SEC_DOMAIN":           "demo.netapp.com",
        "SEC_DNS1":             "192.168.0.253",
        "SEC_DNS2":             "",
        "SEC_AD_DOMAIN":        "demo.netapp.com",
        "SEC_AD_USER":          "Administrator@demo.netapp.com",
        "SEC_AD_PASS":          "Netapp1!",
        "SEC_SVM_CIFS_IP":      "192.168.0.211",
        "SEC_SVM_CIFS_NETMASK": "255.255.255.0",
        "SEC_SVM_NFS_IP":       "192.168.0.212",
        "SEC_SVM_NFS_NETMASK":  "255.255.255.0",
        "SEC_CLU_IC1_IP":       "192.168.0.123",
        "SEC_CLU_IC2_IP":       "",
        "SEC_CLU_IC_NETMASK":   "255.255.255.0",
        "SEC_CLU_DEFAULT_GW":   "192.168.0.1",
        "VOL_NAME": "san_vol",
        "WIN_IQN": "iqn.1991-05.com.microsoft:jumphost.demo.netapp.com",
        "LUN_NAME": "lun1",
        "IGROUP_NAME": "igroup1",
        "PRI_ISCSI_IP": "192.168.0.241",
        "SEC_ISCSI_IP": "192.168.0.242",
        "LUN_SIZE": "5",
        "igroups": "igroup1",
        "luns": "lun1",
        "vol_name": "san_vol"
}

Four things to notice before encrypting. First, the inventory of secrets is bigger than a skim suggests: this one file holds admin passwords for two clusters plus the Active Directory join account for both — anyone who reads it owns the storage estate and has a foothold in the domain; the netmasks and port names around them are harmless, but the file encrypts as a unit. Second, the JSON-style formatting works because Ansible parses vars_files as YAML, and YAML accepts quoted-key flow mappings — keep quoting consistent, because a value like Administrator@demo.netapp.com left unquoted parses fine as YAML while breaking any strict JSON tool a colleague later points at the file. Third, the duplicate keys in different cases at the bottom (VOL_NAME and vol_name, IGROUP_NAME and igroups) are deliberate: Ansible variables are case-sensitive, and the lowercase names match the variable interface of NetApp’s prebuilt Galaxy roles while the uppercase ones feed the workshop’s own playbooks — one file serving two naming conventions. Fourth, if your repository splits variables across several files, inventory every file holding a secret before you start — encrypting one and forgetting its sibling protects nothing, and ansible-vault encrypt happily takes multiple filenames in one command. Lab passwords like these are published in every workshop guide; encrypting them is practice for the day the file holds real ones, which is exactly what practice is for.

Sidebar: that first line, #!/usr/bin/env ansible-playbook

The lab’s playbooks open with a shebang, which deserves thirty seconds because it confuses everyone once. #! is the Unix convention telling the kernel which interpreter runs a file when you execute it directly; /usr/bin/env ansible-playbook means “find ansible-playbook on this machine’s PATH” — portable across pipx, pip, and yum installs, whose binary locations all differ. The effect after a one-time chmod +x:

# both forms run the same playbook; the shebang enables the second
ansible-playbook 21_create_pri_svm.yml
./21_create_pri_svm.yml

To YAML the line is just a comment, so it never affects parsing, and every flag you are about to learn passes through the ./ form unchanged. Teams typically keep the explicit form in CI (execute bits do not always survive checkouts) and enjoy the short form on jump hosts.

Step 1 — encrypt the file

One command, two prompts, and the plaintext era of this file is over:

ansible-vault encrypt global.vars

# prove it took - the first line of the file is now a vault header
head -1 global.vars

# repos that split variables across several files: one command covers them all
# ansible-vault encrypt global.vars other_env.vars
[root@centos1 ansible-workshop]# ansible-vault encrypt global.vars
New Vault password:
Confirm New Vault password:
Encryption successful

[root@centos1 ansible-workshop]# head -1 global.vars
$ANSIBLE_VAULT;1.1;AES256

[root@centos1 ansible-workshop]# cat global.vars
$ANSIBLE_VAULT;1.1;AES256
6638643965323633646262656665306333616466396630323136393465356136
3964363833313662643162653630353037633634383265653730363231343336
...

The vault password you typed at those prompts is a new secret you just created — it is not the cluster password, it is the key that unlocks the file, and it now needs a home (a password manager entry, or your CI system’s secret store). What Git, backups, and anyone who copies the repository see from this moment on is the ciphertext: the $ANSIBLE_VAULT;1.1;AES256 header followed by hex. Even git diff reveals nothing but new ciphertext when values change. Critically, the playbook needs zero editsvars_files: - global.vars and every "{{ PRI_CLU_PASS }}" reference stay exactly as they were.

Step 2 — hit the error (everyone does)

The playbook under test is the lab’s volume–qtree–share trio, named for what it does: create_vol_qtree_share.yml. It is worth seeing in full, because it demonstrates the point of the whole exercise — every credential is a variable reference, the anchored &input connection block is reused by all three tasks via the <<: *input merge key, and nothing in this file changed when the vars file was encrypted:

#!/usr/bin/env ansible-playbook
- hosts: localhost
  gather_facts: false
  vars:
    input: &input
      hostname:       "{{ PRI_CLU }}"
      username:       "{{ PRI_CLU_USER }}"
      password:       "{{ PRI_CLU_PASS }}"   # still just a variable reference -
                                             # Ansible decrypts the file in memory
                                             # and this resolves like any other var
      https:          true
      validate_certs: false
      use_rest:       Always
  vars_files:
    - global.vars                        # now AES-256 ciphertext on disk -
                                             # same line, no change needed
  collections:
    - netapp.ontap
  tasks:
  - name: Create volume
    na_ontap_volume:
      name: "{{ PRI_SVM }}_cifs_01"
      state:                 present
      aggregate_name: "{{ PRI_AGGR }}"
      size: "{{ VOL_SIZE }}"
      size_unit: mb
      vserver: "{{ PRI_SVM }}"
      junction_path: "/{{ PRI_SVM }}_cifs_01"
      volume_security_style: ntfs
      policy: default
      <<: *input
  - name : Create Qtree
    na_ontap_qtree:
      state: present
      name: "cifs_01"
      flexvol_name: "{{ PRI_SVM }}_cifs_01"
      vserver: "{{ PRI_SVM }}"
      security_style: ntfs
      <<: *input
  - name : Create share
    na_ontap_cifs:
      state: present
      name: "share_01"
      vserver: "{{ PRI_SVM }}"
      path: "/{{ PRI_SVM }}_cifs_01"
      <<: *input

Now run it the way muscle memory says to — here with --check, previewing changes without making them:

[root@centos1 ansible-workshop]# ansible-playbook create_vol_qtree_share.yml --check
ERROR! Attempting to decrypt but no vault secrets found
[root@centos1 ansible-workshop]#

Read the message precisely, because it says less than panic hears. It does not say the vault is corrupt, the password is wrong, or the file is damaged. It says: this run was handed zero vault passwords to try. Ansible loaded vars_files, met the $ANSIBLE_VAULT header, had no key to attempt, and stopped before touching anything — which is the entire security model working. The instinct this error must never trigger is ansible-vault decrypt “to get unblocked”: that re-writes the plaintext to disk and undoes the exercise. The file is fine. The command was incomplete.

Step 3 — run with the vault flag

ansible-playbook create_vol_qtree_share.yml --check --ask-vault-pass
[root@centos1 ansible-workshop]# ansible-playbook create_vol_qtree_share.yml --check --ask-vault-pass
Vault password:

PLAY [Create volume, qtree, and share] *****************************************

TASK [Create volume] ***********************************************************
changed: [localhost]

TASK [Create Qtree] ************************************************************
changed: [localhost]

TASK [Create share] ************************************************************
changed: [localhost]

PLAY RECAP *********************************************************************
localhost    : ok=3    changed=3    unreachable=0    failed=0    skipped=0

One flag, one prompt, and the run proceeds exactly as it did before encryption — decryption happens in memory, the variables resolve, and nothing decrypted is written back to disk. Note that --check and --ask-vault-pass compose naturally: a vaulted dress rehearsal is the right first run after any change. And keep the two similarly shaped flags straight, because a playbook can legitimately need both: --ask-vault-pass decrypts your files on the control node; --ask-become-pass is sudo on managed nodes. Same shape, different doors.

Figure 01 · The complete lifecycle — where the password is plaintext, and where it never is

1. Plaintextglobal.vars“PRI_CLU_PASS”: “…”exists only at creation— minutes, not monthsencrypt2. Ciphertext$ANSIBLE_VAULT;1.1;AES25666386439653236336462…what disk, Git, andbackups hold forever–ask-vault-pass3. Memory onlyciphertext + vault password→ decrypt during the runnever written backto disk4. Used, then gonevars feed the module,HTTPS to the cluster,discarded at exitthe one secret left to protect: the vault password itself — password manager or CI secret store
Plaintext exists for minutes at creation; ciphertext is the permanent at-rest state; decryption is a runtime, in-memory event. Click to enlarge.

Day-2 operations: view, edit, rekey — never decrypt

Everything you will routinely need, none of which leaves plaintext on disk:

# read the values without decrypting the file
ansible-vault view global.vars

# change values: opens decrypted in $EDITOR, re-encrypts on save
ansible-vault edit global.vars

# change the vault password itself (e.g. after a team departure)
ansible-vault rekey global.vars

The subcommand to treat as radioactive is ansible-vault decrypt — it has exactly one legitimate use (permanently un-vaulting a file that no longer needs protection) and one common misuse (working around the no vault secrets found error, which re-exposes every secret the encryption existed to protect). If you find yourself typing decrypt to make an error go away, the answer was a flag, not a key ceremony. For any value change or consistency cleanup, edit is the tool: the change happens inside the vault, and the file never exists decrypted on disk.

Removing the prompt: ansible.cfg and a password file

Typing the vault password every run is correct for production change windows and tedious for a lab. The standing configuration — three commands, run once in the project directory:

cat > ansible.cfg <<'EOF'
[defaults]
vault_password_file = /root/.vault_pass
EOF

echo 'YourVaultPassword' > /root/.vault_pass
chmod 600 /root/.vault_pass

After this, plain ansible-playbook create_vol_qtree_share.yml --check works with no flag — Ansible finds ansible.cfg in the current directory and reads the password file automatically (the ANSIBLE_VAULT_PASSWORD_FILE environment variable does the same per-shell). The honest accounting: you have moved the secret from prompt to file, so the file’s protection is now the control — chmod 600, owned by the automation user, never committed to Git, and in CI written at job start from the pipeline’s secret store rather than living on the runner. For estates with multiple vaults, the newer --vault-id label@source syntax labels which password unlocks which files; file that away for the day you meet it in someone else’s repository.

What Vault does not solve

Vault relocates the secret problem; it does not eliminate it. You traded “credentials readable in every clone of the repository” for “one vault password to protect” — an excellent trade, but that password still needs a managed home, and three residual exposures deserve names. Verbose logging: a task that passes credentials as module parameters can echo them into logs under -vvv; add no_log: true to such tasks before any CI pipeline runs them. Memory during the run: decrypted values exist in the Ansible process while it executes — on a shared control node, control-node hygiene is part of the security boundary. The blast radius of one password: if every environment shares one vault password, every environment falls together; per-environment vault IDs are the production-grade refinement. None of this argues against Vault — it argues for knowing precisely what the tool promised, which was encryption at rest, delivered completely.

Frequently asked questions

Q01

What does “Attempting to decrypt but no vault secrets found” mean?

Your run referenced a vault-encrypted file but supplied no vault password for Ansible to try — nothing is broken or corrupted. Re-run with --ask-vault-pass (or configure vault_password_file in ansible.cfg). Do not “fix” it with ansible-vault decrypt, which writes the plaintext back to disk.

Q02

Do my playbooks change when I encrypt the variables file?

No. The vars_files entry and every {{ variable }} reference stay byte-for-byte identical. Encryption changes the file at rest and adds one requirement at run time: a vault password, via flag or configuration.

Q03

What is the difference between –ask-vault-pass and –ask-become-pass?

--ask-vault-pass decrypts your encrypted files on the control node. --ask-become-pass supplies the sudo password for privilege escalation on managed nodes. Same flag shape, unrelated mechanisms — a single run can legitimately need both.

Q04

What if I lose the vault password?

The file is unrecoverable — AES-256 with no backdoor is the feature. You would recreate the variables file from your records and re-encrypt. This is why the vault password lives in a password manager or CI secret store from day one, and why rekey exists for planned rotations.

Q05

Can I encrypt just one variable instead of the whole file?

Yes — ansible-vault encrypt_string 'SecretValue' --name 'PRI_CLU_PASS' produces an inline-encrypted value you paste into an otherwise plaintext YAML file, keeping non-secret values diffable. Whole-file encryption is simpler to operate; inline strings give finer-grained diffs. Both are legitimate; pick per file.

Q06

Is Ansible Vault enough for production secrets?

For encryption at rest in a repository, yes — it is the standard. Larger estates often layer a dedicated secrets manager (HashiCorp Vault, CyberArk, cloud KMS) behind it via lookup plugins, so credentials are fetched at run time rather than stored at all. Ansible Vault remains the right first step and the right lab habit either way.

Where this leaves you

Five commands now separate your lab from the most common credential failure in automation: encrypt once, --ask-vault-pass per run (or ansible.cfg once), view and edit for day-2, rekey for rotations — and the error that started this article has become a familiar checkpoint instead of a blocker. The habit transfers unchanged to production: the ONTAP playbooks guide runs every example through exactly this pattern, because the playbooks worth keeping are the ones safe to share.

Building automation your auditors will sign off on?

Secrets handling is where automation programs pass or fail review — vault discipline, least-privilege service accounts, and pipelines that never log a credential. WUC engineers build and run automation across NetApp, Cisco, and multi-OEM estates as an automation consultant, infrastructure maintenance provider, and managed services partner.

Prefer to read first? See managed services and post-OEM storage maintenance.

References

  1. Ansible project. Protecting sensitive data with Ansible Vault. The authoritative guide to encrypt, view, edit, rekey, encrypt_string, and vault IDs.
  2. Ansible project. netapp.ontap collection documentation. The modules the example playbook drives.
  3. NetApp Learning Services. STRSW-ILT-RSTAN — Automating ONTAP REST APIs with Ansible. The public workshop whose lab environment this session ran in.
  4. WUC Technologies. NetApp ONTAP Ansible Playbooks and How to Install Ansible. The playbooks this pattern protects and the control node it runs on.
About WUC Engineering
Infrastructure engineers at WUC Technologies running Ansible against multi-OEM estates — NetApp ONTAP storage, Cisco Catalyst and MDS fabrics, and the server platforms between them — under SLA-backed maintenance and managed services engagements. Authorized Dell & Cisco partner.

Find our field guides faster in Google. Add WUC Technologies as a preferred source and our engineering guides carry a “preferred” badge in your Search results, AI Overviews, and AI Mode.

Add as preferred source →