More fun with Freeipa and DNS: Funky records

Posted on Apr 24, 2018

After all the fun we had in part 1 of our FreeIPA + DNS series, we now come to a new chapter.

During the move of openSUSE to Let’s Encrypt we used the excellent tool from ssllabs to verify each server after we changed the certificate. One of our coworkers noted “Hey, why don’t you have a CAA record. It would give you an even better score”. CAA records allow the domain owner to specify which certificate authorities are allowed to create certificates for this domain. Which seems to be a nice benefit for little work. For a longer explanation see this blog post from Qualys.

Of course nothing is easy in life … so the bug against FreeIPA is open for quite a while now. So thomic had the fun idea “Couldn’t we do this with the powerdns thing that we already have?”

When we looked at the powerdns documentation, I noticed they had a much nicer syntax now to access the fields of a zone entry. Time to update to pdns 4.1!

Step 1: port the script

So we take this little script

function axfrfilter(remoteip, zone, qname, qtype, ttl, prio, content)
   resp = {}
   if qtype == pdns.NS and prio == "only.internal.ns.example.com" then
      -- skip this record
      return 0, resp
   end
   -- preserve all others
   return -1, resp
end

… and rewrite it to …

function axfrfilter(remoteip, zone, record)
   resp = {}
   if record:qtype() == pdns.NS and record:content() == "only.internal.ns.example.com" then
      -- skip this record
      return 0, resp
   end
   -- preserve all others
   return -1, resp
end

Restarted pdns and changed the zone in freeipa.

(PDNSException): Cannot understand return code 0 in axfr filter response

Did i mention already … nothing is easy in life.

Seems we found a minor bug in pdns, which luckily had a pull request attached already. Applied the patch to the package, rebuild it and deployed to the server. Ok our ported script now worked! Thank you Aki Tuomi for helping to track this down. :D

♥ The new API for records ♥

Step 2: Adding the CAA records

Missing in the script is above is another “if” to fix our SOA record. Normally FreeIPA would add its own hostname there. Which in our case is not reachable to the outside. So we replace the internal hostname with one of our 3 external NS.

function axfrfilter(remoteip, zone, record)
   resp = {}
   if record:qtype() == pdns.NS and record:content() == "only.internal.ns.example.com" then
     -- skip this record
     return 0, resp
   end
  
   if record:qtype() == pdns.SOA then
     content=string.gsub(record:content(), "freeipa.internal.example.com", "ns1.example.com")
     resp[1] = {
        qname   = record:qname():toString(),
        qtype   = record:qtype(),
        ttl     = record:ttl(),
        content = content
     }
     return 0, resp
   end 

   -- preserve all others
   return -1, resp
end

This example is almost copied from the powerdns documentation. Next stop … more records.

function axfrfilter(remoteip, zone, record)
   resp = {}
   if record:qtype() == pdns.NS and record:content() == "only.internal.ns.example.com" then
     -- skip this record
     return 0, resp
   end
  
   if record:qtype() == pdns.SOA then
     content=string.gsub(record:content(), "freeipa.internal.example.com", "ns1.example.com")
     resp[1] = {
        qname   = record:qname():toString(),
        qtype   = record:qtype(),
        ttl     = record:ttl(),
        content = content
     }
     resp[2] = {
        qname   = record:qname():toString(),
        qtype   = pdns.CAA,
        ttl     = record:ttl(),
        content = "128 issue \"letsencrypt.org\""
     } 
     resp[3] = {
        qname   = record:qname():toString(),
        qtype   = pdns.CAA,
        ttl     = record:ttl(),
        content = "128 issue \"Digicert.com\""
     } 
     resp[4] = {
        qname   = record:qname():toString(),
        qtype   = pdns.CAA,
        ttl     = record:ttl(),
        content = "128 iodef \"mailto:admin@example.com\""
     }
     -- replace and append
     return 1, resp
   end

   -- preserve all others
   return -1, resp
end

The nice part of hooking onto the SOA record for a zone, each zone has this record exactly once. We move the return code of the “SOA” branch to 1, as we want to replace the input record and append new ones. 0 is just replacing the record, and -1 just uses the input record as is.