Concept

SSH known_hosts: What It Does and How to Fix It

~/.ssh/known_hosts is where your SSH client records each server's host key. A mismatch warning means the key changed — verify it before deleting the entry.

CC Chen Chen· Founder·2026-09-09·11 min read

The short answer

~/.ssh/known_hosts is your SSH client’s private address book of server identities. The first time you connect to a machine, the client shows you the server’s host key fingerprint and asks whether you trust it. Say yes and it writes a line into that file: hostname, key type, public key. Every connection after that compares the key the server offers against the stored line.

Two things can go wrong, and they are not equally serious:

  • The authenticity of host ... can't be established — no entry exists yet. This is normal on a first connection. The correct response is to verify the fingerprint through some channel other than the connection you are about to trust, then accept.
  • WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! — an entry exists and does not match. Usually the server was rebuilt or its IP was recycled. Occasionally it means something is sitting between you and the server. Either way, do not delete the entry until you know which.

The safe fix is always the same shape: find out why the key changed, confirm the new fingerprint against the server itself, then remove only the stale entry with ssh-keygen -R hostname and reconnect. Everything below is the detail behind those three steps.

What the known_hosts file actually is

SSH gives you an encrypted channel. It does not, by itself, tell you whose channel it is. Encryption to an attacker who intercepted your connection is still perfectly good encryption. Host keys are the part that answers “who am I talking to”, and known_hosts is where the answer gets remembered.

Every SSH server generates a set of host keys at install time — typically Ed25519 and RSA, sometimes ECDSA — living in /etc/ssh/ as ssh_host_<type>_key and ssh_host_<type>_key.pub. During the handshake, the server proves it holds the private half. Your client’s job is to decide whether that public half is the one it saw last time.

Because there is no global registry of which key belongs to which server, SSH uses trust on first use: the first sighting establishes the baseline, and everything afterwards is checked against it. That model is only as strong as the first decision, which is why the initial prompt deserves more attention than it usually gets — the full argument for taking TOFU seriously is worth reading if you have been hitting Enter on it for years.

Your client reads more than one location:

PathWho owns itTypical use
~/.ssh/known_hostsYouHosts you accepted yourself; the only file the client writes to by default
~/.ssh/known_hosts2YouLegacy second file, still read for compatibility
/etc/ssh/ssh_known_hostsRootHost keys distributed to every user by an administrator

The system-wide file matters more than it looks. A host pre-seeded in /etc/ssh/ssh_known_hosts never produces a first-use prompt, because the fleet’s owner already made the decision for you. That is the grown-up version of host key management, and it is why configuration-managed environments rarely see these warnings at all.

Anatomy of a known_hosts line

Each line has up to five space-separated fields: an optional marker, the hostname patterns, the key type, the base64 key, and a comment. The format is documented in sshd(8) under SSH_KNOWN_HOSTS FILE FORMAT.

server.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
[build.example.com]:2222 ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
|1|F1E2...=|9A8B...= ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAAB...
@cert-authority *.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...
@revoked old.example.com ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI...

Five details in there trip people up:

Non-default ports get brackets. A host you reach on port 2222 is stored as [build.example.com]:2222, not build.example.com. If you changed the SSH port and then try to remove the entry by bare hostname, nothing happens and you conclude the file is broken. It isn’t; you are searching for the wrong string.

Lines beginning |1| are hashed. With HashKnownHosts enabled the client stores an HMAC of the hostname rather than the hostname, so the file no longer doubles as an inventory of every server you have access to. OpenSSH’s own default for this option is no, but the Debian openssh-client package — and therefore Ubuntu — ships /etc/ssh/ssh_config with HashKnownHosts yes, which is documented in Debian’s ssh_config manpage as one of its deliberate deviations from upstream. This single packaging decision explains why the file is readable on a Mac and unreadable on a Debian box, and why so many “just grep the file” instructions fail.

One host can occupy several lines. The client stores one entry per key type it has accepted. A server offering Ed25519 and RSA can therefore appear twice. This is why the warning names a specific line number — and why removing “the” entry by hand often leaves a second one behind.

@cert-authority changes the game entirely. That marker says the key on the line is a certificate authority, and any host presenting a valid certificate signed by it is trusted. One line covers a whole fleet, including machines that do not exist yet, and rebuilding a server no longer produces a warning as long as it gets a fresh certificate. If your organisation runs more than a handful of servers, this is the fix for known_hosts churn rather than a workaround for it.

