Skip to main content
RunBook Academy

← All runbooks in Ansible

medium riskinformational~40 min

Runbook: Troubleshoot a sudo failure

1 · Prerequisites

Confirm every item is in place before any state change.

2 · Pre-checks

Read-only diagnostic commands. If any of these don't match expected output, stop and investigate further.

  • · The exact error text is captured - the four common become failures have four distinct messages and they are the diagnosis
  • · It is confirmed that SSH authentication succeeded; a become failure means the connection worked
  • · The scope is established: one host, one group, or every host
  • · The account Ansible escalates FROM and the account it escalates TO are both known from resolved variables
  • · A working access path exists to read sudoers and the auth log on the target

3 · Procedure

Execute each step in order. Verify the expected output of a step before moving to the next.

  1. 1Capture the exact error string and match it against the four known failures
  2. 2Establish scope, because a fleet-wide become failure is a policy change, not a host problem
  3. 3Resolve the effective become settings: method, become_user, become_flags, whether a password is expected
  4. 4Reproduce by hand over a non-interactive SSH session, which is how Ansible runs sudo
  5. 5Read the sudo decision from the target: sudo -l for the automation account, and the auth log
  6. 6Validate the sudoers files themselves, because a syntax error breaks sudo for everyone
  7. 7Check the two structural traps: requiretty, and pipelining without a matching sudoers configuration
  8. 8Apply the narrowest sudoers change, validated before installation
  9. 9Verify non-interactively, then verify from Ansible
  10. 10Record which of the four failures it was, and make the fix exist in configuration management

4 · Verification

Confirm the procedure actually fixed the problem.

  • sudo -n id succeeds for the automation account over a TTY-less SSH session
  • ansible <host> -b -m command -a id returns uid=0 and the run exits 0
  • sudo -l for the automation account lists the commands the roles need, and no more than intended
  • visudo -c reports no syntax errors across all sudoers files
  • The affected group all succeed, with the count compared against --list-hosts
  • The sudoers rule exists in configuration management, not only on the host

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • Diagnosis is read-only; only sudoers changes need rolling back
  • Keep an open root session while editing sudoers - an invalid file breaks sudo for every account on the host
  • Validate every sudoers file with visudo -c BEFORE installing it, never after
  • If a rule was widened to restore service, narrow it back once the correct rule is known, and record why it was widened
  • Remove any temporary NOPASSWD grant that was not the intended fix, and confirm with sudo -l
  • If requiretty was disabled globally rather than for the automation account, scope it back to the account

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate to the security owner before widening a sudoers rule beyond what the roles need
  • · Escalate if sudo is broken for all accounts on a host - that needs root or console access, not another Ansible run
  • · Escalate to the platform owner if become fails fleet-wide; that is a policy or configuration management change, not a host fault
  • · Escalate if the automation account requires a become password in an unattended context - storing that password is a design decision, not a fix

A become failure means the connection worked. SSH authenticated, the remote interpreter is fine, and the problem is entirely in what happens after login. That narrows the search considerably, and Ansible tells you which narrowing applies if you read the exact string it raised.

There are four common failures and four distinct messages. Match the message first; every step after that follows from which one you have.

When to use this runbook

  • A task fails at privilege escalation rather than at the module.
  • become: true works by hand and fails from Ansible.
  • Escalation stopped working on hosts where it used to work.
  • A newly onboarded host connects but cannot escalate.

Blast radius

None while diagnosing. Editing sudoers is the one change here that can make things worse, and it can make them much worse - see the rollback section before you touch a sudoers file.

Step 1: Match the message

These are the strings ansible-core raises, verified against the 2.21.3 source in the connection and become plugins:

MessageMeaningGo to
Missing sudo passwordsudo asked for a password and Ansible had none to giveStep 3
Incorrect sudo passwordsudo was given a password and rejected itStep 3
Timeout (Ns) waiting for privilege escalation promptsudo produced no recognisable prompt at allStep 6
sudo: a password is required in task outputsudo refused non-interactivelyStep 4
Sorry, user ansible is not allowed to execute ...The rule does not cover this commandStep 4

The first two come from Ansible matching sudo’s own output: Sorry, try again. produces Incorrect sudo password, and sudo: a password is required produces Missing sudo password.

