Fun with FreeIPA and a slightly more complex DNS setup

Posted on Mar 31, 2017

The Plan

+---------+    +------------------------+    +---------------------------+
| FreeIPA | -> | upstream hidden master | -> | public facing dns servers |
+---------+    +------------------------+    +---------------------------+

Sounds simple enough right? Well …

The Fun

Let’s get right too it … FreeIPA only sends out notifications to the NS records listed in the zone. But our hidden master is not reachable from the outside and should not be listed as an NS.

‘But bind has “also-notify” just use that.’ you might say now. Which is correct. So a quick check on the ldap scheme reveals there is no setting in the LDAP tree for it. Ok… the nice solution is dead.

Let’s try the ugly one.

Notify settings in the global scope. It means we will notify our hidden master also about zones it should not handle, but that should not cause any problems besides maybe some unneeded log messages. Not too bad.

Added the settings. Fired up tshark again. Restarted named-pkcs11.

And guess what … it just notified the NS servers again. The also-notify setting only caused notifications for the zones defined in the named.conf.

This smells like a regression in FreeIPA (lack of notify settings per zone) and bind-dyndb-ldap (ignoring the global notify settings).

This is documented in Bug #6791.

The solution

+---------+    +------------------+    +------------------------+    +---------------------------+
| FreeIPA | -> | PDNS with script | -> | upstream hidden master | -> | public facing dns servers |
+---------+    +------------------+    +------------------------+    +---------------------------+

Well the workaround. I remembered that powerdns has lua scripting in basically all their daemons nowadays. A quick check revealed there was also a hook for AXFR. We fired up a new VM and installed pdns with sqlite3 on it.

The config:

# grep -vE '^(#.*|\s*)$' /etc/pdns/pdns.conf
allow-axfr-ips=<ip of hidden master>/32,127.0.0.0/8,::1
also-notify=<ip of hidden master>
launch=gsqlite3
gsqlite3-database=/var/lib/pdns/superslave.db
gsqlite3-pragma-synchronous=0
gsqlite3-pragma-foreign-keys=1
only-notify=<ip of hidden master>/32
slave=yes
slave-renotify=yes

And register the zone with:

pdnsutil create-slave-zone example.com <ip of freeipa>
# verify it works with:
dig -t AXFR @<ip of freeipa> example.com
dig -t AXFR @127.0.0.1 example.com

Now to the lua script. The documentation for the axfrfilter is here. So we copy that example and replace most of the stuff with our NS conditional.

--- /etc/pdns/filter-internal-ns.lua v1
function axfrfilter(remoteip, zone, qname, qtype, ttl, prio, content)
  if qtype == pdns.NS and content == "only.internal.ns.example.com" then
    -- skip this record
    resp = {}
    return 0, resp
  end 
end

To enable the script we insert the following record in our little sqlite DB:

INSERT INTO domainmetadata (domain_id, kind, content)
       VALUES (3, "LUA-AXFR-SCRIPT", "/etc/pdns/filter-internal-ns.lua");

Did some dummy change in the zone and suddenly our pdns instance did not feel responsible for the zone anymore. What happened?

During cleanup I deleted a little bit too much:

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

This looked better. Our pdns felt authorative again for the zone. Though the NS record was still there. A short pondering later … comparing “qname” to the content of the NS record was wrong. We should compare “content”! Changed the line. And now the script reported trying to compare nil values. A few debug prints later … our value was in “prio”. Wait what? Maybe a better signature would be:

function axfrfilter(remoteip, zone, qname, qtype, ttl, field1, field2)

Anyway … v3:

--- /etc/pdns/filter-internal-ns.lua v3
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
dig -t AXFR @127.0.0.1 example.com

Now reported 1 NS record less than our freeipa. The logs (and tshark) confirmed that our upstream hidden master received the changes and shortly after also our public servers.

Time to call it a day.

A little extra fun with capability sets

Ok need to mention one more thing before. … Of course pdns did not start up right away in my setup. I knew the pdns.service file had some security measures enabled. In a first run i just copied pdns.service to /etc/systemd/system and disabled them all. PrivateTmp, PrivateDevices, CapabilityBoundingSet, NoNewPrivileges, ProtectSystem, ProtectHome all gone. “systemctl daemon-reload” and i could focus on my real problem before fighting with this.

Later on i wanted a better solution.

  1. use /etc/pdns/pdns.service.d/
  2. maybe upstream the needed fix

So I reenabled all options and looked at our permissions. An upstream dev told me it should actually work if the DB is in /var/lib/pdns/. This lead me to look at the permissions again. They looked fine too:

drwxr-x--- 2 pdns pdns 6 Nov 16 01:33 /var/lib/pdns

And there it was …. root cant read the /var/lib/pdns … what if pdns tries to open the DB file as root? From fun with apparmor I knew that root reading such files requires the DAC override capability. Quickly creating a config file for it and daemon-reload systemd:

# /etc/systemd/system/pdns.service.d/capset.conf
[Service]
CapabilityBoundingSet=CAP_NET_BIND_SERVICE CAP_SETGID CAP_SETUID CAP_CHOWN CAP_SYS_CHROOT CAP_DAC_OVERRIDE

Voila … my pdns starts up again. The package in server:dns/pdns already has the fix and we will upstream the patch.

And now it is time for the cinema.

P.S.: In pdns 4.1 the API for axfrfilter changed. Please see part 2 on how to port your scripts to the new API. And yes the new API is so much better.