Skip to main content
RunBook Academy

← All break/fix scenarios in Ansible

intermediatecontroller~30 min

Break/Fix: it works on my machine, and on the CI runner it connects to production with no vault and five forks

Reported symptoms

  • The same commit behaves differently on the CI runner and on every engineer workstation
  • On CI the run fails with `Attempting to decrypt but no vault secrets found`, against vaulted files that have not changed
  • On CI the run is roughly ten times slower before it fails
  • On CI the run connects to hosts the repository configuration was supposed to exclude
  • The `ansible.cfg` file is present in the working directory on the runner and its contents are correct
  • Deleting and recreating the file on the runner changes nothing

Evidence

  • · `ansible-config dump --only-changed` on a workstation names the repository `ansible.cfg` as the source
  • · The same command on the runner reports `CONFIG_FILE() = None`
  • · A warning above that output says Ansible is running in a world-writable directory and is ignoring it as a configuration source
  • · `stat -c %a` on the checkout directory on the runner returns a mode with the world-write bit set
  • · The CI job unpacks a build artefact into the workspace with permissions preserved from the archive
  • · Every setting the repository configuration provides falls back to its built-in default on the runner
  • · Neither `ANSIBLE_CONFIG` nor a user-level configuration file exists on the runner to take its place
Diagnosis and resolutionclick to reveal

Root cause

Ansible refuses to load an `ansible.cfg` from a world-writable directory. The reason is sound - a configuration file controls which plugins load, which interpreter runs, and where credentials come from, so honouring one that any user on the machine could rewrite would be a straightforward privilege escalation. On the CI runner the workspace became world-writable when a build step unpacked an archive that carried permissive modes, and from that moment the repository configuration was skipped. Nothing about the file is wrong: it is present, syntactically valid, correctly named and correctly committed, and every check anybody performed confirmed as much. Because there is no user-level configuration and no `ANSIBLE_CONFIG` on the runner, every setting falls back to its built-in default at once - the vault password file, the inventory path, forks, and the connection settings all revert simultaneously, which is why the symptoms look like four unrelated problems rather than one. Ansible does warn, and the warning is a single line above output that nobody reads on a run that is about to fail for what appears to be an unrelated reason.

Remediation

Fix the permissions on the workspace so the directory is not world-writable, and fix the build step that made it so, since a workspace recreated by the next build will be world-writable again. Then remove the dependency on directory-relative configuration discovery entirely: set `ANSIBLE_CONFIG` to an explicit path in the CI job, which is unambiguous, independent of the working directory, and unaffected by permissions. Check what the runs that did complete under default settings actually did, because a run with the wrong inventory and the wrong connection settings may have reached hosts the repository configuration was there to exclude.

Verification

`ansible-config dump --only-changed` on the runner must name the intended configuration file, and every setting the repository provides must appear with that file as its source. Prove the check can fail by making a scratch directory world-writable and confirming the warning returns and `CONFIG_FILE()` reports None. Add a pipeline step that asserts the resolved configuration file is the expected one and fails the build otherwise, then confirm that step goes red when pointed at a directory with the wrong permissions. Finally confirm the settings that matter individually - the vault identity, the inventory path, the forks value - rather than assuming that a named configuration file means every setting arrived.

Prevention

Do not rely on directory-relative configuration discovery in automation. Set `ANSIBLE_CONFIG` explicitly in every pipeline and every scheduled job, so the configuration in force does not depend on where the process happens to be running or on the permissions of that directory. Make the first step of any run print `ansible-config dump --only-changed` into the log, so the effective configuration is recorded alongside the run rather than assumed. Assert the resolved configuration file in CI. Keep workspace permissions tight and treat a world-writable checkout as a build failure in its own right, because it is a security problem before it is an Ansible problem. And when a run behaves differently on two machines, compare effective configuration before comparing anything else - the code is identical and something else is not.

Reported symptoms

A pipeline that has worked for a year starts failing on the CI runner. The same commit works on every engineer workstation.

The failures do not look like one problem:

  • The run fails with Attempting to decrypt but no vault secrets found, against vaulted files nobody has touched.
  • Before it stops, it is roughly ten times slower than the same run on a workstation.
  • The verbose output shows it connecting to hosts that the repository inventory configuration was supposed to exclude.
  • Host key checking behaves differently.

Four symptoms, four theories, four people. The vault password file is present and readable on the runner. The inventory is committed. Nothing in the repository changed.

Somebody checks that ansible.cfg exists in the workspace on the runner. It does. Its contents are byte-identical to the copy in the repository. That is where the investigation stops for two hours.

Evidence provided

Read-only / Safeon an engineer workstation
$ ansible-config dump --only-changed
CONFIG_FILE() = /srv/automation/ansible.cfg
DEFAULT_FORKS(/srv/automation/ansible.cfg) = 50
DEFAULT_VAULT_PASSWORD_FILE(/srv/automation/ansible.cfg) = /etc/ansible/vault-pass
DEFAULT_HOST_LIST(/srv/automation/ansible.cfg) = ['/srv/automation/inventory']
Read-only / Safeon the CI runner, in the same commit
$ ansible-config dump --only-changed
[WARNING]: Ansible is being run in a world writable directory (/builds/ops/automation), ignoring it as an ansible.cfg source. For more information see https://docs.ansible.com/ansible/devel/reference_appendices/config.html#cfg-in-world-writable-dir
CONFIG_FILE() = None
Read-only / Safethe file is present and correct, which is what stalled the investigation
$ ls -l ansible.cfg; md5sum ansible.cfg
-rw-r--r-- 1 build build 214 Aug 11 09:02 ansible.cfg
5d41402abc4b2a76b9719d911017c592  ansible.cfg
Read-only / Safethe directory, not the file
$ stat -c '%a %n' .
777 .
Read-only / Safepermissions preserved from the archive, applied to the workspace
$ grep -n -A3 'unpack' .gitlab-ci.yml
  script:
