Runbook: Troubleshoot an authentication 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.
- · It is confirmed that the failure is at authentication and not at transport - Permission denied means sshd was reached
- · The scope is established: one host, one group, or every host
- · The account Ansible is actually using is known, from resolved inventory rather than from the playbook
- · The identity file Ansible is actually offering is known, from a verbose run rather than from ansible.cfg
- · A working access path exists for reading the server side - another key, console, or a second operator
3 · Procedure
Execute each step in order. Verify the expected output of a step before moving to the next.
- 1Confirm the failure is authentication, from the exact client message
- 2Establish the scope, because everything failing at once points at the controller
- 3Resolve the effective user, port and identity file from a verbose run
- 4Reproduce by hand as the automation account with BatchMode on and IdentitiesOnly on
- 5Read the server side: sshd logs on the target, matched by syslog identifier not by unit name
- 6Ask sshd where it reads authorised keys from, including under any Match block that applies
- 7Compare the offered key fingerprint against the fingerprints in the file sshd actually reads
- 8Check the boring causes: file permissions, full disk, expired account, AllowGroups, locked password
- 9Apply the narrowest fix, then verify with a connecting module
- 10Record which of the two sides was wrong - the key offered or the key expected
4 · Verification
Confirm the procedure actually fixed the problem.
- ✓ansible <host> -m ping returns SUCCESS with the automation accounts default identity, no extra flags
- ✓A run with IdentitiesOnly=yes and only the intended key still succeeds - proving the intended key is the one being accepted
- ✓sshd logs show an Accepted publickey line whose fingerprint matches the intended key
- ✓The fix is present in configuration management, not only on the host
- ✓The whole affected group authenticates, with the SUCCESS count compared against --list-hosts
5 · Rollback
If verification fails, undo the procedure in reverse order.
- ↶Diagnosis is read-only; only the fixes need rolling back
- ↶If a key was added by hand to restore access, remove it once the managed key works and confirm removal by fingerprinting the file
- ↶If password authentication was enabled to get in, disable it again and confirm with sshd -T
- ↶If sshd_config was edited, validate with sshd -t BEFORE reloading, keep the existing session open, and confirm a NEW login succeeds before closing it
- ↶If host key checking was disabled, re-enable it and record the fingerprint from a trusted source
6 · Escalation
When the runbook isn't enough, contact:
- · Escalate to security if a key that should not be trusted is found in authorized_keys, or if a host key changed without explanation
- · Escalate to the identity provider owner if sshd uses AuthorizedKeysCommand - keys do not live on the host and nothing you do there will help
- · Escalate to the host owner if the account is locked or expired, or the disk is full
- · Escalate if every host failed authentication simultaneously - that is a controller or credential-store event and the fleet is not the place to look
Permission denied (publickey) is one of the least informative messages
in system administration, and it is deliberately so - sshd will not tell
a client why it was rejected. That means the client message tells you
almost nothing, and every minute spent theorising from it is wasted.
The method here is simple: find out which key was offered, find out which keys were expected, and compare them. Everything else is a variation on that.
When to use this runbook
- A run fails with
Permission denied (publickey). - Authentication works by hand and fails from Ansible, or vice versa.
- A host stopped authenticating after a change nobody connects to SSH.
- A newly onboarded host never authenticated.
Blast radius
None while diagnosing. The fixes touch one host or one group, and the
one to be careful with is editing sshd_config - see the rollback
section, because that is the change that turns an authentication problem
into a lockout.
Step 1: Confirm it is authentication
ansible web02.example.com -m ping -o| Message | Where you are |
|---|---|
Connection timed out | Transport. Not this runbook - use the unreachable-hosts runbook |
Connection refused | Transport. sshd not listening |
Host key verification failed | Server identity, not client authentication. See the callout below |
Permission denied (publickey) | Authentication. You reached sshd and it rejected the credential |
Permission denied (publickey,password) | Same, and password auth is also offered - note that for later |
Permission denied is good news in one narrow sense: the network is
fine, sshd is running, and the problem is in a much smaller space.
Step 2: Establish the scope
ansible all -m ping -o | tee auth-scope.txt
grep -c SUCCESS auth-scope.txt
grep -c 'Permission denied' auth-scope.txt| Scope | Where the fault almost certainly is |
|---|---|
| One host | That host’s authorized_keys, permissions, account state |
| One group | A shared change: a role that rewrote keys, a Match block, a bastion |
| Everything | The controller: wrong key installed, key permissions changed, agent not loaded, credential expired |
Hosts do not independently stop trusting a key at the same moment. If everything failed at once, look at the controller and you will usually be finished before you would otherwise have finished reading one host’s sshd log.
Step 3: Find out what Ansible is actually offering
Not what ansible.cfg says. What the process does.
ansible web02.example.com -m ping -vvvv 2>&1 \
| grep -E 'ESTABLISH|SSH: EXEC|Offering|Authentications that can continue'That prints the literal ssh command line Ansible builds, with every
option: the user, the port, the identity file, the proxy command. Read
it rather than reconstructing it - the difference between what you
believe is configured and what is on that line is where a good
proportion of these incidents live.
ansible-inventory -i inventories/production --host web02.example.com \
| grep -iE 'ansible_(user|host|port|ssh)'Step 4: Reproduce by hand, correctly
sudo -iu ansible ssh -vv \
-o BatchMode=yes \
-o IdentitiesOnly=yes \
-i /home/ansible/.ssh/id_ed25519 \
-o ConnectTimeout=10 \
ansible@192.0.2.12 trueThree flags, and all three matter:
sudo -iu ansible- testing as yourself proves nothing about the automation account, which has a different key and a different home directory.BatchMode=yes- without it SSH may fall back to a password prompt, and you conclude authentication works when what worked was you typing a password automation does not have.IdentitiesOnly=yes- without it SSH offers every key in the agent and every default identity file. A host that does not trust the key you are testing may still authenticate with a different one, and you will conclude the key is fine.
Read the -vv output for the Offering public key: lines. That is the
list of what the client tried, in order, with fingerprints.
Step 5: Read the server side
The client cannot tell you why. The server can.
# From the console, or over a working access path
sudo journalctl -t sshd -t sshd-session -n 100 --no-pagerWhat to look for:
Accepted publickey for ansible from 192.0.2.1 port 51234 ssh2: ED25519 SHA256:REPLACE_ME
Failed publickey for ansible from 192.0.2.1 port 51234 ssh2: ED25519 SHA256:REPLACE_ME
Authentication refused: bad ownership or modes for directory /home/ansible/.ssh
User ansible from 192.0.2.1 not allowed because none of user's groups are listed in AllowGroups
Invalid user ansible from 192.0.2.1
Each of those points somewhere completely different. The fingerprint in
a Failed publickey line is the key the client offered - compare it
against what the host expects, which is the next step.
Step 6: Ask sshd where it reads keys from
Never assume ~/.ssh/authorized_keys.
sudo sshd -T -C user=ansible,host=web02.example.com,addr=192.0.2.1 \
| grep -iE 'authorizedkeysfile|authorizedkeyscommand|allowgroups|allowusers|denyusers|pubkeyauthentication|permitrootlogin'The -C matters: sshd -T without a connection specification does not
evaluate Match blocks, and a Match block can override
AuthorizedKeysFile for exactly the user you are debugging.
Two answers change everything:
authorizedkeysfilepoints somewhere central - for example/etc/ssh/authorized_keys/%u. Your edits to the home directory are irrelevant.authorizedkeyscommandis notnone- keys come from a helper (LDAP, a key server, a CA). Nothing on the host’s filesystem grants or revokes access. Escalate to whoever owns that source.
Step 7: Compare offered against expected
# What the controller holds
sudo -iu ansible ssh-keygen -lf /home/ansible/.ssh/id_ed25519.pub
# What the host trusts - reads a whole authorized_keys file
sudo ssh-keygen -lf /home/ansible/.ssh/authorized_keysStep 8: Check the boring causes
More authentication incidents are caused by these than by keys.
# Permissions - sshd refuses group- or world-writable paths
stat -c '%a %U:%G %n' /home/ansible /home/ansible/.ssh /home/ansible/.ssh/authorized_keys
# expect: 700 or 755 on the home, 700 on .ssh, 600 on authorized_keys
# Full disk - breaks auth and reports it as a key problem
df -h /home /var /tmp
# Account state
passwd -S ansible
chage -l ansible | grep -i expire
# Is the account excluded by policy?
sudo sshd -T -C user=ansible,host=web02.example.com,addr=192.0.2.1 \
| grep -iE 'allowgroups|allowusers|denygroups|denyusers'
id ansibleThe permissions one is worth memorising: sshd refuses to use
authorized_keys if the file, the .ssh directory or the home
directory is writable by anyone other than the owner. A role that set
mode: '0777' on a home directory produces Permission denied (publickey) on every host it touched, and nothing about the message
suggests permissions.
A full /home is the second: sshd cannot do what it needs, and reports
a public key failure.
Step 9: Fix, then verify
Apply the narrowest fix that addresses what you found. Then verify properly:
# Default path, no special flags - this is what automation will do
ansible web02.example.com -m ping -o
echo "exit=$?"
# And prove it is the intended key being accepted, not a fallback
sudo -iu ansible ssh -o BatchMode=yes -o IdentitiesOnly=yes \
-i /home/ansible/.ssh/id_ed25519 ansible@192.0.2.12 true && echo 'intended key OK'
# Whole group
ansible web -m ping -o | grep -c SUCCESSThen confirm the fix exists in the repository. A key added by hand, a
permission corrected on one host, an sshd_config edited live - all of
these disappear at the next converge or the next rebuild.
Rollback
| Emergency action taken | Follow-up |
|---|---|
| Key added by hand | Replace with the managed key; remove the manual one; fingerprint the file to confirm |
| Password authentication enabled | Disable it; confirm with sshd -T | grep passwordauthentication |
sshd_config edited | Validate with sshd -t, reload from an open session, and confirm a new login before closing it |
| Host key checking disabled | Re-enable; record the fingerprint from a trusted source |
| Permissions loosened | Set them to the correct values, not looser ones that happen to work |
# 1. Validate before reloading - a bad config stops sshd starting
sudo sshd -t && echo 'config OK'
# 2. Reload from your CURRENT session, and keep it open
sudo systemctl reload ssh 2>/dev/null || sudo systemctl reload sshd
# 3. From a SECOND terminal, prove a new login works
ssh -o BatchMode=yes ansible@192.0.2.12 true && echo 'new session OK'
# 4. Only now close the first sessionStep 3 is the whole point. An existing session survives a reload of a broken configuration; the next login does not. Closing the session before testing a new one is how an authentication problem becomes a lockout.
Common patterns
| Symptom | Likely cause | Resolution |
|---|---|---|
| Every host failed at once | Controller: wrong key, key permissions, agent | Check the controller first |
| Works by hand, fails from Ansible | You tested as yourself, or SSH fell back to another key | sudo -iu ansible with IdentitiesOnly=yes |
| Works from Ansible, fails by hand | Different user or identity file than you assumed | Read the -vvvv ssh command line |
Permission denied after a role ran | The role changed home or .ssh permissions | stat the paths; sshd refuses group-writable |
Permission denied, key is definitely present | Full disk, or the agent offered too many keys first | df -h; IdentitiesOnly=yes |
| sshd logs appear empty | journalctl -u sshd on a Debian-family host | Use -t sshd -t sshd-session |
Key added to authorized_keys, still refused | sshd reads keys from elsewhere, or a Match block overrides it | sshd -T -C user=... |
| Nothing on the host explains it | AuthorizedKeysCommand - keys come from an identity provider | Escalate to that owner |
Escalation
Escalate when:
- A key you do not recognise is in
authorized_keys, or a host key changed without explanation. Security, immediately. AuthorizedKeysCommandis in use. The host is not authoritative.- The account is locked or expired, or the disk is full. Host owner.
- Every host failed simultaneously and the controller checks out.