Domain Name Server mit PowerDNS – Schritt 1 – Konzept, Netzwerk und Authorative Server

Nachdem ich ja schon einiges in der letzten Zeit über DNS und Co geschrieben habe, geht es diesmal um eine konkrete Umsetzung. Lange Zeit habe ich schon einen eigenen DNS-Server betrieben, damals noch mit BIND. Mit den letzten Serverumzügen habe ich das dann der Einfachheit halber meinem Hosting-Anbieter überlassen (das richte ich ein, wenn der Rest läuft …). Wie das mit aufgeschobenen Aufgaben so ist, irgendwann wird der Status Quo zu einen “Dauerorium” (die verstetigte Version eines Provisoriums).

Nun wollte ich es endlich angehen, und dabei auch einige zusätzliche Funktionen nachzurüsten. Unter anderem einen eigenen Ipv6 fähigen DynDNS-Service. Nach etwas Recherche stand ein grober Plan:

  • PowerDNS mit MySQL Backend
  • MySQL Server (läuft bereits, bekommt nur ein zusätzliches Schema)
  • Ein (Dyn)DNS-Frontend zur einfacheren Verwaltung und als HTTP(s) Schnittstelle zum Updaten
  • Microservice-Ansatz für die Einzelteile mit Docker als Paravirtualisierung
  • soweit möglich alles nativ in IPv6

Docker Netzwerk vorbereiten

