JMM’s notes on

PowerDNS

An authoritative nameserver (and I guess more)

PowerDNS seems pretty flexible. I’m just looking for a way to serve my own DNS since my registrar doesn’t support SSHFP records among other things. Oh, also I wanted to be able to get a wildcard cert from LetsEncrypt, so I’d need to do a DNS-01 challenge.

I’m just playing around with PowerDNS for now, since I’m actually pretty bad in terms of understanding how DNS works.

The Nix package is powerdns, so you can try:

$ nix-shell -p powerdns

$ pdns_server --help
syntax:

  --8bit-dns | --8bit-dns=yes | --8bit-dns=no
	Allow 8bit dns queries
  --allow-axfr-ips=...
	Allow zonetransfers only to these subnets
  --allow-dnsupdate-from=...
	A global setting to allow DNS updates from these IP ranges.
  --allow-notify-from=...
	Allow AXFR NOTIFY from these IP ranges. If empty, drop all incoming notifies.
  --allow-unsigned-autoprimary=...
	Allow autoprimaries to create zones without TSIG signed NOTIFY
  --allow-unsigned-notify=...
	Allow unsigned notifications for TSIG secured zones

Configuration

I wanted to set up a pretty simple example to play around with PowerDNS without being root. By default PowerDNS is going to try to load stuff from /etc/pdns/pdns.conf, and I don’t really want to edit a file there. Instead, I want to try modifying some stuff in some directory I control. So, to do this, I load configuration from the current directrory like so:

pdns_server "--socket-dir=$(pwd)" "--config-dir=$(pwd)/"

Then you’ll need to have a pdns.conf in this directory. Here’s a basic one that tries to load a BIND configuration:

# A super basic and bad PowerDNS config
launch=bind
bind-config=named.conf
local-address=127.0.0.1
local-port=5300
webserver=yes
webserver-port=8989
webserver-address=127.0.0.1
webserver-allow-from=127.0.0.1,::1
log-dns-details
log-dns-queries
loglevel=5
# This is just so I filter out timestamps when posting stuff online
log-timestamp=no

Logging here was pretty helpful for figuring out what’s going wrong. I struggled for 20 minutes because it turned out that the bind backend wasn’t loading my config and just silently failing. This caused pdns to think it wasn’t authoritative for some jmm.example domain I was trying.

Here’s the named.conf:

zone "jmm.example" IN {
     type master;
     file "jmm.example.zone";
     allow-update { none; };
};

And here’s jmm.example.zone:

$TTL 86400
@   IN  SOA ns1.jmm.example. admin.jmm.example. (
            2024071110 ; serial number
            3600       ; refresh
            1800       ; retry
            604800     ; expire
            86400 )    ; minimum

; Name servers
jmm.example.	IN	NS	ns1.jmm.example.
jmm.example.	IN	NS	ns2.jmm.example.

ns1		IN	A	192.0.2.1
ns2		IN	A	192.0.2.2

@   IN  NS   jmm.example.
@   IN	A    127.0.0.1
@   IN 	AAAA ::1

www  IN A    127.0.0.5
www  IN	TXT  jmm=Josh Moller-Mara
www  IN	TXT  hello=World

Now, to actually start running pdns_server here, I do:

[nix-shell]$ pdns_server "--socket-dir=$(pwd)" "--config-dir=$(pwd)/"
Loading '/nix/store/lf8642l65iigdjr57lm8kv1cjb59ir2h-pdns-4.8.4/lib/pdns/libbindbackend.so'
This is a standalone pdns
Listening on controlsocket in '/░░░░░░░░░░░░░░░░░░░░░/powerdns/pdns.controlsocket'
UDP server bound to 127.0.0.1:5300
TCP server bound to 127.0.0.1:5300
PowerDNS Authoritative Server 4.8.4 (C) 2001-2022 PowerDNS.COM BV
Using 64-bits mode. Built using gcc 12.3.0.
PowerDNS comes with ABSOLUTELY NO WARRANTY. This is free software, and you are welcome to redistribute it according to the terms of the GPL version 2.
[webserver] Listening for HTTP requests on 127.0.0.1:8989
Polled security status of version 4.8.4 at startup, no known issues reported: OK
[bindbackend] Parsing 1 domain(s), will report when done
[bindbackend] Done parsing domains, 0 rejected, 1 new, 0 removed
Creating backend connection for TCP
About to create 3 backend threads for UDP
Done launching threads, ready to distribute questions

