--- title: "Ansible Fails with Permission Denied While `ssh ` Works (Host Alias Bypass)" domain: selfhosting category: troubleshooting tags: - ansible - ssh - ssh-config - inventory - host-vars - authentication - troubleshooting status: published created: 2026-04-21 updated: 2026-04-21 --- # Ansible Fails with Permission Denied While `ssh ` 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@` confirms the custom key is never offered. ## The Fix **Declare the key path in inventory** so Ansible passes it explicitly, independent of SSH config: ```yaml # 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 ` 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@` (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.