WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED: Fix
When OpenSSH prints WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! and then Host key verification failed., your client has stopped trusting the server it is talking to — the host key presented on this connection does not match the one pinned in ~/.ssh/known_hosts. The reflexive fix you will find in most threads — delete the line, or worse, delete the whole file — is exactly wrong for the one case that matters: an actual man-in-the-middle attack. This post is the triage I run before I clear anything, the exact ssh-keygen commands, and the fleet-level setup that makes the warning rare in the first place. All commands below were executed and verified on an OpenSSH 10.0p2 client; the warning text is quoted from the OpenSSH portable source (sshconnect.c / sshconnect2.c, current master).
I hit this on a Tuesday, on a box I manage through a bastion, and the first Google hit told me to run rm ~/.ssh/known_hosts. That advice is how you quietly accept a new attacker-controlled key. Before you delete anything, you need to know whether the key changed because the server was legitimately rebuilt — or because something between you and the server now owns the connection.
TL;DR
- The warning is SSH's trust-on-first-use (TOFU) model working as designed. Your client stores the server's host key the first time you connect; every later connection must present the same key. A mismatch means either the server's keys changed or someone is intercepting the connection.
- Never clear the warning before you know which one it is. Legitimate causes: cloud instance rebuilt, Docker container recreated (fresh keys in the container), OS reinstall, or an IP that now belongs to a different machine. Attack cause: a MITM on the path.
- Verify the new fingerprint out of band first. Get the key the server sends now (
ssh-keyscan), compare its fingerprint to the one your provider, admin, or official docs publish — from a channel other than the SSH connection itself. - The fix is
ssh-keygen -R <host>— it removes only that host's entries (and keeps a.oldbackup). Then reconnect and accept the new key only after your out-of-band check passed. - The permanent cure is automation: SSHFP/DNSSEC records, OpenSSH's
UpdateHostKeys, or centrally managedknown_hosts— so a key change is announced and verified, not discovered as a wall of@signs at 9 AM.
What the warning is actually telling you
The banner is generated by the client, not the server. OpenSSH compares the host key the server sent during key exchange against what is stored in your known_hosts file; on a mismatch the client prints this (text quoted from sshconnect.c, warn_changed_key()):
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
Someone could be eavesdropping on you right now (man-in-the-middle attack)!
It is also possible that a host key has just been changed.
The next line prints the fingerprint of the key the server sent on this connection — that value is the whole ballgame, and it is the one line you must verify before doing anything else. The client then closes with the path and line of the offending entry, and exits 255:
Please contact your system administrator.
Add correct host key in /home/kai/.ssh/known_hosts to get rid of this message.
Offending ECDSA key in /home/kai/.ssh/known_hosts:12
Host key verification failed.
Read the last two lines carefully. Offending ECDSA key in ...:12 tells you the type of key that no longer matches and the exact line number. If the entry is line 12 and the server now presents an ED25519 key you have never stored, that is a normal rotation story: a rebuilt server often keeps its RSA key but changes its ED25519/ECDSA keys, or an old client stored only the ECDSA key from a server that now prefers ED25519. OpenSSH stores one entry per key type per host, so a partial rotation trips the check even when the server is completely legitimate.
Two related client messages are worth knowing so you do not confuse them. For a brand-new host with no entry at all, OpenSSH prints The authenticity of host 'host (ip)' can't be established and asks you to confirm the fingerprint — that is the happy path. And if the connection dies after the key check, you are in a different failure class entirely: I covered that server-side story in ssh_exchange_identification: Connection closed by host. This warning fires before authentication, so your password and keys never enter the picture — which is exactly why a MITM wants you to ignore it.
First triage: key rotation or man-in-the-middle?
Do not touch known_hosts yet. Answer one question first: did anything about that host legitimately change since the last successful connection? Work the table top to bottom:
| Situation | Most likely cause | Safe move |
|---|---|---|
| Cloud instance was stopped and rebuilt, or an autoscaler replaced it | Provider reimaged the disk; new host keys generated at first boot | Verify fingerprint against the provider console / metadata API, then ssh-keygen -R |
| Docker container was recreated (compose up, deploy) | Container /etc/ssh is ephemeral; every recreate rolls fresh keys | Mount a volume for host keys, or verify and -R after each deploy |
| OS reinstalled, server cloned, or bare metal replaced | New key material from the fresh install | Verify with whoever owns the box, then -R |
| A floating IP / NAT rule moved to a different machine | You are now talking to a different server on the same address | Fingerprint check decides: expected server = OK; anything else = stop |
| Nothing changed. Same VM, same container, same IP, same week | Possible MITM — rogue device, compromised router, malicious proxy | Do not clear the warning. Verify out of band, inspect the path, involve the owner |
The tell is in the last row. The warning is rare on a stable fleet: if a host you have connected to every day for months suddenly presents a different key and nobody rotated anything, treat it as a security incident until proven otherwise. That is not paranoia — it is the exact scenario the message's SOMEONE IS DOING SOMETHING NASTY! line exists for, and the reason the fix is a deliberate two-step, not a delete.
Verify the new key out of band before clearing anything
Out of band means: not over the same connection you are trying to fix. You want the server's real fingerprint from a channel an attacker on your network path cannot forge — the cloud provider's console, the vendor's published fingerprint page, a colleague on a different network, or DNS (SSHFP, covered below).
First, fetch the key the server presents right now and compute its fingerprint locally. Here is the workflow, executed against GitHub's real public host so you can see exactly what the output should look like:
# 1. Fetch the host key(s) the server currently presents
ssh-keyscan -t ed25519 github.com 2>/dev/null
# github.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIOMqqnkVzrm0SdG6UOoqKLsabgH5C9okWi0dh2l9GKJl
# 2. Fingerprint it locally — never trust a fingerprint shown over the same link
ssh-keyscan -t ed25519 github.com 2>/dev/null | ssh-keygen -lf -
# 256 SHA256:+DiY3wvvV6TuJJhbpZisF/zLDA0zPMSvHdkr4UvCOqU github.com (ED25519)
GitHub publishes its host fingerprints at GitHub's SSH key fingerprints; the ED25519 value shown above matches the official page byte for byte. That is the whole ritual: keyscan → fingerprint → compare against a source you trust for other reasons. If the value matches what your admin or provider says the server should present, the key change is legitimate and you can clear the stale entry. If it matches nothing, or matches a fingerprint you have never seen documented anywhere, stop and investigate the path before you reconnect. For internal hosts, the same pattern applies to your CMDB, your inventory, or the ssh-keyscan output a colleague fetches from inside the network — the same method I use when fingerprinting infrastructure for audits (see the Nmap "requires root privileges" fix for the scanning side of that work).
The fix: remove the stale entry with ssh-keygen -R
Once you have verified the new key out of band, clear only that host's entries. ssh-keygen -R is the surgical tool; it removes every key type stored for the hostname, works on hashed known_hosts files, and writes a .old backup before touching anything:
# Remove all stored keys for this host
ssh-keygen -R bastion.c2cz.example
# If you keep known_hosts somewhere non-default (CI, scripts, alternate users)
ssh-keygen -R bastion.c2cz.example -f /etc/ssh/ssh_known_hosts
# Verify with the search command: -F prints nothing and exits 1 when the
# host has no entry. (-R itself exits 0 even if the host was not found.)
ssh-keygen -F bastion.c2cz.example
Real output from the verification run on OpenSSH 10.0p2, using GitHub's key so the mechanics are visible:
$ ssh-keygen -R github.com -f kh_gh
# Host github.com found: line 2
kh_gh updated.
Original contents retained as kh_gh.old
$ ssh-keygen -F github.com -f kh_gh
$ echo $?
1
Notice the behavior worth knowing: -R found the entry at line 2, removed it, and left kh_gh.old as a rollback copy. The -F search afterwards prints nothing and exits 1, which is how scripts check for the presence or absence of a host key — and it is the only reliable removal check, because -R itself exits 0 even when it finds no entry (it prints Host ... not found in ... and returns success). If you only want to remove one key type — say the ECDSA entry at line 12 while keeping the ED25519 entry — -R is too blunt; delete that single line in a text editor instead, or back up the file and use -R followed by re-adding only the key type you want to keep.
Before you discard the old entry, one forensic step when you have the previous key available (a .old backup, your CMDB, a colleague's known_hosts): confirm the offending entry really is the key you originally pinned. ssh-keygen -F HOST prints the stored key and ssh-keygen -lf fingerprints it. If the stored entry is not the key you remember trusting, your known_hosts itself may have been modified — treat that as an incident, not a rotation.
Re-add the new host key safely
With the stale entry gone, reconnect. The clean way is to let the normal TOFU prompt show you the new fingerprint and confirm it matches what you verified above. The wording below is what an OpenSSH 10.x client prints on first contact (captured verbatim on 10.0p2; the host, IP and fingerprint are examples — never a value to accept):
ssh kai@bastion.c2cz.example
# The authenticity of host 'bastion.c2cz.example (203.0.113.10)' can't be established.
# ED25519 key fingerprint is SHA256:<the value you verified out of band>.
# This key is not known by any other names.
# Are you sure you want to continue connecting (yes/no/[fingerprint])?
Type yes only if the fingerprint matches your out-of-band value. That comparison is the entire security control: ssh-keyscan will happily return whatever key an attacker on the path presents, so pinning a key that you have not matched to an independent source defeats the check. For unattended re-adds — containers, CI, provisioning scripts — pin the key explicitly, and only after the fingerprint matched:
# ONLY after you matched this key's fingerprint to your out-of-band value:
ssh-keyscan -t ed25519 bastion.c2cz.example >> ~/.ssh/known_hosts
# Verify what you just appended (exit 0 = entry present)
ssh-keygen -F bastion.c2cz.example
ssh-keyscan appends a plaintext or hashed entry (with -H) and never touches existing lines, so it is safe to run repeatedly. Two pitfalls: first, default ssh-keyscan without -t grabs every key type the host offers, which is fine but noisy — prefer -t ed25519 (or add ecdsa if the server still uses it) to keep the file small. Second, if you use StrictHostKeyChecking=accept-new in automation, remember it only helps for hosts with no entry; it does nothing for a changed key, and it silently accepts a first-contact MITM. Reserve accept-new for throwaway environments and keep ask (the default) for anything real.
Fleet-grade prevention: SSHFP, UpdateHostKeys, and managed known_hosts
On a fleet, the goal is that a legitimate key change never surfaces as a scary wall of @ signs, and an illegitimate one never gets silently accepted. Three mechanisms get you there.
1. Publish SSHFP records and enable VerifyHostKeyDNS. SSHFP is a DNS record type (RFC 4255) that carries the host key fingerprint; a DNSSEC-signed zone makes it a trustworthy out-of-band channel. Generate the records from the host's keys, then add them to the zone:
# On the server: print SSHFP records for the host's own key
ssh-keygen -r bastion.c2cz.example -f /etc/ssh/ssh_host_ed25519_key.pub
# Sample output — real records generated from a scratch key to show the format;
# your host's hashes will differ and must come from your own key, never this post:
# bastion.c2cz.example IN SSHFP 4 1 f553873b3aac913791720db3cb768f63bf453831
# bastion.c2cz.example IN SSHFP 4 2 97a98a47923c83e516fb157e9b5f2b5b99f652fe44309c407c75f1b3de3ee2c8
# On the client: consult SSHFP when no known_hosts entry exists
# /etc/ssh/ssh_config.d/sshfp.conf
Host *.c2cz.example
VerifyHostKeyDNS ask
VerifyHostKeyDNS only protects you if the zone is DNSSEC-validated — on a plain, unsigned zone an attacker who can spoof DNS can spoof the SSHFP answer too. That is why the ask value matters: OpenSSH still shows you the fingerprint and lets you confirm before trusting DNS, so a compromised resolver cannot silently inject a key.
2. Let OpenSSH roll keys forward with UpdateHostKeys. Since OpenSSH 7.6 (release notes, 2017), the client can learn additional keys for a host during a session whose key is already trusted, which makes multi-key rotation painless. The server announces its other keys, the client stores them, and after a rotate you are not locked out:
# /etc/ssh/ssh_config.d/update-host-keys.conf
Host *.c2cz.example
UpdateHostKeys ask
With ask, the client shows you the additional keys and asks before storing them — do that once per host during a known-good connection, and future rotations of one key type stop tripping the warning for the others. Leave it at ask; yes accepts additional keys without prompting, which weakens the model. (A subtlety from the OpenSSH source: the client only learns keys whose types it already trusts, so this cannot be used to sneak in a new key type.)
3. Manage known_hosts centrally instead of per-laptop. For more than a few hosts, the answer is not teaching engineers to answer yes. Generate the file once from inventory and distribute it read-only, or verify against your config-management state on every run:
# Generate a fleet known_hosts on the control node — one keyscan, all hosts:
ssh-keyscan -t ed25519 bastion.c2cz.example web1.c2cz.example db1.c2cz.example \
> /etc/ssh/ssh_known_hosts
# If you parse Ansible ad-hoc output instead, keep only real key lines —
# the callback banners ("host | SUCCESS | rc=0 >>") must never reach the file:
ansible all -m shell -a "ssh-keyscan -t ed25519 {{ inventory_hostname }}" \
| awk '/ ssh-ed25519 /' > /etc/ssh/ssh_known_hosts
# In Dockerfiles / CI, pin before you connect — never disable checking
RUN ssh-keyscan -t ed25519 git.c2cz.example >> /etc/ssh/ssh_known_hosts \
&& git clone git@git.c2cz.example:infra/deploy.git
Two operational notes from running this in production. First, Docker is the most common source of this warning in 2026: a compose recreate regenerates container host keys unless you mount them, so either persist /etc/ssh host keys with a volume or treat every deploy as a key rotation and script the -R + keyscan pair. Second, keep a host's old key entries around during a deliberate migration — connect with the old key still trusted, then let UpdateHostKeys ask learn the new one, then remove the old entry. That ordering is what separates a planned rotation from a Monday-morning incident.
What not to do when you see this warning
- Do not run
rm ~/.ssh/known_hosts. It deletes every pin on the machine — including the ones protecting you right now — and you will silently accept whatever key the next connection offers. The.oldbackup thatssh-keygen -Rleaves is there precisely so you never need a sledgehammer. - Do not add
-o StrictHostKeyChecking=noto "fix" it. That flag does not fix anything; it disables the protection for that connection. It belongs in throwaway labs and nowhere that touches production. - Do not clear the warning because the connection is urgent. The five minutes of out-of-band verification above is cheaper than a compromised bastion. If someone is actively intercepting you, every subsequent password and key you type goes through them.
- Do not ignore the
Offending ... key in ...:Nline. It names the exact entry — line 12 of your known_hosts — so the fix is one targeted command, not a file-wide edit. A file-wide edit is how you accidentally delete the entry for the one host that was never the problem.
Verification checklist
- Fingerprint confirmed out of band:
ssh-keyscan -t ed25519 HOST | ssh-keygen -lf -matches the value from your provider console, vendor docs, or DNSSEC-validated SSHFP — not just what the server told you. - Stale entry removed:
ssh-keygen -R HOSTreports the removal and leavesknown_hosts.old;ssh-keygen -F HOSTprints nothing and exits 1. - New key pinned: reconnect shows the TOFU prompt with the verified fingerprint (or
ssh-keyscan ... >> known_hostsfor unattended hosts) and the session proceeds to authentication. - No warning on the next connection: a second
ssh HOSTgoes straight to password/key auth — if the banner reappears, the server's key is changing between connections, which is a different and much more serious problem (see my Tailscale SSL_ERROR_RX_RECORD_TOO_LONG analysis for the same "who is actually answering" logic applied to HTTPS). - Fleet hygiene: container host keys are volume-mounted or rotated deliberately, SSHFP/UpdateHostKeys are configured, and no script on the fleet uses
StrictHostKeyChecking=no.
The whole discipline fits in one sentence: SSH's host-key warning is a question — "do you still trust this server?" — and ssh-keygen -R is only the answer after you have verified the new key through a channel the attacker does not control. Verify first, then remove, then reconnect. That order turns a scary wall of @ signs from a panic into a two-minute procedure.