@revoked is a blocklist. A key on a @revoked line is never accepted, which is how you retire a compromised host key without relying on everyone to clean their own file.

The two warnings, and why only one is scary

The first-contact prompt looks like this:

The authenticity of host 'server.example.com (203.0.113.10)' can't be established.
ED25519 key fingerprint is SHA256:pV8h2Q0k...
Are you sure you want to continue connecting (yes/no/[fingerprint])?

Nothing has gone wrong. Your client has no record of this host and is asking you to create one. Note the third option: you can paste the expected fingerprint instead of typing yes, and the client will only continue if it matches — much better than eyeballing a base64 string. Accepting prints the confirmation that the entry was written:

Warning: Permanently added 'server.example.com' (ED25519) to the list of known hosts.

The second message is a different category of event:

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@    WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!     @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
...
Offending ECDSA key in /home/you/.ssh/known_hosts:14
Host key verification failed.

Here the client had a record, the record didn’t match, and it refused rather than proceeding. The refusal is the feature working. Two things in that output are worth reading rather than scrolling past: the key type named in the “Offending” line, and the line number. Together they tell you exactly which entry to inspect — and if only one key type mismatches while others are fine, that alone is a clue about what happened. Our dedicated walkthrough of the host key verification failed error covers the triage in more depth.

Why a host key changes

Most mismatches are self-inflicted infrastructure changes. Working through them in rough order of likelihood:

  1. The server was rebuilt or reimaged. New install, new host keys. Ubiquitous in any environment with disposable instances.
  2. A cloud provider recycled the IP address. You are connecting to 203.0.113.10, and that address now belongs to somebody else’s virtual machine. The key mismatch is correct — it genuinely is a different computer.
  3. A restored snapshot or cloned image. Restoring an older backup can roll host keys back to a previous set. Cloning a VM template that shipped with baked-in host keys creates the opposite problem: several distinct servers presenting the same identity.
  4. The keys were regenerated deliberately. Someone ran ssh-keygen -A after a bad restore, or the distribution regenerated them during a major upgrade.
  5. A load balancer or proxy in front of port 22. Different backends, different keys, so the mismatch appears and disappears depending on which one you land on.
  6. You are behind a new middlebox. A captive portal, a corporate TLS-inspecting appliance that also intercepts SSH, or an actual attacker on the path.

The famous large-scale example of a legitimate rotation is GitHub replacing its RSA host key at around 05:00 UTC on 24 March 2023, after the private key was briefly exposed in a public repository. Every developer who had connected before that moment got the changed-identity warning on their next git push — and, per GitHub’s own write-up, users on Ed25519 or ECDSA were unaffected, because only the RSA key was rotated. It is a useful reference point in both directions: a real rotation that produced millions of scary warnings, and a case where the warning was doing exactly what it was designed to do.

How to fix it safely

The order matters. Verify first, delete second.

1. Get the real fingerprint from the server, through some other channel. Console access from your provider’s dashboard, an existing session on another machine, a colleague reading it out, or the record your configuration management system keeps. On the server:

ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

That prints the same SHA256:... string your client is complaining about, so you can compare directly. To see every host key the server offers:

for f in /etc/ssh/ssh_host_*_key.pub; do ssh-keygen -lf "$f"; done

The one thing that does not count as verification is asking the connection you already distrust. ssh-keyscan fetches keys over the network and is fine for bootstrapping hosts you have a separate reason to trust; it is not evidence that a mismatch is benign, because an interceptor answers ssh-keyscan just as happily as it answers ssh. If you have no out-of-band path at all and the server is unreachable by other means, treat it as an access problem to solve first rather than a warning to click through.

2. Look at the entry you are about to remove.

ssh-keygen -F server.example.com

-F searches the file — including hashed entries — and prints the matching line with its line number, or exits with a non-zero status if there is none. This works when grep doesn’t, which on Debian and Ubuntu is most of the time.

3. Remove just that host.

ssh-keygen -R server.example.com

-R deletes every entry for the host and writes the previous version of the file to ~/.ssh/known_hosts.old, per the ssh-keygen manual. Note the backup is a single slot, so running the command twice overwrites it — don’t treat it as version control. For a non-default port, quote the bracketed form:

ssh-keygen -R '[build.example.com]:2222'

4. Reconnect and confirm the fingerprint. You will get the first-contact prompt again. Paste the fingerprint you obtained in step 1 at the prompt instead of typing yes, so the match is checked rather than assumed.

