SSHFP: SSH host key fingerprint
The TOFU replacement for SSH at scale. Instead of trusting the host key on first connect (and hoping nothing's tampering with it), publish the host's key fingerprint in DNS, sign it with DNSSEC, and let clients verify.
| Type number | 44 |
| RFC | 4255 (base) · 6594 (ECDSA, SHA-256) · 7479 (Ed25519) |
| RDATA | algorithm · fingerprint type · hex fingerprint |
| Required for trust | DNSSEC: without it, SSHFP is no better than TOFU |
What it holds#
Three fields:
| Field | Codes |
|---|---|
| Algorithm | 1 = RSA · 2 = DSA (disabled by default since OpenSSH 7.0, removed in OpenSSH 10.0) · 3 = ECDSA · 4 = Ed25519 · 6 = Ed448 |
| Fingerprint type | 1 = SHA-1 (deprecated) · 2 = SHA-256 |
| Fingerprint | Hex string of the hash |
Code assignments live in the IANA DNS SSHFP registry.
bastion.example.com. SSHFP 4 2 9f8b76e2...e1d4Fields: algorithm (4 = Ed25519), fingerprint type (2 = SHA-256), hex fingerprint.
A host typically publishes one SSHFP per key type (RSA + Ed25519 + ECDSA, three records). The client computes the fingerprint of the key the server offered and compares.
When to use it#
- Servers that need automated trust (CI, build agents, ephemeral instances) without operators clicking through "the authenticity of host can't be established."
- Any environment that runs DNSSEC end-to-end and wants stronger-than-TOFU SSH.
- Multi-host or fleet SSH: propagating known-hosts files manually does not scale; SSHFP via DNSSEC does.
When not to use it#
- If you do not run DNSSEC. SSHFP without DNSSEC is less secure than TOFU because it gives clients a false sense of authority.
- For single-host setups with one operator: known-hosts is fine.
Client setup#
openssh has supported SSHFP since 3.8, but you must enable it (the default is no):
# ~/.ssh/config
Host *
VerifyHostKeyDNS yesWhen the SSHFP answer arrives DNSSEC-validated, ssh trusts a matching key and skips the TOFU prompt entirely. If the record matches but is not validated, ssh treats it as VerifyHostKeyDNS ask: it prints Matching host key fingerprint found in DNS. and still shows the standard host-key prompt (ssh_config(5)).
Generating fingerprints#
On the SSH server, generate the records for every host key. -r <name> tells ssh-keygen to emit SSHFP resource records for that host name. Without -f, ssh-keygen reads the default host keys in /etc/ssh and prints SSHFP records for all of them; add -f to target a specific key file. Run once per key file:
ssh-keygen -r bastion.example.com -f /etc/ssh/ssh_host_ed25519_key.pub
ssh-keygen -r bastion.example.com -f /etc/ssh/ssh_host_rsa_key.pubThe output is zone-file format ready to paste into your zone. Fields: name, IN, SSHFP, algorithm, fingerprint-type, hex fingerprint:
bastion.example.com IN SSHFP 4 1 deadbeef...
bastion.example.com IN SSHFP 4 2 badcafe0...ssh-keygen emits two lines per key, SHA-1 (type 1) and SHA-256 (type 2). Publish only the type 2 lines unless you must support very old clients.
Querying SSHFP records#
The demo zone publishes a live SSHFP at sshfp.example.isitdns.net (Ed25519 / SHA-256). Query it with dig:
dig @1.1.1.1 sshfp.example.isitdns.net SSHFP +short4 2 0766E147A76CEA6A5F9F098742FDF4E6229977DC84291F9D528990E5 BE4A3A2DThe fields: algorithm 4 (Ed25519), fingerprint type 2 (SHA-256), then the hex fingerprint.
nslookup retrieves SSHFP but does not decode the fields by name; it prints the rdata under a generic rdata_44 label:
nslookup -type=SSHFP sshfp.example.isitdns.net 1.1.1.1Server: 1.1.1.1
Address: 1.1.1.1#53
Non-authoritative answer:
sshfp.example.isitdns.net rdata_44 = 4 2 0766E147A76CEA6A5F9F098742FDF4E6229977DC84291F9D528990E5 BE4A3A2D
Authoritative answers can be found from:nslookup cannot decode SSHFP field names (algorithm, fingerprint type) or display the DNSSEC AD bit. Use dig for anything beyond confirming the record exists.
Verifying DNSSEC validation#
isitdns.net is fully DNSSEC-signed and anchored (DS keytag 2371 at .net). A validating resolver returns AD=true for SSHFP records in this zone. Verify with DoH:
curl -s -H 'accept: application/dns-json' \
'https://cloudflare-dns.com/dns-query?name=sshfp.example.isitdns.net&type=SSHFP' \
| jq '{AD: .AD, answer: .Answer}'{
"AD": true,
"answer": [
{
"name": "sshfp.example.isitdns.net",
"type": 44,
"TTL": 300,
"data": "\\# 34 04 02 07 66 e1 47 a7 6c ea 6a 5f 9f 09 87 42 fd f4 e6 22 99 77 dc 84 29 1f 9d 52 89 90 e5 be 4a 3a 2d"
}
]
}The AD: true flag means the record passed DNSSEC validation end-to-end. The data field is in RFC 3597 generic wire format: 34 bytes, one algorithm byte (04), one fingerprint-type byte (02), then the 32-byte SHA-256 hash. dig presents the same data in the human-readable 4 2 0766... form. The TTL reads 300 when fresh and counts down when the answer comes from cache.
nslookup cannot show the AD bit. Use dig or DoH to confirm DNSSEC validation.
Gotchas#
- SHA-1 (type 1) is deprecated. Publish SHA-256 (type 2) only, except when supporting very old clients.
- DNSSEC is the whole point. Without a validated DNSSEC chain,
VerifyHostKeyDNS yescannot succeed. If you do not sign your zone, SSHFP is theatre. - ssh trusts the resolver's AD bit, not its own validation. OpenSSH does not validate DNSSEC itself; it believes the AD flag returned by the resolver in
/etc/resolv.conf. That flag travels unauthenticated, so the path to the resolver must be trusted, in practice a validating resolver on localhost. RFC 4255 section 2.4 requires exactly this: clients that do not validate themselves must have a secure channel to whatever does. - Host key rotation drives churn. Every host-key change requires updating SSHFP. Tooling (Ansible role, server provisioning script) should own the lifecycle.
- Multiple keys per host. Most hosts have RSA + Ed25519 + ECDSA. Publish SSHFP for each; the client matches on whichever key the server offers.
- Wildcards do not make sense. SSHFP is per-host.
*.example.com SSHFPis not meaningful: the fingerprint is specific to one host key, not a wildcard name.