UPDATE: Corrected the paragraph about the permissions of the AuthorizedKeys file.
If you read this blog on a regular basis, you probably use the little tool called SSH, especially its ubiquitous and most popular implementation OpenSSH.
Maybe you’re savvy enough to only use it with public/private keys, and therefore protect yourself from dictionary attacks. If you do then you know that in order to configure access to a new host, you need to make a copy of a public key available to that host (usually by writing it to its disk). Managing keys can be painful if you have many hosts, especially when you need to renew one of the keys. What if DNSSEC could help?
CC BY 2.0 image by William Neuheisel
With version 6.2 of OpenSSH came a feature that allows the remote host to retrieve a public key in a customised way, instead of the typical authorized_keys
file in the ~/.ssh/
directory. For example, you can gather the keys of a group of users that require access to a number of machines on a single server (for example, an LDAP server), and have all the hosts query that server when they need the public key of the user attempting to log in. This saves a lot of editing of authorized_keys
files on each and every host. The downside is that it's necessary to trust the source these hosts retrieve public keys from. An LDAP server on a private network is probably trustworthy (when looked after properly) but for hosts running in the cloud, that’s not really practical.
DNSSEC is helpful here. That's right: now that we can verify responses from a DNS server, we can safely store public keys in DNS records!
So let's say we administer example.com
and want to give Alice and Bob access to machines foo
, bar
and baz
in that domain. We'll store their respective public keys in TXT[1] records named alice_pubkey.example.com
and bob_pubkey.example.com
. To be entirely accurate, it doesn’t really matter which zone these records belong to, but I’ll consider here that we only have one domain. The requirements are:
the machines need to run OpenSSH server version 6.2 or later
they also need a DNSSEC validating resolver (we'll use
unbound-host
)Alice and Bob's keys need to be less than 256 characters long (ECDSA or Ed25519 keys will work)
DNSSEC needs to be correctly set up on the domain
example.com
(surprise!)
Alice and Bob generate keys like this:
foo:~$ ssh-keygen -t ecdsa
or like this:
foo:~$ ssh-keygen -t ed25519
and then follow the instructions. They will of course provide a non-empty passphrase. Then they send us (or whoever administers the zone file for example.com
) the public key file, which may look like this:
ssh-ed25519 AAAAC3N...VY4A= alice@foo
We can strip the comment alice@foo
out of that file, and use the rest as the value to create a TXT record with the name alice_pubkey
in the domain example.com
. Then, retrieving the key is as easy as this:
foo:~$ unbound-host -t TXT alice_pubkey.example.com
alice_pubkey.example.com has TXT record “ssh-ed25519 AAAAC3N…”
With -v
, unbound-host will show us whether the signature has been verified
foo:~$ unbound-host -v -t TXT alice_pubkey.example.com
alice_pubkey.example.com has TXT record “ssh-ed25519 AAAAC…” (insecure)
With -D
, it will actually check the signature:
foo:~$ unbound-host -D -v -t TXT alice_pubkey.example.com
alice_pubkey.example.com has TXT record “ssh-ed25519 AAAAC3N…” (secure)
If no record exists, it will show this:
foo:~$ unbound-host -D -v -t TXT charlie_pubkey.example.com
charlie_pubkey.example.com has no TXT record (secure)
Note that the absence of record is also labelled “secure”, thanks to NSEC.
Let’s prepare to parse that output. The sshd_config man page shows that sshd needs a specific user to run the program that will retrieve public keys. This is following the best practices of privilege separation. Let's call that user pubkeygrab
and create an account on foo
, bar
and baz
, giving it just the permissions it needs to work and nothing more:
foo:~$ useradd -m -d /var/empty -s /sbin/nologin pubkeygrab
Then create the script pubkeygrab.sh
, and store it on each of the machines. Obviously, we'll make sure only root can edit it:
foo:~$ cat /usr/local/bin/pubkeygrab.sh
#!/bin/sh
USER=$1
/usr/sbin/unbound-host -v -D -t TXT ${USER}_pubkey.example.com \\
| /usr/bin/grep -v "no TXT record" \\
| /usr/bin/grep ' (secure)$' \\
| /usr/bin/sed 's/.* "\(.*\)" (secure)$/\1/'
Now I'm certain that a lot of readers will have something to say about the style or the efficiency of this shell script, I just wrote it that way to highlight what steps need to be taken:
it retrieves a TXT record, and doesn't output anything if the record doesn't exist
if
unbound-host
has not confirmed that the record was correctly DNSSEC signed, it doesn't output anythingif the above is successful, it filters out the text to return only the public key
it doesn't try to do anything complex, because complexity is the enemy of security (or at least, that’s a point of view that I share with a few people)
it works with multiple records
I'm sure you will write your own program to do the above. Just make sure it works only when you want it to. It is critical to ensure that it doesn't return anything at least when:
a record for the corresponding user doesn't exist
the records are not signed or not properly signed
the local copy of the root key (
/var/unbound/root.key
, here) is corrupted.
Bonus points to you if you find more cases.
Now that you have read the warning above, add the following to /etc/ssh/sshd_config
on foo
, bar
and baz
:
AuthorizedKeysCommand /usr/local/bin/pubkeygrab.sh
AuthorizedKeysCommandUser pubkeygrab
and restart sshd
. Check that the users alice
and bob
exist on each machine too. Note that the above change will also apply to all existing users. Now you can go to your CloudFlare account, select the domain example.com
, and create the TXT records alice_pubkey
and bob_pubkey
. Paste their respective public keys in the value field. Soon after, Alice and Bob can log in. Ask Charlie to try too. If the above works for Alice and Bob, but fails for Charlie, congratulations, you have turned CloudFlare into a PKI for SSH.
If you remove the TXT records, Alice and Bob’s access should be revoked, and they should be unable to login, once the TTL of the TXT record is expired. However, note that when the output of pubkeygrab.sh
is empty, sshd
reverts to the usual AuthorizedKeysFile
parameter to find a public key. If Alice and Bob are cheeky and want to keep their access after you removed their TXT records, they just need to copy their public key into that file any time before you ban them. If you don't want that, make sure the AuthorizedKeysFile
parameter points to a place Alice and Bob can't write to.
I hope this is showing how interesting DNSSEC can be, and that we have more news on this topic soon.
Yes, it would be better to have a dedicated record, instead of overloading TXT records. ↩︎