Für alle DNS-Container habe ich mir erst einmal ein getrenntes Docker IPv6-Netzwerk eingerichtet, welches ein Subnetz meines allgemeine DockerSubnetzes ist, das wiederum sich innerhalb des mir zugewiesenen IPv6Prefix befindet (wie groß man sich das Prefix wählt ist Geschmackssache, mit /112 hat man immer noch 16 Bit zur fast freien Verfügung (65536 Möglichkeiten, so viele Container brauche ich für da Beispiel hier nicht). Man sollte sich hier wie gewohnt von der CIDR-Notation und den “Mythen” um IPv6 nicht ins Boxhorn jagen lassen. Zu Docker und IPv6 habe ich ja schon einen Artikel verfasst, für die automatische Vergabe von IPv6 durch Docker sollte man /88 nicht unterschreiten, aber in unserem Fall sind derartige Automatismen eher sogar etwas unpraktisch,

docker network create --ipv6 --subnet 2001:db8:ffff:ffff:ffff:ffff:0053::/112 dns

PowerDNS – Microservices von Haus aus

PowerDNS bringt schon seit Beginn eine klassiche Aufgabenteilung mit, lange bevor man von Microservices oder gar Docker sprach. Auch früher konnte man bereits ja mehrere Services laufen lassen, wenn auch auf unterschiedlichen Ports. PowerDNS teilt sich daher in drei Teile:

  • Authorative Server – beantwortet DNS-Anfragen zu Bereichen für die er als zuständig konfiguriert ist
  • Recursor – löst Anfragen nach beliebigen Hosts auf und speichert sie im Cache
  • DNSdist – eine Art Load-Balancer / Reverseproxy für DNS-Anfragen

Der “klassische” Aufbau eines singulären DNS-Servers mit PowerDNS funtkioniert wie folgt: Der DNSDist hört auf Port 53 (dem DNS-Port) und reicht seine Anfragen an entsprechend konfigurierte Authoratives bzw. Recursor-Prozesse weiter. Da man Ports nur einmal verwenden kann, nimmt man in der Regel einfach einen nicht privelegierten Port (> 1024) für die nachgelagerten Prozesse wie den Authorative und den Recursor. Wenn man in der komfortablen Lage war, dass man eine ganze Serverfarm sein Eigenen nennt konnte man auch dort natürlich schon nachgelagerte Server eintragen (die noch nicht einmal mit PowerDNS laufen mussten).

In der modernen Docker-Welt nimmt man gemäß des Prinzips “ein Container = ein Prozess/Service” entsprechende Container. Auch hier wäre es prinzipiell möglich die Container auf mehreren verschiedenen Hosts zu verteilen z.B. um eine höhere Ausfallsicherheit zu bekommen. Ich gehe im Folgenden aber eher von der “Sparversion” für ein Hobby-Projekt aus, bei der man ein wenig aufs Geld achten muss und daher erst mal nur einen physikalischen (oder auch virtualisierten) Server zur Verfügung hat.

PowerDNS Authorative Server aufsetzen

Wie in Docker üblich ist es kein großes Hexenwerk sich ein eigenes Dockerfile zusammen zu stellen mit dem man den Service laufen lassen möchte:

FROM pschiffe/pdns-mysql
COPY pdns.conf /etc/pdns/pdns.conf
ENTRYPOINT [ "/usr/sbin/pdns_server" ]

In die Konfigurationsdatei enthält in diesem Fall alles was wir einstellen müssen – man könnte diese Werte ggf. auch per Environment-Variable “reinreichen” aber das würde an dieser Stelle zu weit führen – diese Thematik werde ich in einem meiner nächsten Artikel einmal näher betrachten. Ich bin mir hierbei im Klaren, was ich tue – aber: ein Schritt nach dem anderen ist zum Verständnis leichter als hier auch noch das Fass der Credentials aufzumachen. Das fertige Image kann ich laufen lassen, aber ich darf es eben höchstens in eine private Docker-Registry packen – denn in der Datei sind ggf. ja Passwörter und Co hinterlegt.

# MySQL Configuration
#
# Launch gmysql backend
launch=gmysql

# gmysql parameters
gmysql-host=2001:db8:ffff:ffff:ffff:ffff:0053:0002
gmysql-port=3306
gmysql-dbname=powerdns
gmysql-user=my_powerdns_user
gmysql-password=#####SECRET_PW######
gmysql-dnssec=yes

#Mode of operation
master=yes
dnsupdate=yes

#Webserver-Stuff
webserver=yes
webserver-address=[::]
webserver-password=###############SuperSecretWebInterfacePW#####################
webserver-port=80
webserver-allow-from=192.0.2.3,[2001:db8:ffff:ffff::]/64
api=yes
api-key=############SomeRandomStringHere#########

Wie üblich habe ich hier natürlich Dummy-Werte als Beispiel eingesetzt. Die Config ist eigentlich recht straight forward, man gibt des System mit, dass es bitte den mysql-Connector verwerwenden soll, dazu die notwendigen “Eckdaten” wie Server-Adresse (wichtig: IP und keinen Namen, sonst baut man sich beim Nameserver ganz schnell ein Henne-Ei-Problem), Benutzername, Passwort etc.
Der Operation-Mode legt einige Dinge für den Server selbst fest, unter anderem soll er sich als DNS-Master verhalten (er bezieht seine Informationen aus der MySQL-Datenbank und nicht von einem anderen System, es darf auch mehrere Master geben, dazu später mehr. Zudem möchten wir die Funktionalität haben, dass der Server auf DNS-Updates reagieren darf – im Normalfall ist diese Option sicherheitsgerichtet abgeschalten.

Der letzte Abschnitt ist nicht zwingend notwendig, hilft aber ggf. beim Fehlersuchen, da man dann ggf. eine graphische Möglichkeit zur Kontrolle hat. Sinnvoller Weise ist die Nutzung des Webservers aber auch auf Quell-Adresssen eingeschränkt, damit man dieses Tool nicht mehr als notwendig exponiert. Das kann man natürlich an die eigenen Gegebenheiten anpassen.
Vor dem Starten sollte man noch einen Datenbankbenutzer anlegen und die notwendigen Tabellen erstellen. Ich habe dem DNS ein eigenes Datenbankschema spendiert, das ist hilfreich aber nicht zwingend notwendig. Die notwendigen Tabellen samt passender SQL-Statements finden sich in der Dokumentation zu PowerDNS.

Weiter geht es mit Bauen und Laufen lassen (der eigene user sollte in der docker-Gruppe sein, sonst muss man alles als root machen).

[user@system ~]$ docker build docker build -t powerdns-authorative .
[user@system ~]$ docker run --ip6 2001:db8:ffff:ffff:ffff:ffff:0053:3 --network dns --name power_dns_authorative_primary -d powerdns-authorative

Der letzte Befehl startet das Dockerimage als Container. Etwas ungewohnt sind hier ggf. die explizizten Parameter für das Netzwerk und die IPv6-Adresse – mit dem Befehl “–network #name#” gibt man das vorher konfigurierte Netzwerk an, das ist ganz praktisch zur Isolation einzelner Services und man kann ggf. auch recht leicht Firewall-Regeln zum Abschirmen der Container schreiben, zudem hat Docker einige interne Namensauflösungen (sozusagen eine Spezial-Mini-DNS) was die Container zur Kommunikation untereinander verwenden können. Dem Container einen Namen zu geben schadet auch nicht, so kann man ihn später leichter wieder finden. Besonders wichtig ist in diesem Zusammenhang die fest vorgegebene IPv6-Adresse – für erste Tests kann man es noch bei den Automatismen von Docker belassen, aber wenn man später im Loadbalancer (dnsdist, siehe oben bzw. nachfolgende Beiträge) eintragen muss dann braucht man eine feste Referenz. Namen taugen hier eher nicht, denn wenn der Namerserver noch nicht läuft kann er auch keine Namen auflösen.

In der Datenbank trägt man nun am Besten einmal eine (Sub-)Domain zum Testen ein (klassisch wäre hier my.example.com)

INSERT INTO `domains` VALUES (1, 'my.example.com', NULL, NULL, 'NATIVE', NULL, NULL);
INSERT INTO `records` VALUES (1, 1, 'test.my.example.com', 'A', '192.168.1.1', 120, NULL, NULL, 0, NULL, 1);
INSERT INTO `records` VALUES (2, 1, my.example.com', 'SOA', 'localhost admin.my.example.com 1 10380 3600 604800 3600', 86400, NULL, NULL, 0, NULL, 1);
INSERT INTO `records` VALUES (3, 1, 'test2.my.example.com', 'A', '192.168.1.2', 3600, 0, NULL, 0, NULL, 1);
INSERT INTO `records` VALUES (4, 1, 'test2.my.example.com', 'AAAA', '2001:db8:ffff:ffff:ffff:ffff:0815:7', 3600, 0, NULL, 0, NULL, 1);
INSERT INTO `records` VALUES (5, 1, 'test2.my.example.com', 'AAAA', '2001:db8:ffff:ffff:ffff:ffff:4711:8', 3600, 0, NULL, 0, NULL, 1);

Nun kann man den authorative Server auch schon erreichen und befragen


[user@system ~]$ dig @2001:db8:ffff:ffff:ffff:ffff:0053:3 -t A test.my.example.com
<<>> DiG 9.11.3-1ubuntu1.12-Ubuntu <<>> -6 @2001:db8:ffff:ffff:ffff:ffff:0053:3 test.my.example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6707
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;test.my.example.com. IN AA

;; ANSWER SECTION:
test.my.example.com. 120 IN A 192.168.1.1

;; Query time: 3 msec
;; SERVER: 2001:db8:ffff:ffff:ffff:ffff:0053:3#53(2001:db8:ffff:ffff:ffff:ffff:0053:3)
;; WHEN: Tue Jan 18 21:50:47 CEST 2020
;; MSG SIZE rcvd: 71

[user@system ~]$ dig @2001:db8:ffff:ffff:ffff:ffff:0053:3 -t AAAA test.my.example.com
<<>> DiG 9.11.3-1ubuntu1.12-Ubuntu <<>> -6 @2001:db8:ffff:ffff:ffff:ffff:0053:3 test.my.example.com
; (1 server found)
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 6707
;; flags: qr aa rd; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 1
;; WARNING: recursion requested but not available

;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1232
;; QUESTION SECTION:
;test.my.example.com. IN AAAA

;; ANSWER SECTION:
test.my.example.com. 120 IN AAAA AAAA 2001:db8:ffff:ffff:ffff:ffff:815:7

;; Query time: 3 msec
;; SERVER: 2001:db8:ffff:ffff:ffff:ffff:0053:3#53(2001:db8:ffff:ffff:ffff:ffff:0053:3)
;; WHEN: Tue Jan 18 21:50:47 CEST 2020
;; MSG SIZE rcvd: 71

Somit haben wir den ersten Schritt getan und einen Server aufgesetzt der uns entsprechende DNS-Anfragen beantwortet – ich empfehle dringend noch etwas mit dem Server zu experimentieren (neue Einträge in die Datenbank schreiben und diese abfragen, auch ruhig einmal andere Record-Typen ausprobieren, da sind der Fantasie eigentlich nur die Grenzen des DNS gesetzt. Man sollte natürlich aufpassen, nicht mit irgendwelchen wichtigen, produktiven Domains zu arbeiten und zu experimentieren bis man sich sicher ist das es funktioniert.

Bis zum produktiven Einsatz sind noch einige weitere Schritte zu beachten. Es schadet auch nichts sich schon einmal einen zweiten authorative Nameserver-Container zu starten, über Replikationsmechanismen braucht man sich bei diesem Beispiel noch keine Gedanken machen, ein zweiter PowerDNS authorative Container greift ebenfalls auf die Datenbank zu und beantwortet Fragen aus dieser. Wer hier Ausfallsicherheit und Redundanz benötigt kann dies z.B. durch replizierende Datenbank-Knoten erreichen (Master-Slave, Master-Master, Cluster etc.). Mit entsprechenden Anpassungen kann man den zweiten Container auch auf einem anderen Server laufen lassen, fürs erste reicht es aber auch mal im gleichen Docker-Netzwerk.

In den kommenden Beiträgen werde ich auf die weiteren Bestandteile von PowerDNS und deren Setup und Zusammenspiel eingehen (ich hoffe ich komme dazu wieder etwas häufiger zu schreiben).