| 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. |