- tar -xf artefacts/dependencies.tar -C .
Read-only / Safeno ANSIBLE_CONFIG to fall back to
$ env | grep -i ansible

Work the evidence before reading on

The file is present, correct and identical. The ansible-config output disagrees with all three of those facts being sufficient.

  1. Compare the two ansible-config dump outputs. One names a configuration file and the other does not. What does CONFIG_FILE() = None mean for every setting the file was providing?
  2. Read the warning line above it. It names a directory, not a file. Check the permissions of that directory rather than of the file.
  3. Four symptoms appeared at once. What single change would produce all four simultaneously?

Before continuing: why would Ansible deliberately refuse to read a configuration file that is present and valid?

Root cause

1. Ansible refuses configuration from a world-writable directory

Ansible discovers ansible.cfg in the current working directory, among other places. It will not load one from a directory that is world-writable, and it says so:

[WARNING]: Ansible is being run in a world writable directory (/builds/ops/automation), ignoring it as an ansible.cfg source.

The reason is a genuine security boundary. A configuration file determines which plugin paths are searched, which Python interpreter runs on targets, where the vault password comes from, and which inventory is used. Honouring one that any local user could rewrite would let any local user redirect an automation run.

The refusal is about the directory, not the file. A perfectly permissioned ansible.cfg inside a world-writable directory is still ignored, because anyone who can write to the directory can replace the file.

2. Every setting reverted at once

The runner has no ANSIBLE_CONFIG environment variable and no user-level or system-level configuration file. So when the repository configuration was skipped, there was nothing to take its place and every setting fell back to its built-in default simultaneously:

SettingRepository valueFallback
Vault password filea pathnone, so decryption fails
Inventorythe repository inventorythe built-in default
Forks505
Host key checkingas configuredthe built-in default

That is why it presented as four unrelated faults. One cause, four visible effects, and each effect plausible enough on its own to sustain its own theory.

3. The workspace permissions came from a build step

A build step unpacks a dependency archive into the workspace, preserving the permissions recorded in the archive. Those permissions include a world-writable mode on the extraction target.

Nothing about the Ansible configuration changed. The directory it lives in did, and it will change again on the next build unless the build step is fixed.

Resolution

  1. Establish what the runs under default settings actually did. A run with the wrong inventory and the wrong connection settings may have reached hosts the repository configuration was there to exclude; that is the part with consequences beyond a red build.
  2. Fix the workspace permissions so the directory is not world-writable, and confirm ansible-config dump --only-changed now names the intended file.
  3. Fix the build step that created the permissions. A workspace recreated by the next build will be world-writable again, and the incident will return looking like a flaky pipeline.
  4. Set ANSIBLE_CONFIG explicitly in the CI job to an absolute path. This removes the dependency on the working directory and on its permissions entirely, and it is the durable fix rather than the immediate one.
  5. Add a pipeline step that prints ansible-config dump --only-changed into the log before anything runs, so the effective configuration is recorded with every run.
  6. Add an assertion that the resolved configuration file is the expected one, and fail the build when it is not.
  7. Check the other pipelines and scheduled jobs on the same runner for the same dependency on directory-relative discovery.
  8. Treat the world-writable workspace as a security finding in its own right and raise it separately - any local user on that runner could have replaced the configuration or the playbooks.

Verification

  1. The intended configuration file is in force. ansible-config dump --only-changed on the runner names it, and every setting the repository provides appears with that file as its source.
  2. The check can fail. Make a scratch directory world-writable, run the same command there, and confirm the warning returns and CONFIG_FILE() reports None. Watching the failure is how you know the check means something.
  3. The pipeline assertion goes red. Point it at a directory with the wrong permissions on a scratch branch and confirm the build fails before any play runs.
  4. The individual settings that matter are correct. Confirm the vault identity, the inventory path and the forks value by name; a named configuration file is not proof that every setting arrived, since the file could also be the wrong one.
  5. The permissions survive a fresh build. Trigger a build from clean and confirm the workspace is not world-writable afterwards, which tests the build-step fix rather than the manual one.
  6. The run succeeds end to end with the vault, the correct inventory and the configured concurrency, and takes a duration consistent with 50 forks rather than 5.
  7. No other job on the runner depends on directory-relative discovery. Each one either sets ANSIBLE_CONFIG or asserts the resolved file.

Prevention

  • Set ANSIBLE_CONFIG explicitly in every pipeline and scheduled job. Directory-relative discovery is convenient at a terminal and inappropriate in automation, where the working directory is decided by whatever ran last.
  • Print the effective configuration at the start of every run. ansible-config dump --only-changed in the log turns this entire class of problem into a two-second comparison.
  • Assert the resolved configuration file in CI, so a run with the wrong configuration fails before it connects to anything.
  • Keep workspace permissions tight, and treat a world-writable checkout as a build failure. Ansible refusing to read it is a symptom; the security exposure is the finding.
  • Remember that configuration discovery is first-match-wins, not merged. Losing one file does not partially degrade behaviour, it reverts every setting that file provided.
  • When the same commit behaves differently on two machines, compare effective configuration first. The code is provably identical, so the difference is in the environment, and this is where it usually is.
  • Be suspicious of several unrelated symptoms appearing together. One cause with four effects is more likely than four causes arriving on the same afternoon.