TTR Networks

Intermediate · PowerDNS on Debian: authoritative, secondary, DNSSEC and recursor · section 10 of 10

Build the lab yourself

✓ Lab-tested 21 October 2026 on Debian 13.7, PowerDNS Authoritative 4.9.17, PowerDNS Recursor 5.2.13, BIND dig 9.20.29

The lab runs on any machine with Docker, including an Apple Silicon Mac, in well under a gigabyte of memory. The four DNS servers run real systemd, so every systemctl, journalctl, pdnsutil and pdns_control command in this lesson works exactly as on a Debian server.

  1. Put the files from this page in one directory, with ns1.conf, ns2.conf, rootns.conf, recursor.conf and lab-root.hints from earlier in the lesson.
  2. docker compose -f lab-compose.yaml up -d --build, then ./lab-setup.sh to address every node.
  3. On each authoritative server, create the database, copy its configuration into /etc/powerdns/pdns.d/ (docker compose cp), and restart pdns. Then follow the lesson: zones on ns1, secondary zones on ns2, the root and net zones on rootns, then DNSSEC.
  4. The recursor's trust anchor must be your lab root's DS, from pdnsutil export-zone-ds . on rootns, because the keys are generated fresh in every lab.
  5. Query from client, which uses the recursor, and from outsider, which isn't allowed to.

notify-test.sh is the script that timed NOTIFY. It runs from the lab directory on your own machine.

lab-compose.yaml
# PowerDNS on Debian: the lab.  docker compose -f lab-compose.yaml up -d --build && ./lab-setup.sh
name: pdns-lab
x-systemd: &systemd                     # Debian with systemd as PID 1
  privileged: true
  cgroup: host
  volumes: ["/sys/fs/cgroup:/sys/fs/cgroup:rw"]
  tmpfs: ["/run", "/run/lock"]
x-host: &host
  build: { context: ., dockerfile: Dockerfile.host }
  image: ttr-dns-host:13
  privileged: true
services:
  rootns:   { <<: *systemd, build: { context: ., dockerfile: Dockerfile.auth }, image: ttr-pdns-auth:13, hostname: rootns, networks: { internet: { interface_name: eth0 } } }
  ns1:      { <<: *systemd, build: { context: ., dockerfile: Dockerfile.auth }, image: ttr-pdns-auth:13, hostname: ns1, networks: { internet: { interface_name: eth0 } } }
  ns2:      { <<: *systemd, build: { context: ., dockerfile: Dockerfile.auth }, image: ttr-pdns-auth:13, hostname: ns2, networks: { internet: { interface_name: eth0 } } }
  recursor: { <<: *systemd, build: { context: ., dockerfile: Dockerfile.recursor }, image: ttr-pdns-rec:13, hostname: recursor, networks: { office: { interface_name: eth0 } } }
  gw:       { <<: *host, hostname: gw, networks: { office: { interface_name: eth0, priority: 100 }, internet: { interface_name: eth1 } } }
  client:   { <<: *host, hostname: client, networks: { office: { interface_name: eth0 } } }
  outsider: { <<: *host, hostname: outsider, networks: { internet: { interface_name: eth0 } } }
# Layer-2-only networks: every address and route comes from lab-setup.sh.
networks:
  internet: { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:71::/64" }] } }
  office:   { enable_ipv4: false, enable_ipv6: true, ipam: { config: [{ subnet: "fd00:72::/64" }] } }
lab-setup.sh
#!/bin/sh
# Address every node and give it its routes. Run after `docker compose up`.
x() { docker compose -f lab-compose.yaml exec -T "$1" sh -c "$2"; }
for n in rootns ns1 ns2 recursor gw client outsider; do x $n 'sysctl -qw net.ipv6.conf.all.disable_ipv6=1'; done
# The "internet": a lab root server and the company's two authoritative servers.
x rootns   'ip addr add 203.0.113.1/24 dev eth0;  ip route add 10.10.0.0/24 via 203.0.113.254'
x ns1      'ip addr add 203.0.113.53/24 dev eth0; ip route add 10.10.0.0/24 via 203.0.113.254'
x ns2      'ip addr add 203.0.113.54/24 dev eth0; ip route add 10.10.0.0/24 via 203.0.113.254'
x outsider 'ip addr add 203.0.113.99/24 dev eth0; ip route add 10.10.0.0/24 via 203.0.113.254'
x gw       'ip addr add 10.10.0.1/24 dev eth0; ip addr add 203.0.113.254/24 dev eth1; sysctl -qw net.ipv4.ip_forward=1'
# The office: the recursor and a client that uses it.
x recursor 'ip addr add 10.10.0.53/24 dev eth0; ip route add default via 10.10.0.1'
x client   'ip addr add 10.10.0.20/24 dev eth0; ip route add default via 10.10.0.1; echo "nameserver 10.10.0.53" > /etc/resolv.conf'
Dockerfile.auth
# A PowerDNS Authoritative Server: Debian 13 with systemd, so pdns runs as a normal service.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      systemd systemd-sysv pdns-server pdns-backend-sqlite3 sqlite3 \
      bind9-dnsutils curl iproute2 iputils-ping procps less \
 && rm -rf /var/lib/apt/lists/* \
 && systemctl mask systemd-modules-load.service
CMD ["/sbin/init"]
Dockerfile.recursor
# The PowerDNS Recursor that office clients use: Debian 13 with systemd.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      systemd systemd-sysv pdns-recursor bind9-dnsutils iproute2 iputils-ping procps less \
 && rm -rf /var/lib/apt/lists/* \
 && systemctl mask systemd-modules-load.service
CMD ["/sbin/init"]
Dockerfile.host
# Clients and the router: Debian 13 with dig.
FROM debian:trixie-slim
RUN apt-get update && apt-get install -y --no-install-recommends \
      bind9-dnsutils iproute2 iputils-ping procps \
 && rm -rf /var/lib/apt/lists/*
CMD ["sleep", "infinity"]
/etc/powerdns/pdns.d/rootns.conf
# /etc/powerdns/pdns.d/rootns.conf: the lab's own root and .net servers.
launch=gsqlite3
gsqlite3-database=/var/lib/powerdns/pdns.sqlite3
gsqlite3-dnssec=yes
local-address=203.0.113.1

Where next

  • Linux Network ConnectivityPrerequisite · Beginner · 80 min

    Find out why a Linux machine can't reach something, layer by layer: interfaces and addresses, routes and ip route get, ARP and the neighbour table, ping and traceroute, DNS, and listening sockets — with real output from a three-host lab.

  • Office gateway: DHCP, DNS and VRRP on VyOSAlso covers DNS · Beginner · 40 min

    Two VyOS routers serving one office LAN: VRRP for a gateway that survives a router failure, DHCP with split pools and a printer reservation, and DNS forwarding. Then a real failover, the DNS outage it exposed, and the fix.

  • OpenVPN on Debian: users, routing and revocationAlso covers debian · Intermediate · 60 min

    Install and run an OpenVPN server on Debian with your own certificate authority: one certificate per user, fixed addresses, routing to an office and a branch LAN, split and full tunnels, tls-crypt, revoking users, and the mistakes that lock everyone out.

  • WireGuard on Debian: users, routing and keysAlso covers debian · Intermediate · 50 min

    Run a WireGuard server on Debian for several users, an office LAN and a branch: keys, wg-quick and systemd, AllowedIPs as routing and access control, split and full tunnels, roaming and keepalives, adding and removing users live, and the silent failures.