PowerDNS with the remote back-end and DNSSEC

The [1]PIPE back-end has been a part of [2]PowerDNS since what feels
like forever: it speaks to a program you write via stdin and stdout.
PowerDNS hands it queries which your process responds to in a
particular textual format, and the name server then converts those to
DNS responses which it returns to its clients. This so-called
coprocess is launched by the name server, and if it should die, it is
re-launched.

While it is slightly slower, the [3]REMOTE back-end has a lot more
features than the PIPE back-end. For one, it can do full [4]DNSSEC
signing, and it can talk to your actual back-end program via Unix
sockets, pipes, or via a RESTful HTTP interface (neither
authentication nor [5]TLS are supported, but these can be added by
hiding the interface behind an appropriate proxy). The RESTful
interface supports GET or POST requests, and if we use POST it sends
queries in [6]JSON-formatted RPC requests.

I wanted to get a feeling for the remote back-end, so I whipped up a
little something which allows me to query for [7]IATA airport codes
and their locations which we return as a [8]DNS LOC record.
; ANSWER SECTION:
lax.airports.aa.  60 IN TXT  "Los Angeles International Airport"
lax.airports.aa.  60 IN LOC  33 56 36.233 N 118 24 29.808 W 0.00m 1m 1
0000m 10m

In order to activate the remote back-end, I configure the following in
pdns.conf:
launch=gmysql,remote
# gmysql-dnssec
gmysql-dbname=pdns
gmysql-host=127.0.0.1
gmysql-port=3306
gmysql-user=pdns
gmysql-password=pdns
remote-connection-string=http:url=http://192.168.1.130:9053,url-suffix
=,timeout=2000
# remote-dnssec=yes

I launch gmysql before remote because the former should be queried
first (your mileage will vary), and the remote-connection-string
defines how PowerDNS accesses its remote back-end - in this case via
HTTP.

The back-end process is in Python (code [9]here) and it implements a
/lookup endpoint which is used by PowerDNS to get the data. When we
query for a TXT or LOC record, PowerDNS actually fires off an ANY
query to our interface (SOA and NS queries are passed in with their
qtypes), as in curl
http://192.168.1.130:9053/lookup/LAX.airports.aa/ANY:
{
 "result": [
   {
     "ttl": 60,
     "auth": 1,
     "qname": "LAX.airports.aa",
     "qtype": "TXT",
     "content": "Los Angeles International Airport"
   },
   {
     "ttl": 60,
     "auth": 1,
     "qname": "LAX.airports.aa",
     "qtype": "LOC",
     "content": "33 56 36.233 N 118 24 29.808 W 0.00m"
   }
 ]
}

Had I configured url-suffix=.xyz, for example, the HTTP queries would
have had .xyz added to them (e.g. /lookup/LAX.airports.aa/ANY.xyz)
which might be useful to return something static, or when each of your
queries are to be handled by their own PHP script.

Using [10]query-loc, which we've [11]mentioned here already, we can
check if this is working:
$ query-loc ibz.airports.aa
38 52 35.742 N 1 22 04.091 E 0.00m 1.00m 10000.00m 10.00m

Lazy DNSSEC

But what about [12]DNSSEC? I'll restart the PowerDNS server with the
comments in the above configuration removed. I mentioned earlier, that
the remote back-end is much more capable than the pipe back-end is: it
is able to do full DNSSEC by itself including delegation and key
storage, and [13]Aki Tuomi, the author of this back-end, has a
complete example in Perl called [14]autorev which demonstrates this.
(BTW, Aki is the same person who [15]implemented the PKCS#11 interface
in PowerDNS.)

Now, I am far too lazy to do all this, so I'll use PowerDNS for the
heavy lifting, letting it create and store the keys for me. In order
to be able to do that, I have to add our zone to the domains table so
that PowerDNS can associate the DNSSEC keys we create for the zone (in
the cryptokeys database table) with this domain:
INSERT INTO domains (name, type) VALUES ('airports.aa', 'NATIVE');

