Skip to main content
RunBook Academy

← All runbooks in Ansible

low riskinformational~30 min

Runbook: Troubleshoot unreachable hosts

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 failing command is recorded, including the inventory, the limit and any extra variables
  • · The recap is read: unreachable and failed are different numbers and they mean different things
  • · It is established whether the failure affects one host, one group, or everything - the answer changes the whole diagnosis
  • · Whether this ever worked, and what changed since, is established before any theory is formed
  • · The controller itself has network access - a controller that lost DNS looks exactly like a fleet that went away

3 · Procedure

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

  1. 1Read the recap and confirm the failure is UNREACHABLE rather than a task failure
  2. 2Establish the scope: one host, one group, or all hosts
  3. 3Resolve what Ansible is actually connecting to: ansible_host, ansible_user, ansible_port, proxy settings
  4. 4Test the transport by hand with ssh -v, as the automation account, to the resolved address
  5. 5Separate the layers in order: name resolution, TCP reachability, SSH handshake, authentication, remote interpreter
  6. 6For a group-wide failure, check the shared component: bastion, firewall, credential, DNS
  7. 7For a single host, check the host: sshd running, disk full, host key changed, account locked
  8. 8Confirm the fix with a connecting module, not with debug
  9. 9Record which layer failed, because that is the only useful part of the incident note

4 · Verification

Confirm the procedure actually fixed the problem.

  • ansible <host> -m ping returns SUCCESS and the run exits 0
  • The exit code is checked explicitly - an unreachable host produces exit code 4, verified on 2.21.3
  • A run against the full group returns SUCCESS for every host, with the count compared against --list-hosts
  • The diagnosis names a specific layer - DNS, TCP, SSH, auth, interpreter - not "it was flaky"

5 · Rollback

If verification fails, undo the procedure in reverse order.

  • Diagnosis is read-only; there is nothing to roll back until a fix is applied
  • If host key verification was disabled to get past the problem, re-enable it and record the fingerprint properly
  • If a firewall rule was added to restore access, confirm it is the narrowest rule that works and that it is declared in configuration management
  • If an SSH key was added by hand to restore access, replace it with the managed key and remove the manual one
  • Any change made under pressure to restore reachability is temporary until it exists in the repository

6 · Escalation

When the runbook isn't enough, contact:

  • · Escalate to the network team if TCP reachability fails from the controller but succeeds from elsewhere
  • · Escalate to the host owner if the host is up but sshd is not, or the disk is full
  • · Escalate to security immediately if a host key has changed and nobody can account for it - that is a possible machine-in-the-middle, not an inconvenience
  • · Escalate if the whole fleet became unreachable at once; the cause is almost always on the controller or in a shared credential, and widening the search wastes the window

“Unreachable” is Ansible telling you it never got far enough to run anything. That is a narrower statement than it sounds, and the whole value of this runbook is in taking it literally: the failure is somewhere between the controller’s network stack and the remote Python interpreter, and there are five distinct layers in that gap.

Diagnose them in order. Skipping to “it must be the key” is how a DNS change costs an hour.

When to use this runbook

  • A playbook recap shows unreachable=1 or more.
  • A run exits 4.
  • A host that was managed yesterday cannot be reached today.
  • A newly onboarded host never connected in the first place.

Blast radius

None. Every step in this runbook is read-only until you apply a fix, and the fixes are called out separately.

Step 1: Read the recap properly

Read-only / Safethe recap distinguishes two things
ansible-playbook -i inventories/production site.yml --limit web
echo "exit=$?"
PLAY RECAP
web01.example.com : ok=4 changed=1 unreachable=0 failed=1
web02.example.com : ok=0 changed=0 unreachable=1 failed=0

web01 connected fine and a task failed - that is a different runbook. web02 was never reached. Only the second is this runbook’s problem, and the distinction is the first thing to establish because the two have no diagnostic steps in common.

Exit codes, verified on 2.21.3:

OutcomeExit code
Success0
Task failure only2
Unreachable host only4
Task failure and unreachable host4

Unreachable takes precedence. A CI gate that tests only for 2 treats a run containing both as a pass.

Step 2: Establish the scope

This single question halves the search space.

Read-only / Safehow wide is it
ansible all -m ping -o | tee reachability.txt
grep -c SUCCESS reachability.txt
grep -c UNREACHABLE reachability.txt
ScopeLook at
One hostThat host: sshd, disk, host key, account, its own firewall
One groupTheir shared component: bastion, subnet firewall, group-level connection vars
EverythingThe controller, not the fleet: DNS, key permissions, agent, routing, expired credential

The “everything” case is the one where instinct is least useful. Twenty hosts do not fail simultaneously; one controller does. Check the controller first and you will usually be done in two minutes.

Step 3: Resolve what Ansible is actually connecting to

Do not assume the inventory hostname is the connection target.

Read-only / Safewhat does inventory say
ansible-inventory -i inventories/production --host web02.example.com

Read ansible_host, ansible_user, ansible_port, ansible_ssh_common_args and ansible_ssh_private_key_file. A host whose ansible_host points at an address that was re-allocated last month fails in a way that looks exactly like a dead machine.

Read-only / Safewhat does ansible actually do
ansible web02.example.com -m ping -vvvv 2>&1 | grep -E 'ESTABLISH|SSH: EXEC|ansible_host'

