Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for Ansible By James Joyner IV · · 10 min read

Ansible Error Guide: 'UNREACHABLE!' Failed to connect to the host via ssh

Fix Ansible's UNREACHABLE! Failed to connect to the host via ssh error: diagnose SSH auth, host key prompts, wrong user/port, DNS, and unreachable inventory hosts.

  • #ansible
  • #troubleshooting
  • #errors
  • #ssh

Overview

UNREACHABLE! is Ansible’s way of saying it never reached the remote host at all — the connection plugin (almost always SSH) failed before any module could run. Unlike a task FAILED!, an unreachable host means the transport layer broke: SSH could not authenticate, could not open a TCP connection, or the host key was rejected. Ansible records the host as unreachable and, by default, drops it from the rest of the play.

You will see this at the top of a play, usually on the implicit gather_facts / setup step:

fatal: [web-01]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: ssh: connect to host 10.0.4.21 port 22: Connection refused", "unreachable": true}

The actual cause is in the text after Failed to connect to the host via ssh:Connection refused, Permission denied (publickey), Host key verification failed, No route to host, and Operation timed out are all completely different problems wearing the same UNREACHABLE! headline.

Symptoms

  • Tasks never run; the host fails on the first step (Gathering Facts).
  • Play recap shows unreachable=1 for the host.
  • ansible -m ping <host> fails the same way.
  • Other hosts in the same play succeed, isolating it to one host or one credential.
ansible web-01 -i inventory.ini -m ping -vvv
web-01 | UNREACHABLE! => {
    "changed": false,
    "msg": "Failed to connect to the host via ssh: ssh: connect to host 10.0.4.21 port 22: Connection timed out",
    "unreachable": true
}

Common Root Causes

1. SSH key/authentication is rejected

The remote user does not accept the key (or password) Ansible is offering. The tell-tale message is Permission denied (publickey).

ansible web-01 -i inventory.ini -m ping -vvv 2>&1 | grep -i "permission denied"
"msg": "Failed to connect to the host via ssh: ... Permission denied (publickey,password)."

Confirm the same key works manually with the exact user Ansible uses:

ssh -i ~/.ssh/id_ed25519 deploy@10.0.4.21 hostname

If the manual SSH also fails, the problem is the key/user on the server, not Ansible.

2. Wrong remote user or SSH port

Inventory or defaults point at a user/port the host does not use. By default Ansible connects as the local user on port 22.

grep -E 'ansible_user|ansible_port|ansible_host' inventory.ini
[web]
web-01 ansible_host=10.0.4.21 ansible_user=ubuntu ansible_port=2222

If the host actually listens on 22 and the user is deploy, every connection is Connection refused on 2222 or Permission denied for ubuntu.

3. Host key verification failed

A changed or unknown host key blocks the connection when host_key_checking is on (the default).

ansible web-01 -i inventory.ini -m ping 2>&1 | grep -i "host key"
"msg": "Failed to connect to the host via ssh: Host key verification failed."

This is common after re-imaging a host that keeps the same IP. The old key in ~/.ssh/known_hosts no longer matches.

4. Host is genuinely down or firewalled (refused / timeout)

Connection refused means the TCP port answered with a reset (sshd not running, wrong port). Connection timed out / No route to host means packets never got there (firewall, security group, host down).

nc -zv 10.0.4.21 22
ping -c 2 10.0.4.21
nc: connect to 10.0.4.21 port 22 (tcp) failed: Connection refused

5. DNS resolution failure

The inventory uses a hostname that does not resolve from the control node.

ansible web-01 -i inventory.ini -m ping 2>&1 | grep -i "resolve"
"msg": "Failed to connect to the host via ssh: ssh: Could not resolve hostname web-01.internal: Name or service not known"
getent hosts web-01.internal

(No output means the name does not resolve — fix DNS or use ansible_host with an IP.)

6. Privilege/control-path or too-many-auth-failures

A stale SSH ControlPersist socket or an agent offering too many keys can trip Too many authentication failures before the right key is tried.

ansible web-01 -i inventory.ini -m ping -vvv 2>&1 | grep -i "authentication failures"
Received disconnect from 10.0.4.21 port 22:2: Too many authentication failures

Pin the identity so SSH stops offering every agent key:

ansible web-01 -i inventory.ini -m ping \
  --ssh-common-args='-o IdentitiesOnly=yes -o IdentityFile=~/.ssh/id_ed25519'

Diagnostic Workflow

Step 1: Read the exact SSH error, not just UNREACHABLE!

ansible web-01 -i inventory.ini -m ping -vvv 2>&1 | grep -i "Failed to connect"

The substring after the colon (Connection refused, Permission denied, Host key verification failed, Could not resolve, timed out) tells you which branch to follow.

Step 2: Reproduce with raw SSH using the same parameters

