AnsibleXLV · Debugging and TroubleshootingDebugging and Troubleshooting
Diagnosing transport failures
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
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.
$ 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— noansible_userand noremote_useris set, sosshwill 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 Ansibletimeoutsetting, default10, handed tossh.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_hostwas set. If you expected a name to be resolved, this is where you find out it was not.
Step 2: run it yourself, verbosely
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 trueGetting 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:
$ 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:
| Setting | Default | What it governs |
|---|---|---|
ssh_args | -C -o ControlMaster=auto -o ControlPersist=60s | Passed to every SSH CLI tool. Compression and multiplexing |
ssh_common_args | empty | Extra args, appended to the defaults |
timeout (DEFAULT_TIMEOUT) | 10 | Becomes ConnectTimeout on the ssh command line |
PERSISTENT_COMMAND_TIMEOUT | 30 | Wait for a response over a persistent connection |
PERSISTENT_CONNECT_TIMEOUT | 30 | How long a persistent connection stays idle before destruction |
HOST_KEY_CHECKING | true | Whether the underlying plugin verifies host keys |
ansible_ssh_verbosity | 0 | Verbosity 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.
# 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.
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
Q1. A task fails with a connection error against one host. What should you do first?
Q2. Setting ssh_args to add an option removes the ControlMaster and ControlPersist defaults, while ssh_common_args appends to them.
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?
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.