-vvvv prints the literal ssh command line Ansible builds, including every option. That line is the ground truth, and it is what you should run by hand in the next step - not an approximation of it.

Step 4: Test the transport by hand, in layers

Read-only / Safelayer by layer
HOST=192.0.2.12

# 1. Name resolution - only if you are connecting by name
getent hosts web02.example.com

# 2. TCP reachability to the SSH port
timeout 5 bash -c "cat < /dev/null > /dev/tcp/$HOST/22" && echo 'tcp open' || echo 'tcp blocked'

# 3. SSH handshake and authentication, as the automation account
sudo -iu ansible ssh -vv -o BatchMode=yes -o ConnectTimeout=10 "ansible@$HOST" true

# 4. Remote interpreter
sudo -iu ansible ssh -o BatchMode=yes "ansible@$HOST" 'command -v python3; python3 -V'

Each layer fails differently, and the message tells you which one you are on:

MessageLayerMeaning
Name or service not knownDNSThe name does not resolve from the controller
Connection timed outTCPFiltered - firewall, security group, wrong address
Connection refusedTCPReached the host, nothing listening on that port
Host key verification failedSSHThe key changed, or was never trusted
Permission denied (publickey)AuthReached sshd, the key was rejected
/bin/sh: python3: not foundInterpreterConnected and authenticated; no remote Python

Verified message on 2.21.3 for a filtered address, using documentation-range hosts:

web01.example.com | UNREACHABLE!: Task failed: Failed to connect to the host
via ssh: ssh: connect to host 192.0.2.11 port 22: Connection timed out

BatchMode=yes in step 3 is important. Without it, SSH may fall back to a password prompt and you will conclude authentication works when what worked was you typing a password that automation does not have.

Step 5: Group-wide failures - check the shared thing

Read-only / Safebastion and shared path
# Is the bastion itself reachable?
ssh -o BatchMode=yes -o ConnectTimeout=10 ansible@bastion.example.com true

# What proxy configuration is in play?
ansible-inventory -i inventories/production --host web02.example.com \
| grep -iE 'proxy|common_args'

# Does the controller still have the key loaded?
sudo -iu ansible ssh-add -l

Step 6: Single-host failures - check the host

If TCP is open and SSH refuses, the host is up and something on it is wrong.

Read-only / Safefrom the console or another path
# Is sshd running and listening where you expect?
systemctl status ssh sshd 2>/dev/null
ss -lntp | grep ':22'

# Full disk breaks authentication in ways that look like a key problem
df -h / /var /home

# Is the automation account usable?
getent passwd ansible
passwd -S ansible

# Does sshd read keys from where you think?
sudo sshd -T -C user=ansible,host=web02.example.com,addr=192.0.2.12 \
| grep -iE 'authorizedkeysfile|authorizedkeyscommand|allowgroups|allowusers'

A full /home is a favourite: sshd cannot write to the account’s directory, key authentication fails, and the message is Permission denied (publickey) - which sends everyone to look at the key.

Step 7: Confirm the fix with something that connects

Read-only / Safeverify
ansible web02.example.com -m ping -o
echo "exit=$?"

# Then the whole group, and compare the count
ansible web -m ping -o | grep -c SUCCESS
ansible-playbook -i inventories/production site.yml --limit web --list-hosts \
| grep -c 'example.com'

Both counts must match. Verifying one host proves one host; the failure you are recovering from was probably not limited to it.

Step 8: Record which layer failed

The useful incident note is one line: which layer. “web02 was unreachable, cause: DNS record removed during the subnet migration” is something the next person can act on. “web02 was flaky, resolved itself” is how the same hour gets spent again next month.

Rollback

Diagnosis changes nothing. The fixes need cleaning up:

Emergency actionFollow-up
Host key checking disabledRe-enable it; verify and record the fingerprint properly
Key added by hand to restore accessReplace with the managed key; remove the manual one
Firewall rule openedNarrow it to the minimum, and declare it in configuration management
Inventory edited to a different addressFix the source of truth, not just the inventory file

Anything done under pressure is temporary until it exists in the repository. An estate accumulates these silently, and the next rebuild does not reproduce any of them.

Common patterns

SymptomLikely causeResolution
Everything unreachable at onceThe controller: DNS, agent, key permissions, routingCheck the controller before the fleet
One group unreachableBastion down, or a shared firewall rule changedTest the bastion directly
Permission denied (publickey) after nothing changedFull disk, or .ssh permissions altered by a roledf -h; stat the .ssh directory
Works by hand, fails from AnsibleDifferent user, key or port than you tested-vvvv and read the actual ssh command line
Works from Ansible, fails by handYou tested as yourself, Ansible uses the automation accountsudo -iu ansible ssh ...
Intermittent unreachable, no patternStale ControlPersist sockets, or a genuinely flapping pathClear ~/.ansible/cp/; then look at the network
Connected and authenticated, module fails immediatelyNo remote Python interpretercommand -v python3; bootstrap with raw
Reachability check passes, real runs failThe check used debug, which never connectsUse ping or setup

Escalation

Escalate when:

  • TCP fails from the controller and succeeds from elsewhere. That is a network path problem.
  • The host is up but sshd is not, or the disk is full. That is the host owner’s.
  • A host key changed and nobody can account for it. That is security’s, immediately.
  • The whole fleet went unreachable at once and the controller checks out.

References

  1. Connection methods and details
  2. ansible.builtin.ping module
  3. ssh(1)