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.

Resolving nameserver

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

; EDNS: version: 0, flags: do; udp: 4096
;mozilla.org.           IN  A

mozilla.org.        60  IN  A
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=

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=

ns1.mozilla.org.    60  IN  A
ns2.mozilla.org.    60  IN  A
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
;; WHEN: Sat Sep 14 16:54:58 2013
;; MSG SIZE  rcvd: 1492


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

Authoritative nameserver

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 

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', '', 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
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 ) 

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.

Generating records

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
  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/emailAddress=postmaster@lupine.me.uk
  _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.