That distinction is worth holding on to. Missing means no password was available; Incorrect means one was and it was wrong. They send you to different places, and the difference is invisible in the recap.

Step 2: Establish the scope

Read-only / Safehow many hosts
ansible all -b -m command -a 'id -u' -o | tee become-scope.txt
grep -c 'rc=0' become-scope.txt
grep -ciE 'missing sudo|incorrect sudo|not allowed' become-scope.txt
ScopeWhere to look
One hostThat host’s sudoers, its sudoers.d drop-ins, its auth log
One groupA role or a policy that ran against that group
EverythingThe controller’s become configuration, or a fleet-wide policy push

Step 3: Resolve the effective become settings

Read-only / Safewhat is Ansible configured to do
ansible-config dump --only-changed | grep -i become
ansible-inventory -i inventories/production --host web02.example.com \
| grep -i become
ansible web02.example.com -b -m command -a 'id' -vvv 2>&1 | grep -i 'sudo'

The -vvv output shows the constructed sudo command line, including the flags and the prompt Ansible sets. Ansible uses a distinctive prompt so it can recognise its own escalation:

[sudo via ansible, key=REPLACE_ME] password:

If a password is expected, the fix is not to store one. It is to decide whether this account should need one at all.

Step 4: Reproduce the way Ansible does it

The single most common false conclusion in this runbook is “it works when I do it”. It works because you did it differently.

Read-only / Safenon-interactive, no TTY
# -T suppresses TTY allocation, which is how Ansible connects.
# -n makes sudo refuse rather than prompt.
sudo -iu ansible ssh -T -o BatchMode=yes ansible@192.0.2.12 'sudo -n id'

That one line reproduces almost every become failure. Compare it with the way it usually gets tested:

Read-only / Safewhat people test instead, and why it misleads
# Interactive, with a TTY, as a human account. Proves nothing about automation.
ssh web02.example.com
sudo id

Three differences, each of which independently produces a failure that the interactive test cannot see: a TTY exists, the account is yours not the automation account, and sudo is allowed to prompt.

Step 5: Read sudo’s own decision

Read-only / Safewhat is this account allowed to do
# As the automation account, on the target
sudo -n -l -U ansible

# What actually happened, from the auth log
sudo journalctl -t sudo -n 50 --no-pager
sudo grep -i sudo /var/log/auth.log | tail -20

sudo -l -U ansible prints the rules that apply to that account, resolved - including drop-ins, aliases and Defaults lines. It is the authoritative answer to “is this allowed”, and it is much faster than reading the files.

The auth log shows the specific refusal:

sudo: ansible : command not allowed ; TTY=unknown ; PWD=/home/ansible ;
      USER=root ; COMMAND=/usr/bin/systemctl restart nginx
sudo: ansible : sorry, you must have a tty to run sudo ; TTY=unknown ;
      PWD=/home/ansible ; USER=root ; COMMAND=/bin/sh -c ...

TTY=unknown in those lines is the tell for the requiretty case.

Step 6: The two structural traps

requiretty

Read-only / Safeis requiretty set
sudo -n -l -U ansible | grep -i requiretty
sudo grep -rn 'requiretty' /etc/sudoers /etc/sudoers.d/ 2>/dev/null

Defaults requiretty makes sudo refuse to run without a terminal. Ansible connects without one, so every escalation fails - and it fails in a way that looks like a permissions problem until you read TTY=unknown in the log.

The fix is to exempt the automation account specifically, not to remove the policy for everyone:

Configuration changescoped exemption
# /etc/sudoers.d/50-ansible   (mode 0440, root:root)
Defaults:ansible !requiretty
ansible ALL=(root) NOPASSWD: ALL

Escalation timeout

Timeout (Ns) waiting for privilege escalation prompt means sudo produced no output Ansible recognised as a prompt. Causes, in order of frequency:

  • sudo is hanging on something - LDAP or a name service lookup that is not answering, an audit backend that is blocking, a full disk.
  • requiretty is set and sudo exited before prompting.
  • A Defaults line changed the prompt in a way Ansible’s matcher does not recognise.
  • The become method is not the one you think - become_method: su behaves differently from sudo.
