ansible-lint in CI Without the Vault Password: 100+ internal-error Findings and Unlinted Playbooks¶
The Problem¶
ansible-lint is clean on your workstation but fails in CI, and the failure never goes away:
internal-error: Unexpected error code 1 from execution of: ansible-playbook --syntax-check -vv site.yml
[ERROR]: The vault password file /root/.ansible/vault_pass was not found
...
Failed: 119 failure(s), 0 warning(s) in 467 files processed of 557 encountered.
One internal-error per top-level playbook, every run, whatever the code looks like. If the lint step was advisory (|| true), it hid this; the day it became a hard gate, CI went permanently red.
The worse half of the problem is silent: those playbooks were not being linted at all. A deliberately bad probe playbook (unnamed play, shell instead of command, no FQCN, no changed_when) produced exactly one finding in CI — the internal-error — and none of the real ones. A file whose syntax check fails gets no other rule evaluated. Role task files, handlers and templates are linted normally, so the gap is only the playbooks sitting at the top of the repo.
Why It Happens¶
ansible-lint runs ansible-playbook --syntax-check on each playbook. Syntax checking loads the inventory's variables, including an encrypted group_vars/all/vault.yml, and ansible.cfg points vault_password_file at a file that doesn't exist on the CI runner. (ansible-core 2.19 reports a missing password file as an error, not just a warning.) The check dies, ansible-lint wraps the failure as internal-error, and stops there for that file.
Putting the vault password in CI as a secret fixes it, but puts your master secret on the CI path — anything that can edit a workflow can print it. Usually not worth it for a linter.
Fix: stub the vault in the throwaway CI checkout¶
Linting never needs the secret values, only files that parse. So in the CI step only:
- install the collections your repo declares (a bare
ansible-corehas none, and without them the playbooks fail withsyntax-check[unknown-module]); - replace the encrypted vault file with an empty YAML document in the ephemeral checkout, and restore it on exit;
- satisfy
vault_password_filewith a dummy file via the environment (this overridesansible.cfg); - run plain
ansible-lint.
- name: ansible-lint
run: |
ansible-galaxy collection install -r collections/requirements.yml
trap 'git checkout -- group_vars/all/vault.yml' EXIT
printf -- '---\n' > group_vars/all/vault.yml
echo dummy > /tmp/vault_pass_dummy
export ANSIBLE_VAULT_PASSWORD_FILE=/tmp/vault_pass_dummy
ansible-lint --offline
Nothing secret is in CI, nothing is committed (the stub exists only in the runner's disposable checkout, and the trap restores the real file so later steps are unaffected), and the encrypted file itself can stay excluded from lint via exclude_paths in .ansible-lint.
Verify (don't skip — it's how you know the gate bites)¶
- Whole repo:
Passed: 0 failure(s) … in 467 files … Last profile that met the validation criteria was 'production'. - Probe: add a deliberately bad top-level playbook on a scratch branch (or run
ansible-linton it locally with the same stub). Before the fix it reports one vault-causedinternal-error; after, it reports the real findings (name[play],command-instead-of-shell,fqcn[action-core],name[missing],no-changed-when). Delete the probe afterwards.
Costs and gotchas¶
- Time. Installing the collections from Galaxy dominated the job: about 8.5 minutes per run here, up from ~40 seconds. Cache
~/.ansible/collectionsbetween runs if that hurts. - Galaxy dependency. If Galaxy is down the step fails loudly — noisy, but not silent.
- Don't hand-roll a filter instead. The tempting shortcut is to drop the
internal-errorfindings in a script. It turns the gate green but leaves the playbooks unlinted, which is the real problem. - A separate advisory
ansible-playbook --syntax-checkstep (if you keep one) still needs the real vault and will keep logging the password error; it only goes quiet if it also gets the stub. - If your
ansible.cfgsetsvault_password_file, the environment variable wins — that is what makes the dummy file work without editing the config.
Related¶
- Ansible: Vault Password File Not Found — the same error on a workstation, and how to find which config sets it
- Forgejo Actions: Jobs Finish, Then Are Marked Failed ~12 Minutes Later — the other CI problem that surfaced the same day