Skip to main content
RunBook Academy

AnsibleXLV · Debugging and TroubleshootingDebugging and Troubleshooting

Diagnosing privilege escalation failures

Advanced⏱ ~24 minansible-playbooksshsudo

What you'll learn

  • Reproduce a become failure outside Ansible to establish where the fault lives
  • Recognise the distinct signatures of missing rule, missing password and requiretty
  • Explain the unprivileged become_user temp file problem and evaluate the options
  • Use ANSIBLE_KEEP_REMOTE_FILES safely and know what it leaves behind

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

Escalation failures are the second half of the transport story, and they respond to the same discipline: reproduce it outside Ansible first.

The connection succeeded — you know that, because a task that did not need become would have worked. What failed is the step after it. That narrows the problem to one command, run over a connection you have already proved.

The one command that splits the problem

Read-only / Safeask sudo directly, over the connection Ansible uses
ssh ops@app-047.example.com 'sudo -n true' ; echo "exit=$?"

# and to see what the sudoers policy actually grants this user
ssh ops@app-047.example.com 'sudo -l -n'

The defaults are worth knowing while reading that table. Verified from ansible-config list on 2.21.3: become_method defaults to sudo, become_user to root, and become_ask_pass to false — so by default Ansible escalates to root via sudo and will not prompt, which is why a rule without NOPASSWD fails rather than asking.

The four signatures

Missing sudo password

The remote user has a sudoers rule that requires a password, and no password was supplied. Ansible cannot prompt in an automated run.

Two fixes, and the choice is a policy decision rather than a technical one. Either the automation account gets NOPASSWD for the commands it needs — narrow, auditable, documented in the sudoers file — or the password is supplied from a vault at run time, which means the password exists in the automation path and must be protected everywhere it goes.

Most estates choose NOPASSWD scoped tightly, because a password that automation knows is not much of a second factor.

Incorrect sudo password

A password was supplied and rejected. Worth distinguishing from the previous case because the diagnosis is different: this one means your vault content or your lookup is wrong, not that your sudoers policy is.

Check it without printing it. sudo -n true returning “a password is required” proves the policy; whether the password you hold is correct is a separate question and one you resolve by rotating rather than by echoing.

user is not allowed to execute

No rule matched. The most common cause on a mixed fleet is that the rule exists in /etc/sudoers.d/ on the hosts built by one process and not on the hosts built by another — which makes this a fleet-consistency finding as much as a permissions one.

The second most common: the rule is scoped to specific commands, and Ansible does not run the command you think. It runs a shell invoking a Python interpreter on a temp file, so a sudoers rule granting /usr/bin/systemctl does not grant what a systemd_service task actually executes.

no tty present / requiretty

sudo is configured to require a terminal. Ansible does not allocate one by default, and pipelining makes it less likely to have one.

This is a sudoers setting on the managed node, and the fix belongs there — Defaults:<user> !requiretty scoped to the automation account rather than removed globally. Working around it from the Ansible side by forcing a pty is possible and hides a policy the host’s owner set deliberately.

Becoming an unprivileged user

The awkward case, and the one where Ansible’s own mechanics are the problem rather than the sudoers policy.

When you connect as ops, become: true and become_user: appuser, the module payload is written to a temp directory owned by ops, and then a process running as appuser has to read it. Neither user is root, so no ordinary permission arrangement makes that work without either widening the file or using POSIX ACLs.

The failure message names the situation directly, and the documented options are:

ApproachWhat it costs
POSIX ACLs on the temp directory (setfacl available on the target)Requires acl installed and a filesystem mounted with ACL support
world_readable_tempThe module payload is world-readable while it exists
pipeliningAvoids writing the payload at all, but requires requiretty off and does not work for every module
Connect as the target user directlyCleanest where the credential model allows it
Read-only / Safewhat world_readable_temp actually says — read from ansible-doc on 2.21.3
$ ansible-doc -t shell sh | grep -A 14 world_readable_temp
   world_readable_temp  This makes the temporary files created on the
                      machine world-readable and will issue a
                      warning instead of failing the task.
                      It is useful when becoming an unprivileged
                      user.
      set_via:
        env:
        - name: ANSIBLE_SHELL_ALLOW_WORLD_READABLE_TEMP
        ini:
        - key: allow_world_readable_tmpfiles
          section: defaults
        vars:
        - name: ansible_shell_allow_world_readable_temp
      default: false
      type: boolean

ANSIBLE_KEEP_REMOTE_FILES

The debugging tool of last resort: keep the module payload on the managed node so you can run it by hand.

Read-only / Safewhat the setting is and what it disables — read from ansible-config on 2.21.3
$ ansible-config list | grep -A 6 '^DEFAULT_KEEP_REMOTE_FILES'
DEFAULT_KEEP_REMOTE_FILES:
default: false
description:
- Enables/disables the cleaning up of the temporary files Ansible used to execute
  the tasks on the remote.
- If this option is enabled it will disable ``ANSIBLE_PIPELINING``.

Two things follow. First, it changes execution behaviour: with pipelining disabled, the payload is written to disk on every task, which is a different code path from the one you were debugging. Second, and more seriously:

Read-only / Safeusing it, and cleaning up after it
# one host, one run, one task
ANSIBLE_KEEP_REMOTE_FILES=1 ansible-playbook -i inventory/prod site.yml \
--limit app-047.example.com --tags failing_task -vvv

# then, on the host, run the preserved payload by hand to see the raw error
ssh ops@app-047.example.com \
'ls -d ~/.ansible/tmp/ansible-tmp-*/ | tail -1'

# and clean up when finished — this deletes files, so it is deliberate
ssh ops@app-047.example.com 'rm -rf ~/.ansible/tmp/ansible-tmp-*'

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task with become: true fails on one host. Which command establishes fastest whether the fault is in sudo or in Ansible?

  2. Q2. Which are accurate about world_readable_temp on ansible-core 2.21.3? Select all that apply.

  3. Q3. Setting ANSIBLE_KEEP_REMOTE_FILES has no effect on how tasks execute; it only stops the cleanup step at the end.

  4. Q4. A sudoers rule grants the automation account NOPASSWD on /usr/bin/systemctl, but a systemd_service task still fails with user is not allowed to execute. Why?

Passing score: 75%. Answers are checked in this browser.