Read-only / Safeis sudo itself slow
sudo -iu ansible ssh -T ansible@192.0.2.12 'time sudo -n true'
sudo -iu ansible ssh -T ansible@192.0.2.12 'getent group ansible; time id ansible'

If sudo -n true takes several seconds, the fault is in name resolution or in a sudo plugin, and no sudoers change will fix it.

Step 7: Validate sudoers before you install anything

Configuration changethe safe edit sequence
# 0. Open a second session that is ALREADY root, and leave it open
#    sudo -i

# 1. Write the candidate somewhere harmless
cat > /tmp/50-ansible <<'EOF'
Defaults:ansible !requiretty
ansible ALL=(root) NOPASSWD: ALL
EOF

# 2. Validate the candidate file itself
visudo -cf /tmp/50-ansible

# 3. Install with the required ownership and mode
install -m 0440 -o root -g root /tmp/50-ansible /etc/sudoers.d/50-ansible

# 4. Validate the whole resolved policy, not just the new file
visudo -c

# 5. Prove it from a NEW non-interactive session before closing the root one
ssh -T -o BatchMode=yes ansible@192.0.2.12 'sudo -n id'

Mode 0440 and owner root:root are requirements, not conventions. sudo ignores files in sudoers.d that are group- or world-writable, and it ignores them silently - the rule is present, readable, correct, and has no effect.

The same applies to filenames: sudo skips files in sudoers.d whose names contain a dot or end in ~. A drop-in called 50-ansible.conf does nothing.

Step 8: Verify

Read-only / Safeverify in the right order
# 1. Non-interactive, no TTY - the way automation does it
sudo -iu ansible ssh -T -o BatchMode=yes ansible@192.0.2.12 'sudo -n id'

# 2. From Ansible
ansible web02.example.com -b -m command -a 'id' -o
echo "exit=$?"

# 3. The whole group
ansible web -b -m command -a 'id -u' -o | grep -c 'rc=0'

# 4. What the account can now do - check it is not more than intended
ansible web02.example.com -m command -a 'sudo -n -l -U ansible' -o

Step 4 is the one to not skip. A become failure fixed by widening a rule is a become failure fixed by granting privilege, and reading back what was granted is how that stays deliberate.

Rollback

Emergency actionFollow-up
Rule widened to NOPASSWD: ALL to restore serviceNarrow to what the roles need; record why it was widened
requiretty removed globallyScope it back to Defaults:ansible !requiretty
A drop-in installed by handAdd it to the role that manages sudoers, or it disappears at the next converge
Password authentication or --ask-become-pass usedRemove; decide the design question properly
Service impact possiblerevert a sudoers drop-in
# With an open root session available
rm -f /etc/sudoers.d/50-ansible
visudo -c
ssh -T -o BatchMode=yes ansible@192.0.2.12 'sudo -n id' ; echo "expect failure, rc=$?"

Common patterns

SymptomLikely causeResolution
Missing sudo passwordThe rule is not NOPASSWD, or does not match this commandsudo -l -U ansible; fix the rule
Incorrect sudo passwordA password was supplied and is wrongCheck ansible_become_password; usually the wrong vault value
Timeout ... waiting for privilege escalation promptrequiretty, or sudo hanging on a name lookupTTY=unknown in the auth log; time sudo -n true
Works by hand, fails from AnsibleYou tested interactively, as yourself, with a TTYssh -T ... 'sudo -n id'
Rule is in sudoers.d and has no effectWrong mode or owner, or a dot in the filename0440 root:root, no dot, no tilde
Escalation fails only when pipelining is onPipelining requires sudo to work without a TTYExempt the account from requiretty, or disable pipelining
Whole fleet lost escalation at onceA policy push or a role changed sudoers everywhereLook at what converged, not at the hosts
sudo broken for everyone on a hostInvalid sudoers fileConsole or root session; visudo -c

Escalation

Escalate when:

  • A rule would have to be wider than the roles justify.
  • sudo is broken for all accounts on a host. That needs console access.
  • Escalation failed fleet-wide. That is a configuration management or policy event.
  • The automation account is required to supply a become password in an unattended context.

References

  1. Understanding privilege escalation: become
  2. sudoers(5)
  3. sudo(8)