Skip to content

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:

  1. install the collections your repo declares (a bare ansible-core has none, and without them the playbooks fail with syntax-check[unknown-module]);
  2. replace the encrypted vault file with an empty YAML document in the ephemeral checkout, and restore it on exit;
  3. satisfy vault_password_file with a dummy file via the environment (this overrides ansible.cfg);
  4. 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-lint on it locally with the same stub). Before the fix it reports one vault-caused internal-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/collections between 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-error findings in a script. It turns the gate green but leaves the playbooks unlinted, which is the real problem.
  • A separate advisory ansible-playbook --syntax-check step (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.cfg sets vault_password_file, the environment variable wins — that is what makes the dummy file work without editing the config.