majorwiki/05-troubleshooting/ansible-ssh-host-alias-bypass.md
MajorLinux 2dbeb22ef9 wiki: add Ansible SSH Host Alias Bypass troubleshooting article
Documents why `ansible myhost -m ping` fails with Permission denied
while `ssh myhost` works — SSH Host blocks match on literal pattern,
not on resolved HostName, so `ansible_host: <IP>` bypasses the alias
and the declared IdentityFile never gets applied. Covers the portable
fix (ansible_ssh_private_key_file in host_vars), the symlink sidebar
for standardizing key names across control nodes, alternatives, and
a diagnosis checklist.

Also catches index.md up with the ansible-check-mode-false-positives
article that was already published but missing from the nav.
2026-04-21 09:15:22 -04:00

3.8 KiB

title domain category tags status created updated
Ansible Fails with Permission Denied While `ssh <alias>` Works (Host Alias Bypass) selfhosting troubleshooting
ansible
ssh
ssh-config
inventory
host-vars
authentication
troubleshooting
published 2026-04-21 2026-04-21

Ansible Fails with Permission Denied While ssh <alias> Works (Host Alias Bypass)

The Problem

Ansible can't connect to a host, but plain SSH to the same host works fine from the same shell:

$ ssh myhost
last login: ...

$ ansible myhost -m ping
myhost | UNREACHABLE! => {
    "msg": "Failed to connect to the host via ssh: user@10.0.0.42: Permission denied (publickey).",
    "unreachable": true
}

The host is online, the key works, the inventory defines the host — yet Ansible sees Permission denied (publickey).

Why It Happens

Ansible is connecting to the host by IP, and SSH doesn't apply Host alias blocks when you connect by IP.

Your ~/.ssh/config likely looks like this:

Host myhost
    HostName 10.0.0.42
    User myuser
    IdentityFile ~/.ssh/id_ed25519_custom

When you run ssh myhost, SSH matches the Host myhost block and uses the custom key.

When Ansible runs, its inventory specifies ansible_host: 10.0.0.42. SSH is invoked as ssh user@10.0.0.42 — and the Host directive matches on literal pattern, not on resolved HostName. The string 10.0.0.42 doesn't match the pattern myhost, so the block is skipped. SSH falls back to default identity files (id_rsa, id_ecdsa, id_ed25519) — none of which are the actual key — and authentication fails.

Running ssh -vv user@<IP> confirms the custom key is never offered.

The Fix

Declare the key path in inventory so Ansible passes it explicitly, independent of SSH config:

# host_vars/myhost/vars.yml
ansible_user: myuser
ansible_host: 10.0.0.42
ansible_ssh_private_key_file: ~/.ssh/id_ed25519

This is the portable fix — it lives in the inventory repo, so every control node that pulls the repo picks it up.

Caveat: if different control nodes store the same key under different filenames (e.g. id_ed25519 on one machine, id_ed25519_fleet on another), pick a single canonical path and standardize. The simplest way is to symlink on the outlier machines:

ln -s id_ed25519_fleet ~/.ssh/id_ed25519
ln -s id_ed25519_fleet.pub ~/.ssh/id_ed25519.pub

Then host_vars can declare one path and work everywhere.

Alternative Fixes (Less Portable)

Second Host block matching the IP. Add to ~/.ssh/config:

Host myhost 10.0.0.42
    HostName 10.0.0.42
    User myuser
    IdentityFile ~/.ssh/id_ed25519_custom

The Host line accepts multiple patterns. Works immediately but is per-machine — doesn't help other control nodes.

Change ansible_host to the alias instead of the IP. Requires DNS or /etc/hosts entry that resolves the alias fleet-wide. Works if you already have reliable name resolution; otherwise adds infrastructure for no real gain.

How to Diagnose This

If ssh <alias> works but Ansible fails with Permission denied (publickey):

  1. Check ansible_host in host_vars / inventory. If it's an IP, suspect alias bypass.
  2. Run ssh -vv user@<IP> (use the IP, not the alias) and look at the Offering public key lines. If the key from your Host block isn't listed, the block isn't being applied.
  3. Fix in inventory, not SSH config, if you want the result portable across control nodes.

Why This Gotcha Is Invisible

SSH doesn't log "Host block skipped because pattern didn't match." The key simply isn't offered, and the server responds with the same generic Permission denied (publickey) you'd see for any auth failure. The inventory and SSH config both look correct in isolation — it's the interaction between them that's broken.