What not to do: ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null is the incantation that circulates whenever someone is in a hurry. It works, in the sense that the error goes away permanently — along with the only mechanism that would have told you about an interception. Reserve it for throwaway lab machines whose traffic you would be happy to hand to a stranger.

The settings that change known_hosts behaviour

These all live in ~/.ssh/config or /etc/ssh/ssh_config; see our SSH config file guide for how the blocks are structured and matched.

OptionDefaultWhat it does
StrictHostKeyCheckingaskask prompts on new hosts and refuses changed ones; accept-new auto-accepts new but still refuses changed; no/off accepts both
UserKnownHostsFile~/.ssh/known_hosts ~/.ssh/known_hosts2Which files the client reads and writes
GlobalKnownHostsFile/etc/ssh/ssh_known_hostsAdministrator-managed hosts; read-only from the client’s perspective
HashKnownHostsno upstream, yes on Debian/UbuntuStore hostnames as HMACs
CheckHostIPno since OpenSSH 8.5Whether to also key entries by IP address
UpdateHostKeysyes, conditionallyLearn additional and replacement host keys from a server you already trust
VerifyHostKeyDNSnoCheck SSHFP records in DNS instead of prompting

Four of these deserve more than a table row.

accept-new is the value most people actually want. Added in OpenSSH 7.6, released 3 October 2017, it was introduced explicitly as “a safer subset of the current behaviour of StrictHostKeyChecking=no”: unseen hosts are accepted automatically, changed hosts are still refused. If you are disabling strict checking because a provisioning script chokes on the interactive prompt, this is the option to reach for instead — you keep the alarm that matters and lose only the one you were going to answer yes to anyway.

CheckHostIP used to be why you had duplicate entries. When enabled, the client stores and checks the host key against the IP address as well as the name, which produced two entries per server and a mismatch every time an address moved. OpenSSH 8.5, released 3 March 2021, turned it off by default on the grounds that it offered little benefit while making key rotation harder — particularly for hosts behind IP-based load balancers. If you still see paired name/IP entries in an old file, that is the archaeology.

UpdateHostKeys quietly does a lot of work. Also enabled by default in 8.5, it lets a server you have already authenticated hand over its other host keys, so it can retire an old algorithm without triggering a warning. The conditions in the ssh_config manual are strict — the host must have authenticated with a key you already trusted, from UserKnownHostsFile rather than the global file, using a plain key rather than a certificate. The non-obvious part is the enabling rule: it defaults to yes only if you have not overridden UserKnownHostsFile and have not enabled VerifyHostKeyDNS. Point UserKnownHostsFile somewhere custom and you silently lose automatic rotation, which is a genuinely surprising interaction between two options that look unrelated.

Order of preference is a separate thing. If you get no matching host key type rather than a mismatch, the problem is algorithm negotiation, not identity — that error has its own fix. Which key type ends up in your known_hosts depends on HostKeyAlgorithms and what the server offers; the Ed25519 versus RSA comparison covers why modern servers lead with Ed25519.

Where known_hosts lives on a phone

Mostly, it doesn’t. iOS and Android sandbox application storage, and mobile SSH clients generally keep accepted host keys in an internal database or the platform keystore rather than in a file at a path you can open, edit, or sync. There is no ~/.ssh/known_hosts to ssh-keygen -R.

That produces one consequence worth planning around, and it is not obvious until it bites you. Your phone’s trust store is separate from your laptop’s. A server you verified carefully at your desk two years ago is a brand-new first-use decision the first time you connect from your phone — and realistically, that first time is at 02:00 during an incident, on a train, when the honest answer to “is this fingerprint correct?” is “I have no idea”. The trust-on-first-use model assumes you are in a position to verify. Emergency access is precisely when you are not.

Two habits fix this cheaply:

  • Connect from the phone once while everything is calm, at your desk, with the fingerprint in front of you on the laptop. The pinning is done, and the phone is now a working emergency channel rather than a fingerprint quiz.
  • Keep fingerprints where the phone can read them — your password manager, your runbook, a note in your infrastructure repo. Verification is only possible if the reference value is reachable from the device asking the question.

TermAI pins the host key per saved connection and shows both the old and new fingerprint when one changes, so you can compare them on the spot rather than choosing between “accept” and “give up”. If you connect over a private network — TermAI has Tailscale built in — the tailnet gives you an independently authenticated path to the machine, but note it is a second layer rather than a substitute: SSH still checks its own host key on top, and it should. The broader question of what is and isn’t reasonable to do from a phone during an incident is covered in our honest risk breakdown of mobile SSH in production.

