Ansible Error Guide: 'Permission denied (publickey,password)' — Fix SSH Auth Failures
Fix Ansible's 'Permission denied (publickey,password)' SSH failure by correcting the remote user, key path, agent, and ansible_user vars so plays authenticate.
- #ansible
- #automation
- #troubleshooting
- #errors
Fixing errors like this? Get 500 free DevOps AI prompts
500 copy-paste AI prompts for the stack you actually run — one PDF, free.
Overview
Ansible reports this when the SSH transport cannot authenticate to a managed host, so the host is marked UNREACHABLE before any task runs:
fatal: [web01]: UNREACHABLE! => {"changed": false, "msg": "Failed to connect to the host via ssh: user@web01: Permission denied (publickey,password).", "unreachable": true}
The parenthetical (publickey,password) lists the authentication methods the server offered, and the message means every method Ansible tried was rejected. This is a connection-layer failure, distinct from a task failure or a sudo password problem — the play never reached the point of running a module.
Symptoms
- Hosts are marked
UNREACHABLE!withPermission denied (publickey,password)and zero tasks run. - A manual
ssh user@hostfrom the control node also fails or prompts unexpectedly. - The same play works for one host/group but fails for another with a different user or key.
- It works interactively but fails under a systemd timer or CI where no SSH agent is loaded.
-vvvvshows the offered keys being rejected one by one before the failure.
Common Root Causes
- Wrong remote user —
ansible_user(orremote_user/-u) does not match an account that exists on the target. - Key not offered or wrong key — the private key Ansible uses is not the pair for the authorized key on the host, or no
ansible_ssh_private_key_fileis set and no agent is loaded. - SSH agent not available in automation — interactive shells load an agent; systemd/CI runners do not, so the key is never offered.
- Public key not installed — the corresponding public key is missing from the target’s
~/.ssh/authorized_keys. - Permissions on the key or authorized_keys — a private key with loose permissions is ignored by SSH; a group-writable
~/.sshis rejected by the server. - Password auth disabled — the server offers only
publickey, so a password-based expectation fails outright.
Diagnostic Workflow
Reproduce the failure with a raw connectivity test and full SSH verbosity, which shows exactly which user and key are tried:
ansible web01 -i inventory/hosts -m ping -vvvv
Test the same connection with plain SSH to separate Ansible from the transport:
ssh -v -i ~/.ssh/id_ed25519 deploy@web01
Confirm what user and key Ansible thinks it should use for that host:
ansible-inventory -i inventory/hosts --host web01 | grep -i 'ansible_user\|private_key'
Check the local key and agent state:
ssh-add -l # is a key loaded in the agent?
ls -l ~/.ssh/id_ed25519 # must be 600 and owned by you
A minimal inventory that sets the connection explicitly, removing ambiguity:
# inventory/hosts.yml
all:
hosts:
web01:
ansible_host: 10.0.0.11
ansible_user: deploy
ansible_ssh_private_key_file: ~/.ssh/deploy_ed25519
ansible web01 -i inventory/hosts.yml -m ping
# web01 | SUCCESS => {"ping": "pong"}
Example Root Cause Analysis
A play ran fine from an engineer’s laptop but every host came back Permission denied (publickey,password) when the same play ran from a systemd timer. ansible web01 -m ping -vvvv from the timer’s environment showed no key being offered at all. The laptop had an SSH agent holding the deploy key; the timer ran under a service account with no agent and no ansible_ssh_private_key_file set, so Ansible had no key to present and the server, which had password auth disabled, rejected the connection.
The fix was to set ansible_ssh_private_key_file explicitly in group_vars so the key no longer depended on an agent being loaded, and to ensure the service account owned that key file with 600 permissions. A ping under the exact service-account environment confirmed the connection before the timer was re-enabled. The lesson: never rely on an interactive SSH agent for unattended runs — pin the key file.
Prevention Best Practices
- Set
ansible_userandansible_ssh_private_key_fileexplicitly in inventory/group_vars instead of relying on an agent or SSH client defaults. - Keep private keys at
600and~/.sshat700; SSH silently ignores over-permissive keys. - Test unattended runs in the actual service-account environment, not just an interactive shell, so a missing agent surfaces before production.
- Standardize the remote user across a fleet, or set it per-group, so one play does not assume a user that only some hosts have.
- Use
ssh -valongsideansible ... -vvvvto isolate transport problems from Ansible configuration.
Quick Command Reference
# Raw connectivity test with full SSH detail
ansible <host> -i inventory -m ping -vvvv
# Plain SSH to isolate the transport
ssh -v -i ~/.ssh/<key> <user>@<host>
# See the resolved user/key for a host
ansible-inventory -i inventory --host <host>
# Check the local agent and key permissions
ssh-add -l
ls -l ~/.ssh/<key>
Conclusion
Permission denied (publickey,password) is an SSH authentication failure, not an Ansible bug: the server rejected every credential offered. Pin the remote user and private-key file explicitly in inventory, fix key and directory permissions, and always test unattended runs in the environment they will actually run in. Once a raw ansible -m ping succeeds, the play will connect — the transport is the whole problem to solve here.
More Ansible prompts & error guides
Every Ansible AI prompt and troubleshooting guide, in one place.
Get 500 Battle-Tested DevOps AI Prompts — Free
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.