I then use the utility to actually create the keys (KSK and ZSK), and
I set NSEC3 narrow mode. PowerDNS' Narrow mode uses "additional
hashing calculations to provide hashed secure denial of existence `on
the fly', without further involving the database". What this basically
means is that it lies its pants off but is able to convince the client
that something really doesn't exist if it doesn't. ;-)
$ pdnssec secure-zone airports.aa
Securing zone with rsasha256 algorithm with default key size
Zone airports.aa secured
Adding NSEC ordering information

$ pdnssec set-nsec3 airports.aa '1 0 5 DEED' narrow
NSEC3 set, please rectify-zone if your backend needs it

Now I obtain the DS for this zone:
$ pdnssec show-zone airports.aa
Zone is not presigned
Zone has NARROW hashed NSEC3 semantics, configuration: 1 0 5 deed
keys:
..
DS = airports.aa IN DS 220 8 2 09992c9728d682de6029bb4c3bba6a51f0976fa
c84c0972eab371423218814e0 ; ( SHA256 digest )
..

I then copy that DS record (or the DNSKEY of the KSK if you prefer)
into a file which I configure in [16]Unbound as follows:
auto-trust-anchor-file: "/usr/local/etc/unbound/airports.aa.anchor"
..
stub-zone:
       name: "airports.aa"
       stub-addr: 192.168.1.114  # PowerDNS

Querying this [17]Unbound server shows us a validated response (+ad
flag) and the data.
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 4, AUTHORITY: 0, ADDITIONAL:
1

;; ANSWER SECTION:
sin.airports.aa.        60 IN TXT "Singapore Changi International Airp
ort"
sin.airports.aa.        60 IN RRSIG TXT 8 3 60 (
                               20151112000000 20151022000000 3340 air
ports.aa.
                               FTH/xwY2VHm9B98RQrNp8mO8Ldo9iEX5xoWgZz
Tyzcw5
                               QzjjdZrjrCmrdPBXUqHVWF4hy7nblmckCn55Py
BYi+nO
                               nVwtNmgjWEsHvjxwvi7Yaxvf8lb7RtpOmOVw83
AIBw5t
                               zsH5JwP4LRQIV3aik/NjBUKs4J1tN2eHPxeaJB
Q= )
sin.airports.aa.        60 IN LOC 1 21 40.223 N 103 59 24.734 E 0.00m
1m 10000m 10m
sin.airports.aa.        60 IN RRSIG LOC 8 3 60 (
                               20151112000000 20151022000000 3340 air
ports.aa.
                               hp4Mks4DVTqxWS64k+xNTl97jEeA3JFpBC2orE
7R4HlI
                               DVUrv/b7IB+kq4Ed9I4gx7Tw8IdliGjqNeIsCS
wC7XiQ
                               v6p5vKyQ2g9r3lDq257rnAXmKZzpRClxSU82J2
/f8wX3
                               X3gPvxetr6cmfZ74rhWk+4IXsViFUPp7Dt3kqJ
0= )

Not-quite so lazy DNSSEC

It turns out it's a bit of a mystery why this works at all, or rather
it may not actually be supposed to work: our friends at PowerDNS do
not actually test for the ability to have keys in one back-end and DNS
data in a second. In other words, I have to overcome my laziness and
attempt to do this properly.

If we configure PowerDNS to have only the one remote back-end or
launch it before gmysql we have to implement more functions, in
particular those which provide domain metadata and key material to the
server. I've done this experimentally and it appears to work. When a
client queries PowerDNS for an existing name, we are asked all of
this:
GET /lookup/ibz.aereo.aa/SOA
GET /lookup/aereo.aa/SOA
GET /lookup/ibz.aereo.aa/NS
GET /lookup/ibz.aereo.aa/ANY
GET /getDomainMetadata/aereo.aa/PRESIGNED
GET /getDomainKeys/aereo.aa/0

getDomainKeys must return one or more DNS keys in BIND Private Key
format 2 which are easily created with
ldns-keygen -a RASHA256 -b 2048 -k aereo.aa

I then simply open the .private key file and return the ASCII blob I
find there. (I did say "lazy", didn't I?)
@get("/getDomainKeys/:qname/:kind")
def getDomainKeys(qname, kind):
   ''' all zones get the same key '''

   # ldns-keygen -a RSASHA256 -b 2048 -k airports.aa
   privkey = open("Kaereo.aa.+008+09736.private").read()
   key = {
       "id"    : 1,
       "flags" : 257,
       "active" : True,
       "content" : privkey,
   }

   return dict(result=[ key ])

With a bit more work we'd have some sort of nice utility which creates
keys and drops them into a data store from which we subsequently serve
them. Alternatively, we can implement addDomainKey and use the pdnssec
secure-zone utility to generate the keys and store them therein. The
following command submits a PUT request to our back-end script:
$ pdnssec add-zone-key aereo.aa zsk
Added a ZSK with algorithm = 8, active=0
@put("/addDomainKey/:zone")
def addDomainKey(zone):
   ''' accept a key from pdnssec '''
   active = request.params.get('active')
   keyblob = request.params.get('content')
   flags = request.params.get('flags')  # 256/257

   print "Receiving key (%s) for %s" % (flags, zone)

   f = open("key-%s.private" % zone, "w")
   f.write(keyblob)
   f.close()

   return dict(result=True)

If we're asked for a non-existent name NSEC/NSEC3 come into the spiel:
GET /lookup/xnada.aereo.aa/SOA
GET /lookup/aereo.aa/SOA
GET /lookup/xnada.aereo.aa/NS
GET /lookup/xnada.aereo.aa/ANY
GET /lookup/*.aereo.aa/ANY
GET /getDomainMetadata/aereo.aa/PRESIGNED
GET /getDomainKeys/aereo.aa/0
GET /getDomainMetadata/aereo.aa/NSEC3PARAM
GET /getDomainMetadata/aereo.aa/NSEC3NARROW
GET /lookup/aereo.aa/SOA
GET /lookup/aereo.aa/ANY
GET /getDomainMetadata/aereo.aa/SOA-EDIT
@get("/getDomainMetadata/:qname/:kind")
def getDomainMetadata(qname, kind):

   res = "0"
   if kind == 'NSEC3PARAM':
       res = "1 0 5 DEADBE"
   elif kind == 'NSEC3NARROW':
       res = "1"

   return dict(result=[res])

Here, my getDomainMetadata function says "No" when asked whether it's
pre-signed, responds with 1 0 5 DEADBE when asked for the NSEC3PARAM
record, and it responds with 1 when asked wether to do NSEC3NARROW
(for the simple reason that I cannot be bothered to implement the
getBeforeAndAfterNamesAbsolute routine).

So this seems to work: when I query my validating [18]Unbound server,
I see:
;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL:
1
;; ANSWER SECTION:
ibz.aereo.aa.           60      IN      TXT     "Ibiza Airport"

And if you're wondering where all this is documented, that is a very
good question. The [19]remote back-end is well documented, but the how
and why which routine is invoked by PowerDNS proper is hard to come
by. I was lucky to have a longish rant session^W^Wchat with the chaps
at PowerDNS who did their best to push me in the right directions - I
herewith claim all mistakes and omissions.

Aki has a Python package called [20]remotebackend-python which helps
in building scripts for the remote back-end, and a
[21]remotebackend-gem which is similar for Ruby.

References

  1. https://doc.powerdns.com/md/authoritative/backend-pipe/
  2. http://www.powerdns.com/
  3. https://doc.powerdns.com/md/authoritative/backend-remote/
  4. http://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions
  5. http://en.wikipedia.org/wiki/Transport_Layer_Security
  6. http://json.org/
  7. https://en.wikipedia.org/wiki/International_Air_Transport_Association_airport_code
  8. http://jpmens.net/2010/11/14/where-is-your-dns-server-located/
  9. https://github.com/jpmens/pdns-remote-airports
 10. http://www.bortzmeyer.org/query-loc.html
 11. http://jpmens.net/2010/11/14/where-is-your-dns-server-located/
 12. http://en.wikipedia.org/wiki/Domain_Name_System_Security_Extensions
 13. https://twitter.com/akituomi
 14. https://github.com/cmouse/pdns-v6-autorev
 15. http://jpmens.net/2015/03/30/powerdns-with-a-smartcard-hsm-for-dnssec/
 16. http://unbound.net/
 17. http://unbound.net/
 18. http://unbound.net/
 19. https://doc.powerdns.com/md/authoritative/backend-remote/
 20. https://github.com/cmouse/pdns-remotebackend-python
 21. https://github.com/cmouse/pdns-remotebackend-gem