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.
- 1Capture the exact error string and match it against the four known failures
- 2Establish scope, because a fleet-wide become failure is a policy change, not a host problem
- 3Resolve the effective become settings: method, become_user, become_flags, whether a password is expected
- 4Reproduce by hand over a non-interactive SSH session, which is how Ansible runs sudo
- 5Read the sudo decision from the target: sudo -l for the automation account, and the auth log
- 6Validate the sudoers files themselves, because a syntax error breaks sudo for everyone
- 7Check the two structural traps: requiretty, and pipelining without a matching sudoers configuration
- 8Apply the narrowest sudoers change, validated before installation
- 9Verify non-interactively, then verify from Ansible
- 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: trueworks 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:
| Message | Meaning | Go to |
|---|---|---|
Missing sudo password | sudo asked for a password and Ansible had none to give | Step 3 |
Incorrect sudo password | sudo was given a password and rejected it | Step 3 |
Timeout (Ns) waiting for privilege escalation prompt | sudo produced no recognisable prompt at all | Step 6 |
sudo: a password is required in task output | sudo refused non-interactively | Step 4 |
Sorry, user ansible is not allowed to execute ... | The rule does not cover this command | Step 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
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| Scope | Where to look |
|---|---|
| One host | That host’s sudoers, its sudoers.d drop-ins, its auth log |
| One group | A role or a policy that ran against that group |
| Everything | The controller’s become configuration, or a fleet-wide policy push |
Step 3: Resolve the effective become settings
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.
# -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:
# Interactive, with a TTY, as a human account. Proves nothing about automation.
ssh web02.example.com
sudo idThree 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
# 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 -20sudo -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
sudo -n -l -U ansible | grep -i requiretty
sudo grep -rn 'requiretty' /etc/sudoers /etc/sudoers.d/ 2>/dev/nullDefaults 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:
# /etc/sudoers.d/50-ansible (mode 0440, root:root)
Defaults:ansible !requiretty
ansible ALL=(root) NOPASSWD: ALLEscalation 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.
requirettyis set and sudo exited before prompting.- A
Defaultsline changed the prompt in a way Ansible’s matcher does not recognise. - The become method is not the one you think -
become_method: subehaves differently fromsudo.
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
# 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
# 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' -oStep 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 action | Follow-up |
|---|---|
Rule widened to NOPASSWD: ALL to restore service | Narrow to what the roles need; record why it was widened |
requiretty removed globally | Scope it back to Defaults:ansible !requiretty |
| A drop-in installed by hand | Add it to the role that manages sudoers, or it disappears at the next converge |
Password authentication or --ask-become-pass used | Remove; decide the design question properly |
# 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
| Symptom | Likely cause | Resolution |
|---|---|---|
Missing sudo password | The rule is not NOPASSWD, or does not match this command | sudo -l -U ansible; fix the rule |
Incorrect sudo password | A password was supplied and is wrong | Check ansible_become_password; usually the wrong vault value |
Timeout ... waiting for privilege escalation prompt | requiretty, or sudo hanging on a name lookup | TTY=unknown in the auth log; time sudo -n true |
| Works by hand, fails from Ansible | You tested interactively, as yourself, with a TTY | ssh -T ... 'sudo -n id' |
Rule is in sudoers.d and has no effect | Wrong mode or owner, or a dot in the filename | 0440 root:root, no dot, no tilde |
| Escalation fails only when pipelining is on | Pipelining requires sudo to work without a TTY | Exempt the account from requiretty, or disable pipelining |
| Whole fleet lost escalation at once | A policy push or a role changed sudoers everywhere | Look at what converged, not at the hosts |
| sudo broken for everyone on a host | Invalid sudoers file | Console 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.