ansible-inventory -i inventory.ini --host web-01
{
    "ansible_host": "10.0.4.21",
    "ansible_user": "deploy",
    "ansible_port": 22
}

Then connect manually with those exact values:

ssh -p 22 deploy@10.0.4.21 -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 hostname

If raw SSH fails identically, the fix is on the host/network, not in Ansible.

Step 3: Test reachability and the port

ping -c 2 10.0.4.21
nc -zv 10.0.4.21 22

refused points to sshd/port; timed out / No route points to firewall, security group, or a down host.

Step 4: Resolve host key issues

ssh-keygen -F 10.0.4.21          # show the stored key entry
ssh-keygen -R 10.0.4.21          # remove a stale entry after re-imaging

For first-run automation against trusted hosts, you can disable checking for the run:

ANSIBLE_HOST_KEY_CHECKING=False ansible web-01 -i inventory.ini -m ping

Step 5: Fix inventory/connection variables and re-test

Set the correct user, port, and key in inventory or group_vars, then confirm with ping:

ansible web-01 -i inventory.ini -m ping
web-01 | SUCCESS => {"changed": false, "ping": "pong"}

Example Root Cause Analysis

A nightly playbook starts failing for every host in the db group with UNREACHABLE!, while the web group is fine.

The verbose error is consistent:

fatal: [db-02]: UNREACHABLE! => {"msg": "Failed to connect to the host via ssh: ... Permission denied (publickey)."}

Raw SSH as the play’s user reproduces it:

ssh deploy@10.0.6.12 hostname
deploy@10.0.6.12: Permission denied (publickey).

Checking inventory shows the db group overrides the user:

ansible-inventory -i inventory.ini --host db-02 | grep ansible_user
"ansible_user": "postgres"

The db group_vars set ansible_user: postgres, but the deploy key is only authorized for deploy. Someone moved the SSH key rotation onto deploy and never updated postgres’s authorized_keys. The wrong user is the root cause — Ansible authenticated as postgres, which has no matching key.

Fix: align the group to the user that actually holds the key, then re-test:

# remove the stale db group_vars override, or push the key to postgres
ansible db -i inventory.ini -m ping

All db hosts return pong.

Prevention Best Practices

  • Run ansible all -m ping as a smoke test after any credential, image, or network change — it isolates UNREACHABLE before a real play does.
  • Pin connection settings explicitly in inventory/group_vars (ansible_user, ansible_port, ansible_ssh_private_key_file) so a host’s identity is never inherited by accident.
  • Use IdentitiesOnly=yes in ansible.cfg ssh_args to avoid Too many authentication failures from a loaded agent.
  • Manage known_hosts deliberately: ship it from config management or run a controlled ssh-keyscan, rather than leaving operators to type “yes” or disabling host key checking globally.
  • Alert on hosts that appear in the play recap with unreachable>0 so a dead node is caught before the next run. The free incident assistant can turn a wall of UNREACHABLE output into the likely transport cause.
  • For deeper SSH and inventory patterns, see more in the Ansible guides.

Quick Command Reference

# See the real SSH error behind UNREACHABLE!
ansible <host> -i inventory.ini -m ping -vvv 2>&1 | grep -i "Failed to connect"

# What connection params is Ansible actually using?
ansible-inventory -i inventory.ini --host <host>

# Reproduce with raw SSH
ssh -p <port> <user>@<ip> -o IdentitiesOnly=yes -i <key> hostname

# Reachability / port checks
ping -c 2 <ip>
nc -zv <ip> 22

# Host key fixes
ssh-keygen -F <ip>
ssh-keygen -R <ip>
ANSIBLE_HOST_KEY_CHECKING=False ansible <host> -i inventory.ini -m ping

# Pin the identity for a run
ansible <host> -i inventory.ini -m ping \
  --ssh-common-args='-o IdentitiesOnly=yes -o IdentityFile=~/.ssh/id_ed25519'

Conclusion

UNREACHABLE! is a transport failure, not a task failure — Ansible never reached the host. The real cause hides in the text after Failed to connect to the host via ssh:. The usual root causes:

  1. The key or password is rejected (Permission denied (publickey)).
  2. The wrong ansible_user or ansible_port is in effect.
  3. A stale/changed host key triggers Host key verification failed.
  4. The host is down or firewalled (Connection refused / timed out).
  5. The inventory hostname does not resolve from the control node.
  6. SSH agent or ControlPersist issues cause Too many authentication failures.

Always read the verbose error and reproduce with raw SSH using the exact inventory values — that one step tells you whether to fix the credential, the inventory, the host key, or the network.

Free download · 368-page PDF

Download the Free 500-Prompt DevOps AI Toolkit

500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.

  • 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
  • Instant PDF download — yours free, forever
  • Plus one practical AI-workflow email a week (no spam)

Single opt-in · unsubscribe anytime · no spam.