Skip to main content
RunBook Academy

AnsibleXLV · Debugging and TroubleshootingDebugging and Troubleshooting

Diagnosing transport failures

Advanced⏱ ~26 minansible-playbookssh

What you'll learn

  • Separate an Ansible problem from an SSH problem by reproducing outside Ansible first
  • Read the SSH: EXEC line to see the exact connection Ansible attempted
  • State the default values of ssh_args, timeout and the persistent timeouts
  • Diagnose an agent-forwarding difference between a workstation and a controller

Prerequisites

Verified against ansible-core 2.21.x · ansible (community package) 14.x · Python (controller) 3.12+ · ansible-lint 26.x · Molecule 26.x · Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL / Rocky / AlmaLinux 9.x · 2026-08-11

Not yet marked complete on this device.

There is one habit in this part that resolves more problems than everything else combined, and it takes thirty seconds:

Before you change anything in Ansible, reproduce the failure with plain ssh.

Ansible’s ssh connection plugin is a wrapper around the ssh command line. If ssh cannot connect, Ansible cannot connect, and no amount of inventory variables, ansible.cfg edits or retry loops will change that. Half the time spent on “Ansible connection problems” is spent debugging Ansible for a fault that lives entirely in SSH configuration, DNS, firewalling or credentials.

The two-step separation

Step 1: get the exact command Ansible ran

Run the failing task at -vvv and read the SSH: EXEC line. It is the literal command, with every option Ansible assembled.

