Title: Authenticate the SSH servers you are connecting to
Author: Solène
Date: 05 August 2023
Tags: ssh security
Description: In this article, you will learn how to use SSHFP DNS
records in order to prevent TOFU when using SSH.

# Introduction

It's common knowledge that SSH connections are secure; however, they
always had a flaw: when you connect to a remote host for the first
time, how can you be sure it's the right one and not a tampered system?

SSH uses what we call TOFU (Trust On First Use), when you connect to a
remote server for the first time, you have a key fingerprint displayed,
and you are asked if you want to trust it or not.  Without any other
information, you can either blindly trust it or deny it and not
connect.  If you trust it, the key's fingerprint is stored locally in
the file `known_hosts`, and if the remote server offers you a different
key later, you will be warned and the connection will be forbidden
because the server may have been replaced by a malicious one.

Let's try an analogy.  It's a bit like if you only had a post-it with,
supposedly, your bank phone number on it, but you had no way to verify
if it was really your bank on that number.  This would be pretty bad. 
However, using an up-to-date trustable public reverse lookup directory,
you could check that the phone number is genuine before calling.

What we can do to improve the TOFU situation is to publish the server's
SSH fingerprint over DNS, so when you connect, SSH will try to fetch
the fingerprint if it exists and compare it with what the server is
offering.  This only works if the DNS server uses DNSSEC, which
guarantees the DNS answer hasn't been tampered with in the process. 
It's unlikely that someone would be able to simultaneously hijack your
SSH connection to a different server and also craft valid DNSSEC
replies.

# Setup

The setup is really simple, we need to gather the fingerprints of each
key (they exist in multiple different crypto) on a server, securely,
and publish them as SSHFP DNS entries.

If the server has new keys, you need to update its SSHFP entries.

We will use the tool `ssh-keygen` which contains a feature to
automatically generate the DNS records for the server on which the
command is running.

For example, on my server `interbus.perso.pw`, I will run `ssh-keygen
-r interbus.perso.pw.` to get the records

```
$ ssh-keygen -r interbus.perso.pw.
interbus.perso.pw. IN SSHFP 1 1 d93504fdcb5a67f09d263d6cbf1fcf59b55c5a03
interbus.perso.pw. IN SSHFP 1 2 1d677b3094170511297579836f5ef8d750dae8c481f464a0d2fb0943ad9f0430
interbus.perso.pw. IN SSHFP 3 1 98350f8a3c4a6d94c8974df82144913fd478efd8
interbus.perso.pw. IN SSHFP 3 2 ec67c81dd11f24f51da9560c53d7e3f21bf37b5436c3fd396ee7611cedf263c0
interbus.perso.pw. IN SSHFP 4 1 cb5039e2d4ece538ebb7517cc4a9bba3c253ef3b
interbus.perso.pw. IN SSHFP 4 2 adbcdfea2aee40345d1f28bc851158ed5a4b009f165ee6aa31cf6b6f62255612
```

You certainly noted I used an extra dot, this is because they will be
used as DNS records, so either:

* Use the full domain name with an extra dot to indicate you are not
giving a subdomain
* Use only the subdomain part, this would be `interbus` in the example

If you use `interbus.perso.pw` without the dot, this would be for the
domain `interbus.perso.pw.perso.pw` because it would be treated as a
subdomain.

Note that `-r arg` isn't used for anything but the raw text in the
output, this doesn't make `ssh-keygen` fetch the keys of a remote URL.

Now, just add each of the generated entries in your DNS.

# How to use SSHFP on your OpenSSH client

By default, if you connect to my server, you should see this output:

```
> ssh interbus.perso.pw
The authenticity of host 'interbus.perso.pw (46.23.92.114)' can't be established.
ED25519 key fingerprint is SHA256:rbzf6iruQDRdHyi8hRFY7VpLAJ8WXuaqMc9rb2IlVhI.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])? 
```

It's telling you the server isn't known in `known_hosts` yet, and you
have to trust it (or not, but you wouldn't connect).

However, with the option `VerifyHostKeyDNS` set to yes, the fingerprint
will automatically be accepted if the one offered is found in an SSHFP
entry.

As I explained earlier, this only works if the DNS answer is valid with
regard to DNSSEC, otherwise, the setting "VerifyHostKeyDNS"
automatically falls back to "ask", asking you to manually check the DNS
SSHFP found and if you want to accept or not.

For example, without a working DNSSEC, the output would look like this:

```
$ ssh -o VerifyHostKeyDNS=yes interbus.perso.pw
The authenticity of host 'interbus.perso.pw (46.23.92.114)' can't be established.
ED25519 key fingerprint is SHA256:rbzf6iruQDRdHyi8hRFY7VpLAJ8WXuaqMc9rb2IlVhI.
Matching host key fingerprint found in DNS.
This key is not known by any other names
Are you sure you want to continue connecting (yes/no/[fingerprint])?
```

With a working DNSSEC, you should immediately connect without any TOFU
prompt, and the host fingerprint won't be stored in `known_hosts`.

# Conclusion

SSHFP is a simple mechanism to build a chain of trust using an external
service to authenticate the server you are connecting to.  Another
method to authenticate a remote server would be to use an SSH
certificate, but I'll keep that one for later.

# Going further

We saw that VerifyHostKeyDNS is reliable, but doesn't save the
fingerprint in the file `~/.ssh/known_hosts`, which can be an issue if
you need to connect later to the same server if you don't have a
working DNSSEC resolver, you would have to trust blindly the server.

However, you could generate the required output from the server to be
used by the known_hosts when you have DNSSEC working, so next time, you
won't only rely on DNSSEC.

Note that if the server is replaced by another one and its SSHFP
records updated accordingly, this will ask you what to do if you have
the old keys in known_hosts.

To gather the fingerpints, connect on the remote server, which will be
`remote-server.local` in the example and add the command output to your
known_hosts file:

```
ssh-keyscan localhost 2>/dev/null | sed 's/^localhost/remote-server/'
```

We omit the `.local` in the `remote-server.local` hostname because it's
a subdomain of the DNS zone. (thanks Francisco Gaitán for spotting
it).

Basically, `ssh-keyscan` can remotely gather keys, but we want the
local keys of the server, then we need to modify its output to replace
localhost by the actual server name used to ssh into it.