Signs of Triviality

Opinions, mostly my own, on the importance of being and other things.
[homepage] [index] [jschauma@netmeister.org] [@jschauma] [RSS]

New Adventures in DNSSEC and DANE

May 7th, 2019

I've long had a passive interest in DNSSEC (RFC 2535, RFC 4033, RFC 4034, RFC 4035 etc.; Cloudflare has a good summary) as it addresses a number of problems with the DNS. Unfortunately, I've never made the time to immerse myself in it and to attempt to set up my own nameserver, to manage my own keys, and all that.

But the other day I happened to stumble upon an enticing button in Gandi's admin interface labeled "Enable DNSSEC":

Gandi
Admin Panel: Enable DNSSEC  Gandi
Admin Panel: DNSSEC has been enabled

Clickety-click, and done -- and just like that, my domain netmeister.org now has DNSSEC! Gandi handles all the necessary magic of key management and RR signing etc. for me with no overhead whatsoever to me. Neat!

So let's see what this looks like in practice:

$ dig @9.9.9.9 +dnssec www.netmeister.org AAAA

; <<>> DiG 9.10.6 <<>> @9.9.9.9 +dnssec www.netmeister.org AAAA
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 26108
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags: do; udp: 512
;; QUESTION SECTION:
;www.netmeister.org.		IN	AAAA

;; ANSWER SECTION:
www.netmeister.org.	10800	IN	CNAME   panix.netmeister.org.
panix.netmeister.org.	10800	IN	AAAA    2001:470:30:84:e276:63ff:fe72:3900
www.netmeister.org.	10800	IN	RRSIG	CNAME 13 3 10800 20190516000000 20190425000000 31910 netmeister.org. tviGUJuVupLVoHB5pig9AC8FJZ3WmXrq439vNl2yzN2P/A4GFVA/q7br2ZGauaDb44wJsi9vk+hlSgtXRsJOqg==
panix.netmeister.org.	10800	IN	RRSIG	AAAA 13 3 10800 20190516000000 20190425000000 31910 netmeister.org. omB7Ped2qZQC8JnFHI+4K7KN3lisYUpH280bWdNmNYKnvcDQjVClJqZ4M94uzeb1pj8oG2TUuK8Tv1S0yaAcVA==

;; Query time: 1576 msec
;; SERVER: 9.9.9.9#53(9.9.9.9)
;; WHEN: Thu May 02 21:51:03 EDT 2019
;; MSG SIZE  rcvd: 315

Note the bold ad flag in the above output: this shows that the authentic data bit was set in the query -- the results were verified. We also get the RRSIG records, the signatures of the records we asked for, displayed as well.

Since we need a DNSSEC enabled resolver, in this example we used Quad9's. If your local resolver is DNSSEC enabled, then you can of course leave that out. Otherwise, running dig +dnssec www.netmeister.org AAAA will not actually verify the results.

Ok, let's take a closer look at the signatures:

$ dig www.netmeister.org AAAA +dnssec +short
panix.netmeister.org.
CNAME 13 3 10800 20190516000000 20190425000000 31910 netmeister.org. tviGUJuVupLVoHB5pig9AC8FJZ3WmXrq439vNl2yzN2P/A4GFVA/q7br2ZGauaDb44wJsi9vk+hlSgtXRsJOqg==
2001:470:30:84:e276:63ff:fe72:3900
AAAA 13 3 10800 20190516000000 20190425000000 31910 netmeister.org. omB7Ped2qZQC8JnFHI+4K7KN3lisYUpH280bWdNmNYKnvcDQjVClJqZ4M94uzeb1pj8oG2TUuK8Tv1S0yaAcVA==

A few things to note: First, an RRSIG record always covers resource record sets; that is, if there were multiple AAAA records, there would still only be one RRSIG record. Secondly, you may notice that the CNAME resource record also has an RRSIG. That is both expected (every resource record must have a signature) and unexpected (per RFC 1034 / RFC 1912 etc., a CNAME should not have any other records -- a nuisance when it comes to e.g., CAA records).