Then, to query pdns, I do:

$ dig +norecurse ANY www.jmm.example @127.0.0.1 -p 5300

; <<>> DiG 9.18.24 <<>> +norecurse ANY www.jmm.example @127.0.0.1 -p 5300
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 20717
;; flags: qr aa; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;www.jmm.example.		IN	ANY

;; ANSWER SECTION:
www.jmm.example.	86400	IN	TXT	"hello=World"
www.jmm.example.	86400	IN	TXT	"jmm=Josh Moller-Mara"
www.jmm.example.	86400	IN	A	127.0.0.5

;; Query time: 1 msec
;; SERVER: 127.0.0.1#5300(127.0.0.1) (TCP)
;; WHEN: Thu Jul 11 ░░░░░░░░ PDT 2024
;; MSG SIZE  rcvd: 117

PostgreSQL backend

See the documentation for the generic PostgreSQL backend. A schema for the PostgreSQL backend is at https://github.com/PowerDNS/pdns/blob/master/modules/gpgsqlbackend/schema.pgsql.sql.

NixOS

NixOS already has a services.powerdns option, but here’s a modified version of basically that:

{
  config,
  lib,
  pkgs,
  ...
}:

with lib;

let
  cfg = config.services.jmm-powerdns;
  configOut = pkgs.runCommand "pdnsconf" {} ''
mkdir -p $out
cp ${./pdns.conf} $out/pdns.conf
cp ${./named.conf} $out/named.conf
cp ${./jmm.example.zone} $out/jmm.example.zone
'';
in
{
  options = {
    services.jmm-powerdns = {
      enable = mkEnableOption "JMM’s PowerDNS domain name server";

    };
  };

  config = mkIf cfg.enable {

    # This sets the default `/etc/systemd/system/pdns.service`, which you probably want otherwise.
    # If you use this, other settings are created via an override.
    # systemd.packages = [ pkgs.pdns ];

    systemd.services.pdns-jmm = {
      description = "JMM PowerDNS authoritative setup";
      wantedBy = [ "multi-user.target" ];
      after = [
        "network.target"
        "network-online.target"
        "postgresql.service"
      ];
      wants = [ "network-online.target" ];

      serviceConfig = {
        Type = "notify";
        Restart = "on-failure";
        RestartSec = "1";
        # StartLimitInterval = "0";
        RuntimeDirectory = "pdns";

        AmbientCapabilities = "CAP_NET_BIND_SERVICE";
        CapabilityBoundingSet = "CAP_NET_BIND_SERVICE";

        User = "pdns";
        Group = "pdns";
        LockPersonality = true;
        NoNewPrivileges = true;
        PrivateDevices = true;
        PrivateTmp = true;
        ProtectHome = true;
        ProtectClock = true;
        ProtectControlGroups = true;
        ProtectHostname = true;
        ProtectKernelLogs = true;
        ProtectKernelModules = true;
        ProtectKernelTunables = true;
        ProtectSystem = "full";
        RestrictAddressFamilies = "AF_UNIX AF_INET AF_INET6";
        RestrictSUIDSGID = true;
        SystemCallArchitecture = "native";
        SystemCallFilter = "~ @clock @debug @module @mount @raw-io @reboot @swap @cpu-emulation @obsolete";
        ProtectProc = "invisible";
        TasksMax = "50";
        MemoryMax = "512M";

        WorkingDirectory = "${configOut}";

        ExecStart = [
          # "${pkgs.pdns}/bin/pdns_server --config-dir=${configOut} --guardian=no --daemon=no --disable-syslog --log-timestamp=no --write-pid=no"
          "${pkgs.pdns}/bin/pdns_server --config-dir=${configOut} --guardian=no --daemon=no --log-timestamp=no --write-pid=no"
        ];
      };
    };

    users.users.pdns = {
      isSystemUser = true;
      group = "pdns";
      description = "PowerDNS";
    };

    users.groups.pdns = { };

  };
}

Misc