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.
3.8 KiB
| title | domain | category | tags | status | created | updated | |||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Ansible Fails with Permission Denied While `ssh <alias>` Works (Host Alias Bypass) | selfhosting | 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):
- Check
ansible_hostin host_vars / inventory. If it's an IP, suspect alias bypass. - Run
ssh -vv user@<IP>(use the IP, not the alias) and look at theOffering public keylines. If the key from yourHostblock isn't listed, the block isn't being applied. - 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.