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 1d677b3094170511297579836f5ef8d750dae8c481f464a… | |
interbus.perso.pw. IN SSHFP 3 1 98350f8a3c4a6d94c8974df82144913fd478efd8 | |
interbus.perso.pw. IN SSHFP 3 2 ec67c81dd11f24f51da9560c53d7e3f21bf37b5436c3fd3… | |
interbus.perso.pw. IN SSHFP 4 1 cb5039e2d4ece538ebb7517cc4a9bba3c253ef3b | |
interbus.perso.pw. IN SSHFP 4 2 adbcdfea2aee40345d1f28bc851158ed5a4b009f165ee6a… | |
``` | |
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 establishe… | |
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 establishe… | |
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. |