Either way, we follow the CNAME, retrieve the AAAA record, and get RRSIG records for all results along the way. Each RRSIG record consists of:

  • Type Covered -- CNAME and AAAA in the above
  • Algorithm -- 13 in the above; this is ECDSA Curve P-256 with SHA-256 (RFC 6605) per IANA
  • Labels -- 3 in the above; this is the number of labels (.s) in the original RRSIG RR owner name, including the right-most root label -- basically the number of dots in the full record, so 3 for www.netmeister.org.
  • Original TTL -- 10800 in the above
  • Signature Expiration -- 20190516000000 in the above; an actual timestamp in 32-bit seconds since the epochs, here translated into date +%Y%m%d%H%M%S
  • Signature Inception -- 20190425000000 in the above
  • Key Tag -- 31910 in the above; identifying the DNSKEY used to sign the data in question
  • Signer's Name -- netmeister.org. in the above
  • Signature -- the actual signature in base64 encoding

Our good friend Wireshark illustrates these fields nicely:

Wireshark showing the details of an RRSIG record

The signatures are made using the zone-signing key (ZSK), the public portion of which is then included in the DNSKEY record. That record is itself signed, so a lookup of the DNSKEY record necessarily produces an RRSIG record:

$ dig +nocmd netmeister.org dnskey +dnssec +noall +answer +multi
netmeister.org.         10662 IN DNSKEY 256 3 13 (
                                W1zt0mA1d1FHzK+qkEzMb8B7IZkn7qbHxSzjwDzQr4DU
                                CjlycXDRGdxupCDsE7iaaxDCGR2T8nZsteX0PXx7Bg==
                                ) ; ZSK; alg = ECDSAP256SHA256 ; key id = 31910
netmeister.org.         10662 IN RRSIG DNSKEY 13 2 10800 (
                                20190516000000 20190425000000 31910 netmeister.org.
                                OIUrHowNF2kFBQ/rglfQMfUzYNge4DG4mhCj8pTRVcvt
                                wRoPpR2BOkKt1fQj2vnFFiyCGCL2xwhmk8e9yv+18w== )
$ 

But wait, if the RRSIG for the DNSKEY record was made using keyid 3190, isn't that a self-signed signature? How can we trust it? Well, the answer is that the DNSKEY record itself would be signed by a key-signing key (KSK), the digest of which is stashed in the DS (Delegation Signer) record from the parent zone:

$ dig +nocmd netmeister.org ds +dnssec +noall +answer +multi
netmeister.org.         86266 IN DS 31910 13 2 (
                                9AA11C0C17F25E9C09A8A7360C6292CC5DAA0FDEADDA
                                1D0EDC733949AA05B781 )
netmeister.org.         86266 IN RRSIG DS 7 2 86400 (
                                20190522152847 20190501142847 16454 org.
                                HDxDdbp11oHX8wHBiSzeEWb4JAWjkZwIOOATqh7cvSga
                                9Qln9lou79SxtFMQGGBWlsySWReNFfsjDfLzBGlcfMAc
                                DrrQr+x02hKCG7T8pBaFObmijOymAvqKLe+4qewWLMLF
                                1IZ/kIGgwHFJBGrDgBQ44GGeOPkx9G7jHAL28VM= )
$ 

Note that this response comes from the NS server for .org, not the nameserver authoritative for netmeister.org! The respective RRSIG was made by the DNSKEY with the keyid 16454. In other words, this is where we establish trust from our parent zone, which in turn has its DNSKEY signed by its parent and so on, until you finally reach the (self-signed) root KSK. This graph generated via dnsviz.net and table from Verisign Labs' DNSSEC debugger better illustrate the relationships:

A graph
illustrating the DNSSEC trust relationships for the
netmeister.org domain  DNSSEC Debugger
output from Verisign Labs

Manually validating all these records is going to be a bit painful, so the friendly people from ISC provide the delv(1) utility (included in your bind package) to perform this task for you:

$ delv netmeister.org aaaa +multi +vtrace
;; fetch: netmeister.org/AAAA
;; validating netmeister.org/AAAA: starting
;; validating netmeister.org/AAAA: attempting positive response validation
;; fetch: netmeister.org/DNSKEY
;; validating netmeister.org/DNSKEY: starting
;; validating netmeister.org/DNSKEY: attempting positive response validation
;; fetch: netmeister.org/DS
;; validating netmeister.org/DS: starting
;; validating netmeister.org/DS: attempting positive response validation
;; fetch: org/DNSKEY
;; validating org/DNSKEY: starting
;; validating org/DNSKEY: attempting positive response validation
;; fetch: org/DS
;; validating org/DS: starting
;; validating org/DS: attempting positive response validation
;; fetch: ./DNSKEY
;; validating ./DNSKEY: starting
;; validating ./DNSKEY: attempting positive response validation
;; validating ./DNSKEY: verify rdataset (keyid=20326): success
;; validating ./DNSKEY: signed by trusted key; marking as secure
;; validating org/DS: in fetch_callback_validator
;; validating org/DS: keyset with trust secure
;; validating org/DS: resuming validate
;; validating org/DS: verify rdataset (keyid=25266): success
;; validating org/DS: marking as secure, noqname proof not needed
;; validating org/DNSKEY: in dsfetched
;; validating org/DNSKEY: dsset with trust secure
;; validating org/DNSKEY: verify rdataset (keyid=9795): success
;; validating org/DNSKEY: marking as secure (DS)
;; validating netmeister.org/DS: in fetch_callback_validator
;; validating netmeister.org/DS: keyset with trust secure
;; validating netmeister.org/DS: resuming validate
;; validating netmeister.org/DS: verify rdataset (keyid=16454): success
;; validating netmeister.org/DS: marking as secure, noqname proof not needed
;; validating netmeister.org/DNSKEY: in dsfetched
;; validating netmeister.org/DNSKEY: dsset with trust secure
;; validating netmeister.org/DNSKEY: verify rdataset (keyid=31910): success
;; validating netmeister.org/DNSKEY: marking as secure (DS)
;; validating netmeister.org/AAAA: in fetch_callback_validator
;; validating netmeister.org/AAAA: keyset with trust secure
;; validating netmeister.org/AAAA: resuming validate
;; validating netmeister.org/AAAA: verify rdataset (keyid=31910): success
;; validating netmeister.org/AAAA: marking as secure, noqname proof not needed
; fully validated
netmeister.org.		3600 IN	AAAA 2001:470:30:84:e276:63ff:fe72:3900
netmeister.org.		3600 IN	RRSIG AAAA 13 2 3600 (
				20190516000000 20190425000000 31910 netmeister.org.
				DUKa+iLUk5Tv3+nlQAKMRs0DkOdVeoXJNnYkotRmNW9M
				ZLC/LhjDdqYX50q7LEPSrtF9i01VgmXLBCVyWfBTWw== )
$ 

Alternatively, for example if you are using unbound, you can use the drill(1) tool:

$ drill -S netmeister.org
;; Number of trusted keys: 1
;; Chasing: netmeister.org. A


DNSSEC Trust tree:
netmeister.org. (A)
|---netmeister.org. (DNSKEY keytag: 31910 alg: 13 flags: 256)
    |---netmeister.org. (DS keytag: 31910 digest type: 2)
        |---org. (DNSKEY keytag: 16454 alg: 7 flags: 256)
            |---org. (DNSKEY keytag: 9795 alg: 7 flags: 257)
            |---org. (DNSKEY keytag: 17883 alg: 7 flags: 257)
            |---org. (DS keytag: 9795 digest type: 2)
            |   |---. (DNSKEY keytag: 25266 alg: 8 flags: 256)
            |       |---. (DNSKEY keytag: 20326 alg: 8 flags: 257)
            |---org. (DS keytag: 9795 digest type: 1)
                |---. (DNSKEY keytag: 25266 alg: 8 flags: 256)
                    |---. (DNSKEY keytag: 20326 alg: 8 flags: 257)
;; Chase successful
$ 

Ok, we have DNSSEC. Big whoop. Now what? Well, now that we can trust DNS, we can use it to solve the Trust on First Use problem inherent in traditional SSH server keys and unknown fingerprints. For that, we use the SSHFP resource records (RFC 4255 and RFC 6594). You can trivially generate the SSHFP records using ssh-keygen(1). That is, assuming your various public keys are in /etc/ssh/, you can run:

$ ssh-keygen -r panix
panix IN SSHFP 1 1 53a76d5284c91e140dec9ad1a757da123b95b081
panix IN SSHFP 1 2 96c7f824b942c9225b71cf462ac3ddd4b9d7efa6325d98860ad7fd6268597295
panix IN SSHFP 2 1 5eb67b5b147548dc510d7dc9fc77382c9283264b
panix IN SSHFP 2 2 c62f9c25e25e89984557916d41b85e75c44e46c3d629c014c20d7243aea14b58
panix IN SSHFP 3 1 5d98620a58f688998549949b3f29eaea68709e5e
panix IN SSHFP 3 2 62475a22f1e4f09594206539aaff90a6edaabab1ba6f4a67ab3906177455cf84
panix IN SSHFP 4 1 69549b079f02a59e5814a66fdb0877f447e4c4e3
panix IN SSHFP 4 2 dd6e3f398637eb1086b9b2bf3f29c161dba0d12a39109e6cc87ef2486d28d5de
$ 

In this example, we end up with (in order):

  • key type RSA, fingerprint type SHA1 (1 1)
  • key type RSA, fingerprint type SHA-256 (1 2)
  • key type DSA, fingerprint type SHA1 (2 1)
  • key type DSA, fingerprint type SHA-256 (2 2)
  • key type ECDSA, fingerprint type SHA1 (3 1)
  • key type ECDSA, fingerprint type SHA-256 (3 2)
  • key type Ed25519, fingerprint type SHA1 (4 1)
  • key type Ed25519, fingerprint type SHA-256 (4 2)

Alternatively, you could of course calculate the fingerprints yourself:

$ for f in /etc/ssh/*.pub; do
        awk '{print $2}' $f | openssl base64 -d -A | openssl sha1
        awk '{print $2}' $f | openssl base64 -d -A | openssl sha256
done
(stdin)= 5eb67b5b147548dc510d7dc9fc77382c9283264b
(stdin)= c62f9c25e25e89984557916d41b85e75c44e46c3d629c014c20d7243aea14b58
(stdin)= 5d98620a58f688998549949b3f29eaea68709e5e
(stdin)= 62475a22f1e4f09594206539aaff90a6edaabab1ba6f4a67ab3906177455cf84
(stdin)= 69549b079f02a59e5814a66fdb0877f447e4c4e3
(stdin)= dd6e3f398637eb1086b9b2bf3f29c161dba0d12a39109e6cc87ef2486d28d5de
(stdin)= da39a3ee5e6b4b0d3255bfef95601890afd80709
(stdin)= e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
(stdin)= 53a76d5284c91e140dec9ad1a757da123b95b081
(stdin)= 96c7f824b942c9225b71cf462ac3ddd4b9d7efa6325d98860ad7fd6268597295
$ 

Either way, add those to your DNS zone, and verify:

$ delv panix.netmeister.org sshfp +multi
; fully validated
panix.netmeister.org.   10800 IN SSHFP 4 1 (
                                69549B079F02A59E5814A66FDB0877F447E4C4E3 )
panix.netmeister.org.   10800 IN SSHFP 4 2 (
                                DD6E3F398637EB1086B9B2BF3F29C161DBA0D12A3910
                                9E6CC87EF2486D28D5DE )
panix.netmeister.org.   10800 IN SSHFP 3 1 (
                                5D98620A58F688998549949B3F29EAEA68709E5E )
panix.netmeister.org.   10800 IN SSHFP 2 1 (
                                5EB67B5B147548DC510D7DC9FC77382C9283264B )
panix.netmeister.org.   10800 IN SSHFP 1 1 (
                                53A76D5284C91E140DEC9AD1A757DA123B95B081 )
panix.netmeister.org.   10800 IN SSHFP 2 2 (
                                C62F9C25E25E89984557916D41B85E75C44E46C3D629
                                C014C20D7243AEA14B58 )
panix.netmeister.org.   10800 IN SSHFP 3 2 (
                                62475A22F1E4F09594206539AAFF90A6EDAABAB1BA6F
                                4A67AB3906177455CF84 )
panix.netmeister.org.   10800 IN SSHFP 1 2 (
                                96C7F824B942C9225B71CF462AC3DDD4B9D7EFA6325D
                                98860AD7FD6268597295 )
panix.netmeister.org.   10800 IN RRSIG SSHFP 13 3 10800 (
                                20190516000000 20190425000000 31910 netmeister.org.
                                Tn6HWogGLOfoyQ0lq1K9LYpn/3FUcPUlUiVwDhjF42Cl
                                ubu+KbGXxOIPWNFF161crDrKwXc/ZdVXCaMeqIuksQ== )
$ 

Again, note that we have a single RRSIG record for the entire SSHFP resource record set. With these records in place and signed, and if you are using a DNSSEC enabled resolver, set VerifyHostKeyDNS=yes and then you can verify the authenticity of the server without requiring an entry in your ~/.ssh/known_hosts file:

$ ssh -v -o UserKnownHostsFile=/dev/null -o VerifyHostKeyDNS=yes panix.netmeister.org
[...]
debug1: Server host key: ecdsa-sha2-nistp521 SHA256:YkdaIvHk8JWUIGU5qv+Qpu2qurG6b0pnqzkGF3RVz4Q
debug1: found 8 secure fingerprints in DNS
debug1: matching host key fingerprint found in DNS
[...]
panix$ 

(Without DNSSEC, these records could still be added, but your ssh(1) client will not accept them as sufficient to allow you to connect.)

Ok, so that's pretty useful. The next time you rotate your hosts' keys, you no longer have to update all clients' known_hosts, so long as you update your SSHFP records in the DNS. Similarly, you can publish your PGP keys into the DNS (RFC 4398, RFC 7929). What else can we do with DNSSEC?

DANE is great...

A sitting Great Dane Well, there's DNS-based Authentication of Named Entities, aka DANE (RFC 6698, RFC 7218, RFC 7671). That is, we can assert the authenticity of an x509 certificate. This can be used to allow for authentication of e.g., TLS connections without the need for a trusted root CA. But you can also use this to effectively implement HTTP Public Key Pinning (HPKP, RFC 7469, deprecated by Google since then): by adding the right TLSA resource records, you can thus "pin" the leaf cert or a "trust anchor assertion", and clients enforcing DANE would reject a fraudulently obtained certificate, even if it was signed by an otherwise trusted certificate authority.

(This approach can be used to protect against MitM attacks and the notorious middleware boxes, sold as "SSL Proxy" solutions. If DANE was in widespread use, these systems would not work. Or, at a minimum, they'd have to also control the DNS in question and add TLSA records on the fly for the certificates they generate. It'd get ugly quickly.)

TLSA records are created for the given name using _<port>._<protocol> service labels and consist of Certificate Usage, a Selector, Matching Type, and finally the associated data. To generate the record for a "Domain Issued Certificate" using the SHA-256 hash of the Subject Public Key, you could run:

$ <cert.pem openssl x509 -noout -pubkey -outform DER |  \
        openssl rsa -pubin -pubout -outform DER 2>/dev/null |                   \
        openssl sha256 | awk '{print toupper($NF);}'
FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA14344731EB5
$ 

This value would then be added for every Subject Alternate Name (SAN) in the certificate, so the required records for the cert used on this website would be (as of 2019-05-03; the fingerprint will of course change the next time the certificate is renewed):

_443._tcp.mta-sts IN TLSA   3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA14344731EB5
_443._tcp.panix   IN TLSA   3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA14344731EB5
_443._tcp.www     IN TLSA   3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA14344731EB5
_443._tcp         IN TLSA   3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA14344731EB5

Using the Subject Public Key here instead of using the full certificate has the advantage that you can generate the TLSA records from the CSR before you even have received or deployed your certificate. If you do that, merely replace x509 with req in the above openssl(1) command.

But of course now you have to actually keep your TLSA records in sync with your certificate! Thanks to Let's Encrypt, many of us are now renewing our certificates automatically and frequently, so care must be taken to at the same time update the TLSA records. Since DNS lookups may be cached, you mustn't remove the old TLSA records until your TTL has expired! In fact, it's probably a good idea to retain the old TLSA record for, say, two times your current TTL before you remove it. Similarly, you must not deploy the new certificate until the new TLSA record has propagated!

(One way to reduce this risk and avoid having to generate new TLSA records every time you renew your certificate would be to instead add a TLSA record using a "trust anchor assertion" in the certificate usage field (2), thereby effectively pinning the CA's root cert.)

Generating these records manually is a bit of a pain. I also found out that the web UI offered by my DNS provider -- Gandi -- does not allow adding of TLSA records and instead requires those records to be added using their API. Combined with the desire to integrate the record generation with the certificate provisioning via Let's Encrypt, I ended up putting together a script called gandi-tlsa-glue that processes the CSR or cert and adds the required records:

$ gandi-tlsa-glue -v -v -v www.netmeister.org.pem
=> Processing cert 'www.netmeister.org.pem'...
==> Creating TLSA record for SAN 'www.netmeister.org'...
===> Adding TLSA record '_443._tcp.mail IN 3600 3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA143' in domain 'netmeister.org'...
==> Creating TLSA record for SAN 'mta-sts.netmeister.org'...
===> Adding TLSA record '_443._tcp.mta-sts IN 3600 3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA143' in domain 'netmeister.org'...
==> Creating TLSA record for SAN 'panix.netmeister.org'...
===> Adding TLSA record '_443._tcp.panix IN 3600 3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA143' in domain 'netmeister.org'...
==> Creating TLSA record for SAN 'netmeister.org'...
===> Adding TLSA record '_443._tcp IN 3600 3 1 1 FB5CB6BC898A19916EB81523AB47DD9540D22326A28CA02EC4EBA143' in domain 'netmeister.org'...
$ 

Verification of your TLSA records can be done manually:

$ tlsa=$(dig +short _443._tcp.www.netmeister.org tlsa | sed -e 's/^. . . //' -e 's/ //')
$ cert=$(</dev/null openssl s_client -connect www.netmeister.org:443 2>/dev/null | \
        openssl x509 -noout -pubkey -outform DER |                                       \
        openssl rsa -pubin -pubout -outform DER 2>/dev/null |                         \
        openssl sha256 | awk '{print toupper($NF);}')
$ [ x"${cert}" = x"${tlsa}" ] && echo match
match
$ 

There are online tools to verify your TLSA records as well, of course:

DANE
verification via an external website

Happy with your DANE authenticated HTTPS traffic, you can the move forward and add records for e.g., your mail server:

$ gandi-tlsa-glue -p 25 mail.netmeister.org.pem
$ delv _25._tcp.panix.netmeister.org tlsa +multi
; fully validated
_25._tcp.panix.netmeister.org. 3600 IN TLSA 3 0 1 (
                                A4FB20F6CD96FCF78687717D8E4E15C831DB467B49F9
                                9777AE6336634B5A785C )
_25._tcp.panix.netmeister.org. 3600 IN RRSIG TLSA 13 5 3600 (
                                20190516000000 20190425000000 31910 netmeister.org.
                                rjLTZs7cCwNV5Quu/Sxn8wMCVaWVXTMIPU4efOM7LkeA
                                622OcqdUKbPms/oMeFh92weDLbA3DIfTwnPbr7jwDw== )
$ 

How do we know that this works? Let's break out our openssl(1) swiss army knife once more:

$ dig _25._tcp.panix.netmeister.org tlsa +short
3 0 1 A4FB20F6CD96FCF78687717D8E4E15C831DB467B49F99777AE633663 4B5A785C
$ openssl s_client -connect panix.netmeister.org:25 -starttls smtp \
        -dane_tlsa_domain panix.netmeister.org \
        -dane_tlsa_rrdata "3 0 1 A4FB20F6CD96FCF78687717D8E4E15C831DB467B49F99777AE6336634B5A785C"  
[...]
---
SSL handshake has read 3979 bytes and written 481 bytes
Verification: OK
Verified peername: panix.netmeister.org
DANE TLSA 3 0 1 ...49f99777ae6336634b5a785c matched EE certificate at depth 0
---
[...]
$ 

Ok, so let's make sure our mail server actually uses DNSSEC and verifies DANE. Fortunately, postfix has simple instructions to configure opportunistic DANE support:

$ postconf smtp_dns_support_level smtp_host_lookup smtp_tls_security_level       
smtp_dns_support_level = dnssec
smtp_host_lookup = dns
smtp_tls_security_level = dane
$ 

As noted in the Postfix instructions, these settings apply to outgoing mail only; to test them, we need mail servers to which we send emails where it (a) has a valid TLSA record in a DNSSEC signed zone; (b) has a (any) TLSA record in a zone without DNSSEC; and (c) has an invalid TLSA record in a DNSSEC signed zone. Fortunately, our friends over at Have DANE? provide just such a setup, and sending mail with the correctly configured mail server yields the expected and desired result:

Verification screen showing DANE SMTP sending.

In our mail logs, these connections then look as follows:

<mail.info>May  6 22:37:28 panix postfix/smtp[14515]: certificate verification failed for
        wrong.havedane.net[2001:1af8:4700:a118:90::7c0]:25: untrusted issuer
        /C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=MyOrganizationalUnit/CN=Fort-Funston
        CA/name=EasyRSA/emailAddress=me@myhost.mydomain
<mail.info>May  6 22:37:28 panix postfix/smtp[14515]: B33C1857A0: Server certificate not trusted
<mail.info>May  6 22:37:28 panix postfix/smtp[22214]: B33C1857A0: to=<fcad73fa9ea1bc94@do.havedane.net>,
        relay=do.havedane.net[5.79.70.105]:25, delay=2.1,
        delays=0.1/0.18/1.6/0.17, dsn=2.0.0, status=sent (250
        2.0.0 Ok: queued as 6141CC0CDA)
<mail.info>May  6 22:37:28 panix postfix/smtp[14515]: certificate verification failed for
        wrong.havedane.net[5.79.70.105]:25: untrusted issuer
        /C=US/ST=CA/L=SanFrancisco/O=Fort-Funston/OU=MyOrganizationalUnit/CN=Fort-Funston
        CA/name=EasyRSA/emailAddress=me@myhost.mydomain
<mail.info>May  6 22:37:28 panix postfix/smtp[14515]: B33C1857A0: to=<fcad73fa9ea1bc94@wrong.havedane.net>,
        relay=wrong.havedane.net[5.79.70.105]:25, delay=2.2,
        delays=0.1/0.23/1.8/0, dsn=4.7.5, status=deferred
        (Server certificate not trusted)
<mail.info>May  6 22:37:28 panix postfix/smtp[11388]: B33C1857A0: to=<fcad73fa9ea1bc94@dont.havedane.net>,
        relay=dont.havedane.net[5.79.70.105]:25, delay=2.3,
        delays=0.1/0.23/1.8/0.16, dsn=2.0.0, status=sent (250
        2.0.0 Ok: queued as 971A2C0CDA)

Note that we see two attempts to deliver mail to wrong.havedane.net -- once via IPv4, and once via IPv6. In either case, the mail is not delivered due to a cert issued by an untrusted issuer, intentionally served by the server. The certificate public key fingerprint noted in the TLSA record in the DNS is also (intentionally) incorrect:

$ dig _25._tcp.wrong.havedane.net tlsa +short 
3 1 1 553ACF88F9EE18CCAAE635CA540F32CB84ACA77C47916682BCB542D5 1DAA871E
2 1 1 27B694B51D1FEF8885372ACFB39193759722B736B0426864DC1C79D0 651FEF72
$ openssl s_client -connect wrong.havedane.net:25 -starttls smtp </dev/null 2>/dev/null | \
        openssl x509 -noout -pubkey -outform DER | \
        openssl rsa -pubin -pubout -outform DER 2>/dev/null | \
        openssl SHA256 | awk '{print toupper($NF);}'
553ACF88F9EE18CCAAE635CA540F32CB84ACA77C47916682BCB542D51DAA871F
$ 

Mail to dont.havedane.net is delivered, even though no TLSA records are found, as our postfix configuration is still opportunistic; changing smtp_tls_security_level to dane-only would cause it to fail delivery to mail servers without DANE:

$ sudo postconf smtp_tls_security_level=dane-only
$ sudo /etc/rc.d/postfix restart
postfix/postfix-script: stopping the Postfix mail system
postfix/postfix-script: waiting for the Postfix mail system to terminate
postfix/postfix-script: starting the Postfix mail system
$ mail 2784c98ff7d91188@dont.havedane.net </dev/null
No message, no subject; hope that's ok
$ sudo grep dont.havedane.net /var/log/maillog
<mail.warn>May  6 23:19:11 panix postfix/smtp[26740]: warning: TLS policy lookup for
        dont.havedane.net/dont.havedane.net: no TLSA records found
<mail.info>May  6 23:19:11 panix postfix/smtp[26740]: 8FB79857A2: to=<2784c98ff7d91188@dont.havedane.net>,
        relay=none, delay=0.16, delays=0.08/0.08/0.01/0,
        dsn=4.7.5, status=deferred (no TLSA records found)
$ 

Hvorfor gør du ikke DANE?

Ok, so if DNSSEC / DANE is so great, can we just use this everywhere? Like... your browser?

Mozilla and Google both have been going back and forth on their plans to implement DNSSEC and DANE. and even our otherwise ever reliable curl(1) does not have DNSSEC / DANE support: after over 6 years, it remains an open TODO item, although partial work was previously done.

Google initially implemented DNSSEC authenticated HTTPS in Chrome, but later removed it. (There's some irony in Google arguing against DNSSEC in favor of HPKP, only to then remove HPKP from Chrome, shifting entirely towards detection of fraudulent certificates by way of CAA records (RFC 6844), even though each of these solutions appear to me to address different, albeit overlapping or intersecting problems and threats.) Mozilla similarly had plans, but the respective bugzilla tickets were either closed WONTFIX or left open.

A web browser add-on used to be available from https://www.dnssec-validator.cz/, but that required an external binary application in addition to the add-on and was ultimately abandoned in October of 2018. So as it stands right now (as of 2019-05-07), the only browser add-on I was able to find was this one; it adds a simple visual marker in the address bar when a site has a valid DNSSEC protected TLSA record:

Firefox DNSSEC add-on showing a valid record for
this site Firefox DNSSEC
add-on showing an invalid/missing record for
Google

Since a DNSSEC enabled resolver is needed to run these checks, the plugin uses either Cloudflare's 1.1.1.1 or Google's 8.8.8.8 public resolvers over DNS over HTTPS (DoH, RFC 8484). This of course implies that all your DNS queries are sent to either of these third parties with all the privacy implications therein. Allowing the user to specify a DNSSEC enabled resolver would be nice here.

Bad DANE! Sit!

A great
dane sitting on a chewed up sofa.So why is nobody implementing DNSSEC and DANE? If you follow the discussions in the tickets linked above, you find a few concerns and problems noted; in addition, respectable people have argued with reason against DNSSEC. Some of the concerns include:

  • the large(r) records might be used to facilitate DNS reflection and amplification attacks (counter point)
  • DNSSEC adds complexity; failure to carefully manage the records has repeatedly lead to outages, for example when RRSIG records expired
  • there's no revocation mechanism for DNS records, and a compromised key remains valid for the TTL of the TLSA record
  • DANE makes MitM systems (e.g., "SSL Proxy" or other so-called middleboxes) more complicated (which of course is by design, since the MitM is precisely the scenario many of these mechanisms are intended to protect against)
  • DNSSEC / DANE does not solve all problems and security nerds will happily find was to circumvent the protections offered

This list is not complete, but by and large many of the concerns seem to me to fall into the Nirvana Fallacy category. I don't know if DNSSEC and DANE are the right solution -- perhaps instead we should pursue DNSCurve? Some parts of the industry appears to be favoring DNS over TLS (RFC 7858) and/or DNS over HTTPS (RFC 8484), but I simply don't know enough about either to judge.

All I know is that I think it'd be rather useful to be able to trust the DNS; that I'm willing to improve security little by little, even if I can't defeat all attacks; and that I sure do appreciate an easy click-of-the-button method as offered by my DNS provider. Being able to toy around with these solutions is one of the reasons why I continue to run my own web- and mail server instead of any hosted solutions, and for whatever it's worth, these services are now secured via DNSSEC and DANE.

May 7th, 2019


Links from this blog post


[The Zen of Infosec] [Index]