How to stop fighting known_hosts

If you are removing entries every week, the file is not the problem — the workflow is.

Use an SSH certificate authority. One @cert-authority line covering *.example.com replaces a per-host entry for every machine you own, present and future. Rebuilt servers get a fresh certificate from the CA and produce no warning at all, because the thing your client trusts is the CA, not the individual machine. This is the only approach that scales past a couple of dozen servers, and it turns host key rotation from an event into a non-event.

Distribute /etc/ssh/ssh_known_hosts with configuration management. Less elegant than a CA, and it needs re-running whenever a server is rebuilt, but it removes the first-use prompt from every user’s workflow and puts the trust decision where it belongs — with the people who own the fleet.

Name your hosts. Connecting by hostname rather than IP means a machine that moves address does not invalidate anything. Combined with CheckHostIP defaulting to no since 8.5, this eliminates a whole category of mismatch.

Don’t bake host keys into images. A VM template that ships with /etc/ssh/ssh_host_* already populated gives every clone the same identity, which is exactly the property SSH is trying to detect the absence of. Strip them from the image and let first boot generate a fresh set.

Keep the file readable if you can afford to. Hashing is a real defence against a stolen laptop revealing your server inventory, and it is the right default on a machine that leaves the building. But on a workstation that never moves, an unhashed file you can grep, review and diff is easier to keep honest. Decide deliberately rather than inheriting whatever your distribution chose.

FAQ

What is the known_hosts file in SSH?

It is the client-side record of which public host key belongs to which server. On the first connection your SSH client shows the server’s fingerprint and asks whether to trust it; when you answer yes, the client appends a line to ~/.ssh/known_hosts containing the hostname, the key type and the key itself. On every later connection the client compares the key the server presents against that stored line. If they match, the connection continues silently. If they differ, the client refuses to continue. The file is what turns SSH from “encrypted to someone” into “encrypted to the machine I met before”.

Is it safe to just delete known_hosts?

Deleting the whole file is safe for your data but throws away every identity you have ever verified, so the next connection to each server becomes a fresh trust-on-first-use decision that you will almost certainly click through. It also destroys the evidence: if one server’s key changed for a bad reason, wiping the file makes that warning disappear without answering why it appeared. Remove the single offending entry with ssh-keygen -R hostname instead, and only after you have checked the new fingerprint against the server.

How do I remove a single host from known_hosts?

Run ssh-keygen -R hostname. It finds every entry for that host — including hashed entries you cannot read by eye — removes them, and saves the previous file as ~/.ssh/known_hosts.old. Use the exact string you connect with: an entry stored for a non-default port looks like [server.example.com]:2222, so pass ssh-keygen -R '[server.example.com]:2222'. To see the entry before removing it, ssh-keygen -F hostname prints the matching line.

Why does known_hosts contain lines starting with |1|?

Those are hashed entries. When HashKnownHosts is enabled, the client stores an HMAC of the hostname instead of the hostname itself, so a stolen laptop does not hand an attacker a directory of every server you administer. The OpenSSH default for HashKnownHosts is no, but Debian and Ubuntu ship /etc/ssh/ssh_config with HashKnownHosts yes, which is why the file looks unreadable on those systems and normal elsewhere. The client can still match hashed entries; only humans and grep lose.

Does StrictHostKeyChecking=no fix host key errors?

It suppresses them, which is not the same thing. With no or off the client will add unknown keys and continue to hosts whose key has changed, so the one control that would notice an interception is switched off. If your goal is to avoid the interactive prompt on first connection in automation, use accept-new instead: it auto-accepts a host you have never seen but still refuses a host whose recorded key has changed. That value was added in OpenSSH 7.6.

Where is known_hosts on an iPhone or Android SSH app?

There usually isn’t one. iOS and Android apps are sandboxed and most mobile SSH clients keep known host keys in an app database or the platform keystore rather than a ~/.ssh/known_hosts file you can open. That has one practical consequence worth planning for: your phone and your laptop keep separate trust stores, so a server you verified at your desk is still a first-contact decision on your phone — usually while something is on fire, which is the worst moment to be judging a fingerprint. Record fingerprints somewhere you can reach from the phone before you need them.

Try TermAI

Free on iOS and Android. 5 AI requests/day on the free tier, plus unlimited SSH/SFTP and built-in Tailscale.

CC
Chen Chen — Founder of TermAI

Writes about mobile DevOps, terminal UX, and the surprising depth of "boring" infrastructure.