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.
103 lines
3.8 KiB
Markdown
103 lines
3.8 KiB
Markdown
---
|
|
title: "Ansible Fails with Permission Denied While `ssh <alias>` 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 <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:
|
|
|
|
```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 <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.
|