Setting up DNSSEC + DANE ( + SSHFP )
Assuming you’ve been convinced that it’s a good idea to set up DNSSEC and DANE, the point of this article is to demonstrate how I did it for my own domain - the individual steps to get from nothing to valid DANE records weren’t very difficult; just not documented in a recipe-style guide anywhere. Hopefully, this will help you get set up. I’m using Debian Squeeze or Wheezy throughout, depending on host, but the instructions should be similar for most Linux distributions.
This is the part that provides the hierarchical trust model, enabling a random user of your site to trust (more or less, anyway) that when they ask for a record that tells them which certificates are valid for their site, they get the same record that you’re going to upload later.
Firstly, the user needs to be able to make DNSSEC-validatable DNS queries to begin with. This requires that their caching (also known as resolving) nameserver supports DNSSEC queries. This is easy enough to test:
lupine@den:~$ dig +dnssec mozilla.org ; <<>> DiG 9.8.4-rpz2+rl005.12-P1 <<>> +dnssec mozilla.org ;; global options: +cmd ;; Got answer: ;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 25143 ;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 4, ADDITIONAL: 7 ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags: do; udp: 4096 ;; QUESTION SECTION: ;mozilla.org. IN A ;; ANSWER SECTION: mozilla.org. 60 IN A 18.104.22.168 mozilla.org. 60 IN RRSIG A 7 2 60 20131013124658 20130913125405 17933 mozilla.org. k2LOpTkl35qIPmFKVQix87mItL2ycPFTymx0yoZoIt+jpsGhEbQWgiiV FXndEwOKap/RsXdHtzWWWI4vcDdQgES0X/XInAxRKTadceapQ34Nyb0w TN9CpYidxpI35MY9cseZVu9eCKXq0M7VxpSBKSHshby2A/hymJntq1lD sSI= mozilla.org. 60 IN RRSIG A 7 2 60 20131013125201 20130913125405 63920 mozilla.org. N/dNbs71T0oEAJ0ulqeVPg4ty7UwG02QKOFr3tRy0kDpnRsPvIKX8E0e lVxCU/TCEckfS8QQv3JytoOrIwKt/Y1lOI//NuxLIZT8RndMvWaROkrt Ncs3moQAsD6w0sT+Yn7wx1AimVO4udQ8dh3lyYCKHdRq8VfxyK6/5Lws tzQ= ;; AUTHORITY SECTION: mozilla.org. 60 IN NS ns2.mozilla.org. mozilla.org. 60 IN NS ns1.mozilla.org. mozilla.org. 60 IN RRSIG NS 7 2 60 20131013125024 20130913125405 17933 mozilla.org. MlltXDEKazn80b3mMqGSOhCCqeQhuiIsgMXI+kaAABnwXyxzHsli+BEL f1AC3Grog3p9DLtRUPbAm3RWIF6HWgd5gJJ5rcw+50ihWVEwQceWniKD Sl/13G7V8pKR0P4GZjpTg//Go4H6xYZAThhU544zjxis5ytupM+rAW0I +ho= mozilla.org. 60 IN RRSIG NS 7 2 60 20131013125355 20130913125405 63920 mozilla.org. KnOTFZRq6f3K6wbfa6YMjVROHc6kr+RzvthX531H7AQjejB0yAc6ttyI q9J3u/cDg2sdsmROJ91JXkmU7Kjq+LJKrRedQPwY0xLr57ODK/87D3Kv Z9icf5HxarvdN4FlPb7j/uI8EIN4jKXb08976KtPu7BT+6o+1b+rwUWf Ccc= ;; ADDITIONAL SECTION: ns1.mozilla.org. 60 IN A 22.214.171.124 ns2.mozilla.org. 60 IN A 126.96.36.199 ns1.mozilla.org. 60 IN RRSIG A 7 3 60 20131013124618 20130913125405 63920 mozilla.org. e1mdvK7ERSuaNIxSf1O+8vyFJWoGBGGPSFt20KLiF+KBU1siDlywTTBr /UT5cNBB4prqcZ0DdFagnmWE2OploEqof0Nl/IiSPwVGy8eGksGmS0Qf zK78emWv4nQmVkiVokcZqIHiAXPxG9ZafJaTo/BGtnThILmatdnk2xuI JdY= ns1.mozilla.org. 60 IN RRSIG A 7 3 60 20131013125230 20130913125405 17933 mozilla.org. 1wWdtXpmOk9oOwzl8j8Jvz2IyqfVXIMfB9kDRC0AUKQNvUDk85Xp6AfE 2i4vaupFRa5RTKKj4gBTYRqfObhdrJHLNIRx1BMb/mb/B/8IF0HuxXeU IlGU8Wu/GbDHOHrS42Z3i2w9Y+DVUI1JQQlPHapDtD20kzKnClIN9iSa FRo= ns2.mozilla.org. 60 IN RRSIG A 7 3 60 20131013125059 20130913125405 17933 mozilla.org. WcnS3dw6gQ6gM5dP6tKGK+Gwkd3u8AMco2WCU3WzLoK0ADeJo9qjYGzd pSnJLRRMfiKBeWZJvm6g89sS+gPQh1IlncPp6AaGQdAAyl+OtwIswA/n qPQLlWBdJQrfAnzLKDXbOjTH2K9vXxNSUyAL5QzUgLIAB16oTvREbL42 bIc= ns2.mozilla.org. 60 IN RRSIG A 7 3 60 20131013125237 20130913125405 63920 mozilla.org. V2xTFK6cG9v+mBKbZP7a5yXFJUaXKAt1qOP0VmHWrP1n5lNfvcOMrKLc g4vpaxdbA0M1B7xMhX4ps2IYljAUZdzkBCMXp+bYKPKXdkxKRmXsnspF 7Fii5N9q7FKyhLEbsW8G9MRTScE0ohu5s8db6hOGmkcbyvZJmk5+R1Qd aAk= ;; Query time: 285 msec ;; SERVER: 188.8.131.52#53(184.108.40.206) ;; WHEN: Sat Sep 14 16:54:58 2013 ;; MSG SIZE rcvd: 1492 lupine@den:~$
If you see RRSIG records, as above, then you don’t need to do anything. If you don’t, then your resolver doesn’t support DNSSEC. This is fairly common. As a first resort, ask your provider (normally your ISP) to fix it. If that doens’t bear fruit, or if you’re impatient, you can install and use the Unbound resolver.
I was in the latter situation, and my router happens to run a hacked-up version of Debian Squeeze, so I installed Unbound on it and configured the DHCP server to refer to it when configuring clients; so every machine on my home network now has access to a DNSSEC-capable resolver. You can also install and use it locally, which might look like this:
root@den:~# apt-get install unbound # unbound-anchor # for wheezy root@den:~# echo "nameserver 127.0.0.1" > /etc/resolv.conf root@den:~# chattr +i /etc/resolv.conf
The resolv.conf file can be managed and altered in a number of ways - I can’t
actually recommend altering it to point to the Unbound instance you just
installed and making it immutable. If your desktop environment manages DHCP
for you, then you should investigate options for providing the DNS manually.
Debian also has the
resolveconf package which would allow you to specify
static fragments to go into resolv.conf. If you’re old-fashioned and are
using static configuation + /etc/network/interfaces, then the dns-nameservers
directive will let you specify 127.0.0.1 - your local Unbound instance.
** Browser (and other application) support
Now that you can get DNSSEC records from your resolver, through means fair or foul, you need client application support. Firefox has a plugin or two that also support DANE; the equivalent Chrome plugin only supports DNSSEC. Internet Explorer is probably Right Out, and I have no idea about Opera, Safari, and the rest. Another option is to install the Bloodhound browser. Apparently.
Web browsers aren’t the only applications that could make use of DNSSEC and DANE, of course. Mail and XMPP are two other important protocols; Thunderbird has no DNSSEC plugin at the moment, as far as I’m aware, and neither does Gajim or Pidgin. Let me know if you’re aware of any replacements that do - there’s obviously work to be done when it comes to client support. The more servers support DNSSEC, the more pressure there is on client applications to support it, of course. For now, open this web page on your DNSSEC-capable browser and ensure that the DNSSEC plugin is happy.
Now that you’ve got a client environment that can handle DNSSEC records, it’s time to look at getting your own domain DNSSEC-signed. I’ll be using lupine.me.uk as an example throughout; you need to pick (or register) a domain from a DNSSEC-supporting registry, and you should ensure that it’s with a registrar that allows you to upload so-called DNSKEY records to that registry. For me, the answers were “.me.uk” (now “.gs”) and “gandi” - they may be different for you.
Once you’ve got your domain, you need to decide how you’re going to serve DNS with it, in general. I was lazy and just set up my DNS server on the same machine as the website - that’s not generally appropriate for production, but a common deployment is to have a DNS master on the same machine as the website, with geographically-diverse slave servers doing zone transfers over AXFR. I’ll just look at sorting out one nameserver - a.ns.lupine.me.uk - though.
The best authoritative nameserver - by far - for DNSSEC support is PowerDNS. It handles all the difficult details that, if I’m quite honest, I don’t really understand. Debian Squeeze includes version 2.9, and DNSSEC support comes in the 3.x series, so I installed the 3.3 static package available on the website and installed it. Wheezy backports, and Debian Jessie, are both easier to deal with.
PowerDNS is fairly configurable, particularly for backends; I used its sqlite3 backend, and setting it up for that looks like this:
root@oak:/etc/powerdns/pdns.d# cat 00-sqlite3-backend.conf launch=gsqlite3 gsqlite3-database=/var/lib/powerdns/pdns.sqlite3 gsqlite3-dnssec=yes
The pdns.sqlite3 file is autogenerated when you restart PowerDNS, but it lacks certain schema elements that are necessary for DNSSEC. You can add them by running the commands detailed here - for completeness, they’re duplicated below.
root@oak:~# sqlite3 /var/lib/powerdns/pdns.sqlite3 sqlite> alter table records add ordername VARCHAR(255); sqlite> alter table records add auth bool; sqlite> create index orderindex on records(ordername); sqlite> create table domainmetadata ( id INTEGER PRIMARY KEY, domain_id INT NOT NULL, kind VARCHAR(16) COLLATE NOCASE, content TEXT ); sqlite> create index domainmetaidindex on domainmetadata(domain_id); sqlite> create table cryptokeys ( id INTEGER PRIMARY KEY, domain_id INT NOT NULL, flags INT NOT NULL, active BOOL, content TEXT ); sqlite> create index domainidindex on cryptokeys(domain_id); sqlite> create table tsigkeys ( id INTEGER PRIMARY KEY, name VARCHAR(255) COLLATE NOCASE, algorithm VARCHAR(50) COLLATE NOCASE, secret VARCHAR(255) ); sqlite> create unique index namealgoindex on tsigkeys(name, algorithm);
Now add some ordinary DNS records for PowerDNS to serve:
sqlite> insert into domains (name, type) VALUES('lupine.me.uk', 'NATIVE'); sqlite> select id from domains where name = 'lupine.me.uk'; 1 # This may be different for you - I set domain_id below to it # Set your own SOA serial value according to what you prefer sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, 'lupine.me.uk', 'SOA', 'a.ns.lupine.me.uk nick.lupine.me.uk 1378936223', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, 'lupine.me.uk', 'NS', 'a.ns.lupine.me.uk', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, 'a.ns.lupine.me.uk', 'A', '220.127.116.11', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, 'lupine.me.uk', 'MX', 'lupine.me.uk', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, 'www.lupine.me.uk', 'CNAME', 'lupine.me.uk', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, '*.chat.lupine.me.uk', 'CNAME', 'lupine.me.uk', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, '_xmpp-client._tcp.lupine.me.uk', 'SRV', '0 5222 lupine.me.uk', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES( 1, '_xmpp-server._tcp.lupine.me.uk', 'SRV', '0 5269 lupine.me.uk', 3600 );
At this point, the PowerDNS server will respond to DNS requests, but they’re not DNSSEC-signed. Enabling DNSSEC for the domain is as simple as:
root@oak:~# pdnssec secure-zone lupine.me.uk Securing zone with rsasha256 algorithm with default key size Zone lupine.me.uk secured root@oak:~# pdnssec set-nsec3 lupine.me.uk NSEC3 set, please rectify-zone if your backend needs it root@oak:~# pdnssec rectify-zone lupine.me.uk Adding NSEC3 hashed ordering information for 'lupine.me.uk' root@oak:~# pdnssec check-zone lupine.me.uk Checked 14 records of 'lupine.me.uk', 0 errors, 0 warnings. root@oak:~# pdnssec show-zone lupine.me.uk Zone is not presigned Zone has hashed NSEC3 semantics, configuration: 1 0 1 ab keys: ID = 1 (KSK), tag = 7450, algo = 8, bits = 2048 Active: 1 ( RSASHA256 ) KSK DNSKEY = lupine.me.uk IN DNSKEY 257 3 8 [...] ; ( RSASHA256 ) DS = lupine.me.uk IN DS 7450 8 1 [...] ; ( SHA1 digest ) DS = lupine.me.uk IN DS 7450 8 2 [...] ; ( SHA256 digest ) DS = lupine.me.uk IN DS 7450 8 3 [...] ; ( GOST R 34.11-94 digest ) DS = lupine.me.uk IN DS 7450 8 4 [...] ; ( SHA-384 digest ) ID = 2 (ZSK), tag = 15433, algo = 8, bits = 1024 Active: 1 ( RSASHA256 ) root@oak:~#
Now we have a signed DNSSEC zone. If you check the SQLite3 database, you’ll see new records have been generated to match the DNSKEY and DS records displayed by the show-zone command, and the records you’ve added will have had various bits of mysterious glue added. The finer points of DNSSEC are still lost on me, but the important thing to note is that the “KSK DNSKEY” is the important record that allows the chain of trust to be developed; this record is given to the upstream zone via your registry (the “.me.uk” zone for me), who sign it with their key. It is rotated every year or so, and you need to inform the registry whenever it changes; you can have multiple active ones at once. PowerDNS has some documentation on key management best practices here, but I’ve not needed to fuss with any of this, yet.
So, take your DNSKEY record (or possibly DS record - different registrars apparently might ask you for different things) and give it to your registrar. Gandi has a neat “Enable DNSSEC” form you can use; others may vary.
Once they have the record, you’re ready to change the nameservers for the domain to point to the DNS server you’ve just set up. I did this in gandi’s panel, and additional hoops I needed to jump through (because the nameserver was in the lupine.me.uk zone) included notifying Nominet of the “a.ns.lupine.me.uk” name, as well as notifying them of the “glue” between the name and its IP addresses. This varies quite considerably by registry and registrar, so I’ll leave it as an exercise to the reader.
Now that we have a DNSSEC-signed zone, we can add records to it, as defined by RFC 6698. Unless someone is able to compromise the DNS trust anchor, your registry’s keys, or your keys, anyone looking these records up can be confident that they are the ones you uploaded.
Getting a certificate
If you already have a self-signed or CA-issued certificate that you intend to use, then great. If not, you can either buy one from a CA, or become your own mini-CA and issue one for yourself. I’m sticking with a CA-issued one for the next few months, because although DNSSEC has poor client support, DANE support is entirely non-existent; so the value of a non-CA-certified certificate is still almost nil. Using a CA-issued certificate (mine is from StartSSL, and was free) in conjunction with DANE is OK - DANE-aware clients will detect traditionally-MitM’d certificates from such a record - but you miss out on a couple of benefits. Specifically, you’re still dependent on the CA to support sensible (or new/experimental) key types, and if you let the CA generate the private key rather than going the CSR route (don’t do this, ever) then you’re trusting them not to keep a record of what it was.
I may talk about how to generate a self-signed certificate here in the future.
Once you’ve got your certificate and configured your various services to use it (HTTPS especially, but also XMPP, IMAPS, SSMTP, etc), it’s time to link it all together in the DNS. Generating the records (which are known as TLSA records) is a pain, but there is a tool - called swede - to do it for you. It’s Python, only works against HTTPS, and you’d get and use it like this:
lupine@den:~/Development$ git clone https://github.com/pieterlexis/swede Cloning into 'swede'... remote: Counting objects: 116, done. remote: Compressing objects: 100% (55/55), done. remote: Total 116 (delta 67), reused 107 (delta 59) Receiving objects: 100% (116/116), 21.83 KiB, done. Resolving deltas: 100% (67/67), done. lupine@den:~/Development$ cd swede lupine@den:~/Development/swede$ sudo apt-get install python-unbound python-argparse python-ipaddr python-m2crypto # [...] lupine@den:~/Development/swede$ ./swede create --output rfc lupine.me.uk No certificate specified on the commandline, attempting to retrieve it from the server lupine.me.uk. Attempting to get certificate from 18.104.22.168 M2Crypto does not support SNI: services using virtual-hosting will show the wrong certificate! Got a certificate with Subject: /description=z3YBHiV5NCKOeIZs/C=GB/CN=www.lupine.me.uk/emailAddressemail@example.com _443._tcp.lupine.me.uk. IN TLSA 1 0 1 9730ccc0952f3150bc3c640aedb364bd628bc1738ada89826624d9442589eb06
That last line is the TLSA record that identfies your certificate. Even though swede only supports HTTPS, you can change _443 to _5222 and you’ve got an XMPP record - so let’s add a sensible set of TLSA records for this certificate to DNS.
root@oak:~# sqlite3 /var/lib/powerdns/pdns.sqlite3 sqlite> insert into records (domain_id, name, type, content, ttl) VALUES ( 1, '_443._tcp.lupine.me.uk', 'TLSA', '1 0 1 9730ccc0952f3150bc3c640aedb364bd628bc1738ada89826624d9442589eb06', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES ( 1, '_993._tcp.lupine.me.uk', 'TLSA', '1 0 1 9730ccc0952f3150bc3c640aedb364bd628bc1738ada89826624d9442589eb06', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES ( 1, '_5222._tcp.lupine.me.uk', 'TLSA', '1 0 1 9730ccc0952f3150bc3c640aedb364bd628bc1738ada89826624d9442589eb06', 3600 ); sqlite> insert into records (domain_id, name, type, content, ttl) VALUES ( 1, '_5269._tcp.lupine.me.uk', 'TLSA', '1 0 1 9730ccc0952f3150bc3c640aedb364bd628bc1738ada89826624d9442589eb06', 3600 ); sqlite> .exit root@oak:~# pdnssec increase-serial lupine.me.uk && pdnssec rectify-all-zones
Now when you visit your website in a DANE-enabled browser, you’ll see the certificate is considered valid; you could remove all CA certificates from it or use a self-signed certificate to the same end. Success!
As a fillip, now that you’ve done all that work, you can also add SSHFP records to smooth SSH access. That looks like this:
root@oak:~# sshfp --scan lupine.me.uk WARNING: Ignoring -k option, -s was passwd # lupine.me.uk SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze3 # lupine.me.uk SSH-2.0-OpenSSH_5.5p1 Debian-6+squeeze3 lupine.me.uk IN SSHFP 1 1 08C614DAF69DA62937FEFFA025607569B54B8D08 lupine.me.uk IN SSHFP 2 1 67B596A0A593A931DAD21C83F6E7B9F02CBFE6F5 root@oak:~# sqlite3 /var/lib/powerdns/pdns.sqlite3 sqlite> insert into records (domain_id, name, type, content, ttl) VALUES ( 1, 'lupine.me.uk', 'SSHFP', '1 1 08C614DAF69DA62937FEFFA025607569B54B8D08', 3600 ); sqlite> # ... sqlite> .exit root@oak:~# pdnssec increase-serial lupine.me.uk && pdnssec rectify-all-zones
To make use of this, you’ll also need to alter your ssh_config:
lupine@den:~$ echo "\n\nVerifyHostKeyDNS yes" >> ~/.ssh/config
The outcome is that when logging into your machines over SSH from a new location, your SSH client can check the presented host key fingerprints against the ones in DNS, and warn you if they don’t match for any reason - a man-in-the-middle attack, for instance. Or a server reinstall, of course.