Read-only / Safethe connection Ansible actually attempted — executed on ansible-core 2.21.3
$ ansible-playbook -i inv.ini one.yml -vvv
<192.0.2.11> ESTABLISH SSH CONNECTION FOR USER: None
<192.0.2.11> SSH: EXEC ssh -C -o ControlMaster=auto -o ControlPersist=60s
-o KbdInteractiveAuthentication=no -o PreferredAuthentications=gssapi-with-mic,
gssapi-keyex,hostbased,publickey -o PasswordAuthentication=no -o ConnectTimeout=10
-o 'ControlPath="/home/ops/.ansible/cp/a87e32107b"' -o NumberOfPasswordPrompts=1
192.0.2.11 '/bin/sh -c '"'"'echo ~'"'"''
<192.0.2.11> (255, b'', b'ssh: connect to host 192.0.2.11 port 22: Connection
timed out')

Four things are readable directly from that line and each is a common cause of a surprise:

  • FOR USER: None — no ansible_user and no remote_user is set, so ssh will use whatever the controller user’s SSH config says. That is a different user on a CI runner than on your laptop.
  • ConnectTimeout=10 — the Ansible timeout setting, default 10, handed to ssh.
  • ControlPath=... — the multiplexing socket. A stale one here produces failures that look like network faults.
  • The target is an address, not a name — because ansible_host was set. If you expected a name to be resolved, this is where you find out it was not.

Step 2: run it yourself, verbosely

Read-only / Safereproduce outside Ansible
ssh -vvv -o ConnectTimeout=10 ops@app-047.example.com true

# If that works and Ansible does not, add Ansible's options one at a time
ssh -vvv -C -o ControlMaster=auto -o ControlPersist=60s \
-o PreferredAuthentications=gssapi-with-mic,gssapi-keyex,hostbased,publickey \
-o PasswordAuthentication=no \
ops@app-047.example.com true

Getting SSH debug output from inside Ansible

When you cannot easily reproduce by hand — the credential is only on the controller, or the failure only happens under load — ask the connection plugin for it. Note that this is a separate option from the Ansible verbosity level, and defaults to 0:

Read-only / Safessh CLI debugging, requested explicitly — executed on 2.21.3
$ ANSIBLE_SSH_VERBOSITY=3 ansible-playbook -i inv.ini one.yml -vvv
<192.0.2.11> SSH: EXEC ssh -vvv -C -o ControlMaster=auto ...
<192.0.2.11> (255, b'', b'debug1: OpenSSH_10.2p1, OpenSSL 3.5.5
debug1: Reading configuration data /etc/ssh/ssh_config
debug2: resolve_canonicalize: hostname 192.0.2.11 is address
debug1: auto-mux: Trying existing master at "/home/ops/.ansible/cp/a87e32107b"
debug1: Control socket does not exist
debug1: Connecting to 192.0.2.11 [192.0.2.11] port 22.
debug1: connect to address 192.0.2.11 port 22: Connection timed out')

Set it as an inventory variable on the affected host or group (ansible_ssh_verbosity=3) rather than globally. Forty lines per connection attempt across 300 hosts is a log nobody reads.

The settings that matter, with their defaults

Verified against ansible-core 2.21.3 with ansible-doc and ansible-config list:

SettingDefaultWhat it governs
ssh_args-C -o ControlMaster=auto -o ControlPersist=60sPassed to every SSH CLI tool. Compression and multiplexing
ssh_common_argsemptyExtra args, appended to the defaults
timeout (DEFAULT_TIMEOUT)10Becomes ConnectTimeout on the ssh command line
PERSISTENT_COMMAND_TIMEOUT30Wait for a response over a persistent connection
PERSISTENT_CONNECT_TIMEOUT30How long a persistent connection stays idle before destruction
HOST_KEY_CHECKINGtrueWhether the underlying plugin verifies host keys
ansible_ssh_verbosity0Verbosity passed to the ssh CLI itself

Host key verification

HOST_KEY_CHECKING defaults to true, and the frequent advice to set it false is advice to disable a security control that exists to detect exactly the situation you are probably in.

A host key mismatch has three explanations: the host was rebuilt, you are connecting to a different machine than you think, or someone is intercepting the connection. The first is routine and the third is an incident, and turning off checking makes them indistinguishable permanently.

Read-only / Saferesolving a host key change deliberately
# What do we have recorded?
ssh-keygen -F app-047.example.com

# What is the host presenting now?
ssh-keyscan -t ed25519 app-047.example.com 2>/dev/null | ssh-keygen -lf -

# Compare with the key recorded at build time, from the build system,
# not from the host you are currently talking to.

The last comment is the whole discipline. Accepting a new key because the host presented it is not verification; it is agreement. The ansible.builtin.known_hosts module exists so that key distribution can be a managed process rather than a prompt, and the SSH part of this course covers it in full.

The laptop-versus-controller difference is usually the agent

A play works from an engineer’s workstation and fails from the controller. The difference is almost always one of three things, and the first is by far the most common.

Agent forwarding. The workstation has a loaded ssh-agent and the key never leaves it. The controller has no agent, or a different one, or a systemd service with no SSH_AUTH_SOCK in its environment.

Read-only / Safeprove what the agent has, from the machine that is failing
echo "SSH_AUTH_SOCK=${SSH_AUTH_SOCK:-none}"
ssh-add -l || echo 'no agent, or no identities loaded'
ssh -vvv -o IdentitiesOnly=yes -i ~/.ssh/id_ed25519 ops@app-047.example.com true \
2>&1 | grep -E 'Offering|Authentications that can continue|Server accepts'

A different user. FOR USER: None in the SSH: EXEC line means the user comes from the controller’s SSH config or the login name, which differs between a person’s laptop and a service account.

A different ansible.cfg. Covered in the previous lesson — ansible-config dump --only-changed on both machines, and check the first line for CONFIG_FILE() = None.

Knowledge check

Knowledge check · 4 questions

  1. Q1. A task fails with a connection error against one host. What should you do first?

  2. Q2. Setting ssh_args to add an option removes the ControlMaster and ControlPersist defaults, while ssh_common_args appends to them.

  3. Q3. A run with forks: 50 through a bastion leaves a different random subset of hosts unreachable on every attempt. What is the most likely cause?

  4. Q4. A play works from an engineer laptop and fails from the controller. Which explanations should be checked first? Select all that apply.

Passing score: 75%. Answers are checked in this browser.