The PIPE back-end has been a part of 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 REMOTE back-end has a lot more features than the PIPE back-end. For one, it can do full 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 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 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 IATA airport codes and their locations which we return as a DNS LOC record.

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 10000m 10m

In order to activate the remote back-end, I configure the following in pdns.conf:

# gmysql-dnssec
# 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 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

  "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, for example, the HTTP queries would have had .xyz added to them (e.g. /lookup/LAX.airports.aa/ which might be useful to return something static, or when each of your queries are to be handled by their own PHP script.

Using query-loc, which we've 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


But what about 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 Aki Tuomi, the author of this back-end, has a complete example in Perl called autorev which demonstrates this. (BTW, Aki is the same person who 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
DS = airports.aa IN DS 220 8 2 09992c9728d682de6029bb4c3bba6a51f0976fac84c0972eab371423218814e0 ; ( SHA256 digest )

I then copy that DS record (or the DNSKEY of the KSK if you prefer) into a file which I configure in Unbound as follows:

auto-trust-anchor-file: "/usr/local/etc/unbound/airports.aa.anchor"
        name: "airports.aa"
        stub-addr:  # PowerDNS

Querying this 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

sin.airports.aa.        60 IN TXT "Singapore Changi International Airport"
sin.airports.aa.        60 IN RRSIG TXT 8 3 60 (
                                20151112000000 20151022000000 3340 airports.aa.
                                zsH5JwP4LRQIV3aik/NjBUKs4J1tN2eHPxeaJBQ= )
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 airports.aa.
                                X3gPvxetr6cmfZ74rhWk+4IXsViFUPp7Dt3kqJ0= )

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?)

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
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")

    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
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 Unbound server, I see:

;; flags: qr rd ra ad; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
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 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 remotebackend-python which helps in building scripts for the remote back-end, and a remotebackend-gem which is similar for Ruby.

Flattr this
DNS and DNSSEC :: 03 Nov 2015 :: e-mail


blog comments powered by Disqus