In Teil 1 und Teil 2 habe ich über die Einrichtung und den Betrieb des DNS-Servers PowerDNS mit Docker für das heimische Netzwerk geschrieben. Zum Schluss erwähnte ich kurz, dass im Prinzip dieselbe Konfiguration bereits seit längerer Zeit in der weiten Welt da draußen für meine eigenen Domains genutzt wird. Ein paar Unterschiede gibt es natürlich, weshalb ich nun im dritten Teil genauer auf die Konfiguration der DNS-Server eingehen werde, die ich der Einfachheit halber als „externe“ DNS-Server bezeichne.
Dabei handelt es sich um zwei virtuelle Maschinen (VMs) bei einem Provider, die zumindest unter zwei unterschiedlichen IP-Adressen erreichbar sein müssen. Früher gab es bei der DeNIC mal die Regel, dass die DNS-Server in unterschiedlichen „Class-C-Netzen“ stehen müssen, aber wenn es schon keine Netzklassen mehr gibt, hat sich diese Anforderung auch erledigt. Somit verbleiben als Voraussetzung zwei IP-Adressen, die sich aus Redundanzgründen jedoch auf zwei Servern befinden sollten. Die Server bzw. VMs müssen untereinander erreichbar sein, schließlich soll der Zone-Transfer durchgeführt werden können. Ansonsten werden alle Komponenten genutzt, die in den ersten beiden Teilen beschrieben wurden, d.h. der PowerDNS Authoritative Server, PowerDNS Recursor und ebenso dnsdist.
Realer Betrieb, fiktive IPs
Gegeben seien also zwei VMs mit den – zugegebenermaßen sehr fiktiven – IP-Adressen 111.222.333.444 und 222.333.444.555, wobei die erste für den ersten, d.h. Master-DNS-Server und die zweite demzufolge für den zweiten, ergo Slave-DNS-Server verwendet werden. Man möge mir dies verzeihen, denn natürlich sind die IP-Adressen hinter meinen Nameservern ns1.xyzcdn.xyz und ns2.xyzcdn.xyz kein Geheimnis, aber es soll verhindert werden, dass die hier angegebenen Konfigurationsdateien einfach per Copy&Paste genutzt und dabei evtl. eine der echten IP-Adressen übersehen werden. Nicht zuletzt befinde ich mich damit in guter Hollywood-Tradition, wie beispielsweise im Film „Das Netz„:
PowerDNS Docker-Compose-Files
Das Docker-Compose-File auf dem ersten Nameserver sieht wie folgt aus:
version: '3.7' services: mariadb_powerdns: image: mariadb:latest restart: always volumes: - ./mariadb_powerdns/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: <PASSWORD 1> MYSQL_DATABASE: powerdns MYSQL_USER: powerdnsuser MYSQL_PASSWORD: <PASSWORD 2> networks: dns_net: ipv4_address: 172.30.1.10 mariadb_powerdnsadmin: image: mariadb:latest restart: always volumes: - ./mariadb_powerdnsadmin/data:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: <PASSWORD 3> MYSQL_DATABASE: powerdnsadmin MYSQL_USER: powerdnsuser MYSQL_PASSWORD: <PASSWORD 4> networks: dns_net: ipv4_address: 172.30.1.20 powerdns: image: geschke/powerdns-server restart: always environment: PDNS_BACKEND: mysql PDNS_API_KEY: <API KEY 1> PDNS_LOCAL_PORT: 5300 PDNS_MASTER: "true" PDNS_ALLOW_AXFR_IPS: "222.333.444.555/32,10.20.30.40/32,172.30.1.0/24" MYSQL_NAME: powerdns MYSQL_USER: powerdnsuser MYSQL_PASSWORD: <PASSWORD 2> MYSQL_HOST: mariadb_powerdns networks: dns_net: ipv4_address: 172.30.1.30 powerdns_recursor: image: geschke/powerdns-recursor restart: always environment: PDNS_ALLOW_FROM: "127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 111.222.333.444/32, 222.333.444.555/32, 10.20.30.0/24, 172.30.1.0/24" PDNS_LOCAL_ADDRESS: "172.30.1.40" PDNS_LOCAL_PORT: "5301" PDNS_FORWARD_ZONES_FILEPATH: "/etc/powerdns/forward_zones" PDNS_API_KEY: <API KEY 2> volumes: - type: bind source: ./recursor/forward_zones target: /etc/powerdns/forward_zones networks: dns_net: ipv4_address: 172.30.1.40 dnsdist: image: geschke/dnsdist restart: always ports: - "111.222.333.444:53:53/udp" - "111.222.333.444:53:53/tcp" volumes: - type: bind source: ./dnsdist/dnsdist.conf target: /etc/dnsdist/dnsdist.conf networks: dns_net: ipv4_address: 172.30.1.50 powerdns_admin: image: ngoduykhanh/powerdns-admin:latest restart: always logging: driver: json-file options: max-size: 50m environment: - SQLALCHEMY_DATABASE_URI=mysql://powerdnsuser:<PASSWORD 4>@mariadb_powerdnsadmin/powerdnsadmin - GUNICORN_TIMEOUT=60 - GUNICORN_WORKERS=2 - GUNICORN_LOGLEVEL=DEBUG - MAIL_SERVER=mail.geschke.name - VIRTUAL_HOST=ns1.xyzcdn.xyz - LETSENCRYPT_HOST=ns1.xyzcdn.xyz - LETSENCRYPT_EMAIL=ralf@geschke.cloud networks: website_net: dns_net: ipv4_address: 172.30.1.60 networks: dns_net: ipam: driver: default config: - subnet: 172.30.1.0/24 website_net: external: true
Der grundlegende Aufbau ist identisch zu der Verwendung im internen Netz. Wie zu erkennen ist, werden die Ports 8081 der PowerDNS-API und 8082 der PowerDNS-Recursor-API nicht nach außen hin freigegeben, somit fehlen die entsprechenden Einträge. Damit können diese Ports zwar im Docker-internen Netz 172.30.1.0/24 erreicht werden, für alle anderen Server besteht jedoch kein Zugriff. Insofern ist der Zugriff ausschließlich über dnsdist auf den Port 53 (udp und tcp) möglich.
Falls die Absicht bestehen sollte, doch von anderen Rechnern auf die API zugreifen zu wollen, könnten beispielsweise eine Firewall bzw. ein IP-Filter vorgeschaltet werden, oder zwischen den beteiligten Rechnern wird ein virtuelles, privates Netzwerk (VPN) aufgebaut, so dass die Webserver- bzw. API-Ports nur für die IP-Adresse innerhalb des VPN freigeschaltet werden. Beispielsweise bietet mein Provider ein – mit geringer Bandbreite sogar kostenloses – internes VPN an, das zur Kommunikation zwischen den VMs genutzt werden kann. Dieses Netz ist auf den VMs eingerichtet als 10.20.30.0/24, daher ist diese Angabe etwa auch in den Umgebungsvariablen für den erlaubten Zugriff auf den PowerDNS-Recursor zu finden.
Auch der Abschnitt „networks
“ dürfte aus den bisherigen Artikeln bekannt sein. Hinzu gekommen ist jedoch ein weiteres Netzwerk namens „website_net
„, das als extern definiert wurde. Was es damit auf sich hat, wird bei der Betrachtung der Umgebungsvariablen für PowerDNS-Admin deutlich. Der Zugriff auf das Admin-Interface soll nur verschlüsselt per HTTPS möglich sein, schließlich möchte man keine Passwörter unverschlüsselt übertragen. Da die VM auch als Web-Server genutzt wird, läuft dort bereits Nginx-Proxy mitsamt Nginx-Proxy-Companion für das automatische Ausstellen von Let’s-Encrypt-Zertifikaten im Docker-Swarm-Cluster. Dazu ist ein Netzwerk namens „website_net
“ definiert – zur Konfiguration im Detail sei verwiesen an den Artikel „Howto: Nginx-Proxy und Nginx-Proxy-Companion im Docker Swarm Mode auf einem Host„. Durch die neben dem im Docker-Compose-File definierten Netzwerk „dns_net
“ erhält der powerdns_admin-Service zusätzlich das Netzwerk „website_net
“ und hat somit Zugriff darauf. Damit können der Nginx-Proxy-Container (per VIRTUAL_HOST) und der Nginx-Proxy-Companion-Container (per LETSENCRYPT_HOST und LETSENCRYPT_EMAIL) aktiv werden und anhand der Umgebungsvariablen den Reverse-Proxy und das Let’s-Encrypt-Zertifikat bereitstellen. Für alle anderen Services ist der Zugriff auf „website_net
“ nicht notwendig, da die Kommunikation ausschließlich über „dns_net
“ mit fest definierten internen IP-Adressen stattfindet.
Soviel zu den Änderungen beim Docker-Compose-File. Die Datei ./recursor/forward_zones
enthält keine Überraschungen, denn hier findet sich die nun etwas längere Liste der Domains wieder, deren Requests an den Auth-Server weitergeleitet werden sollen.
In der Docker-Compose-Datei für den Slave-DNS sind die meisten Einstellungen identisch zum Master-DNS, daher sollen im Folgenden nur die Unterschiede gezeigt werden:
version: '3.7' services: [...] powerdns: [...] environment: [...] PDNS_SLAVE: "true" PDNS_ALLOW_NOTIFY_FROM: "127.0.0.0/8, 10.0.0.0/8, 192.168.0.0/16, 172.16.0.0/12, 222.333.444.555/32, 111.222.333.444/32, 10.20.30.0/24, 172.30.1.0/24" PDNS_TRUSTED_NOTIFICATION_PROXY: "172.30.1.50" [...] dnsdist: [...] ports: - "222.333.444.555:53:53/udp" - "222.333.444.555:53:53/tcp" [...] powerdns_admin: [...] environment: [...] - VIRTUAL_HOST=ns2.xyzcdn.xyz - LETSENCRYPT_HOST=ns2.xyzcdn.xyz - LETSENCRYPT_EMAIL=ralf@geschke.cloud [...]
Der PowerDNS-Auth-Server muss in den Slave-Betrieb versetzt werden, darüber hinaus werden einige IP-Adressen bzw. Netze für die Notification, d.h. der Initiierung des Zone-Transfers freigeschaltet. Die IP-Adresse für dnsdist ändert sich ebenfalls, darüber hinaus müssen Nginx-Proxy und dessen Companion für Let’s Encrypt den richtigen Hostnamen übermittelt bekommen.
dnsdist und ein paar Hürden
Bleibt nur noch die Konfiguration von dnsdist, die sich auf den Master- und Slave-DNS-Servern ein wenig mehr unterscheidet. Dies hängt jedoch vor allem damit zusammen, dass die VMs, auf denen die DNS-Server betrieben werden, genau diese DNS-Server als System-DNS nutzen sollen. Üblicherweise würden für die Server-Konfiguration die vom Provider bereitgestellten DNS-Server genutzt – oder auf einen frei verwendbaren DNS-Server wie etwa Google Public DNS oder Cloudflare Public DNS Resolver zurückgegriffen. Das funktioniert auch problemlos, die eigenen durch PowerDNS verwalteten DNS-Einträge lassen sich mit den Public-DNS-Servern auflösen:
$ dig @1.1.1.1 kuerbis.org ; <<>> DiG 9.11.5-P4-5.1ubuntu2.1-Ubuntu <<>> @1.1.1.1 kuerbis.org [...] ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 1452 ;; QUESTION SECTION: ;kuerbis.org. IN A ;; ANSWER SECTION: kuerbis.org. 3460 IN A 194.55.15.79 ;; Query time: 76 msec [...] $ dig @8.8.8.8 kuerbis.org ; <<>> DiG 9.11.5-P4-5.1ubuntu2.1-Ubuntu <<>> @8.8.8.8 kuerbis.org [...] ;; OPT PSEUDOSECTION: ; EDNS: version: 0, flags:; udp: 512 ;; QUESTION SECTION: ;kuerbis.org. IN A ;; ANSWER SECTION: kuerbis.org. 3203 IN A 194.55.15.79
Nun sollen jedoch die VMs die selbst gehosteten Nameserver nutzen, nach dem Motto „Eat your own dog food„… An sich wäre dies kein Problem, nur zeigt sich eine kleine Hürde durch den notwendigen Zone-Transfer auf den zweiten DNS-Server, dazu gleich mehr.
Zunächst die Konfiguration von dnsdist in ./dnsdist/dnsdist.conf
auf dem Master:
addLocal('0.0.0.0:53') setACL({'0.0.0.0/0', '::/0'}) newServer({address='172.30.1.30:5300', pool='auth'}) newServer({address='172.30.1.40:5301', pool='recursor'}) recursive_ips = newNMG() recursive_ips:addMask('111.222.333.444/32') recursive_ips:addMask('172.16.0.0/12') recursive_ips:addMask('10.11.12.0/24') recursive_ips:addMask('10.20.30.0/24') addAction(NetmaskGroupRule(recursive_ips), PoolAction('recursor')) addAction(AndRule({OrRule({QTypeRule(dnsdist.AXFR), QTypeRule(dnsdist.IXFR)}), NotRule(makeRule('222.333.444.55/32'))}), RCodeAction(dnsdist.REFUSED)) addAction(AllRule(), PoolAction('auth'))
Die ./dnsdist/dnsdist.conf
auf dem Slave zur direkten Gegenüberstellung:
addLocal('0.0.0.0:53') setACL({'0.0.0.0/0', '::/0'}) newServer({address='172.30.1.30:5300', pool='auth'}) newServer({address='172.30.1.40:5301', pool='recursor'}) recursive_ips = newNMG() recursive_ips:addMask('222.333.444.55/32') recursive_ips:addMask('111.222.333.444/32') recursive_ips:addMask('172.16.0.0/12') recursive_ips:addMask('10.11.12.0/24') recursive_ips:addMask('10.20.30.0/24') addAction(NetmaskGroupRule(recursive_ips), PoolAction('recursor')) addAction(AllRule(), PoolAction('auth'))
Der grundsätzliche Aufbau dürfte aus der bisherigen Beschreibung bereits bekannt sein. Es werden zwei Server-„Pools“ definiert, wobei der Pool namens „auth
“ an den PowerDNS-Auth-Server-Container weiterleitet, und der Pool namens „recursor
“ an den entsprechenden Recursor-Container. Nun soll für alle beteiligten VMs, Container etc., die rekursive Namensauflösung erlaubt sein, während alle Requests, die von außen kommen, an den Auth-Server gehen und somit nur diejenigen Domains auflösen, für die die Auth-Server auch zuständig sind. D.h. eine aus dem Internet kommende DNS-Anfrage einer nicht von den DNS-Servern verwalteten Domain soll auch nicht beantwortet werden – schließlich handelt es sich nicht um explizit dafür vorgesehene, öffentliche DNS-Server.
Das alles wäre ohne besonderen Aufwand lösbar – die internen IP-Adressen bzw. -Netze werden dem Recursor-Pool zugeordnet, so dass der Recursor die Namensauflösung übernimmt. Alle anderen Adressen bzw. Netze gelangen direkt an den Auth-Server-Pool, der nur Anfragen an die verwalteten Domains beantwortet und alle anderen Requests unbeantwortet lässt. Ein kleiner Haken bleibt jedoch: Der Zonentransfer (AXFR- bzw. IXFR-Request) kommt ebenfalls von einer der internen Adressen, namentlich vom zweiten DNS-Server. Gelangen jene Anfragen an den Recursor, schlagen sie jedoch fehl, trotz Eintrag der entsprechenden Zonen in ./recursor/forward_zones
. Warum das so ist, und ob dieses Verhalten von PowerDNS-Recursor so erwünscht bzw. ob es sich ändern lässt, ist mir aktuell nicht bekannt.
Hingegen würden AXFR-Requests, die von außen bzw. von anderen IPs als denjenigen im Recursor-Pool gestellt werden, direkt an den Auth-Server weiter geleitet, der diese auch beantwortet, denn der PowerDNS-Auth-Server-Container muss so konfiguriert sein, dass AXFR Anfragen vom dnsdist-Container erlaubt. Denn innerhalb des Auth-Server-Containers ist keine Unterscheidung möglich, ob ein Request tatsächlich von außen kommt, oder von einem Container innerhalb des Docker-Netzwerks, da letztlich alle Anfragen aus dem Docker-Netzwerk zu kommen scheinen, somit ist keine Zugriffsbeschränkung möglich. Diese übernimmt die Regel von dnsdist, die besagt, dass AXFR-Requests nur von der IP-Adresse des Slave-DNS-Servers erlaubt sind, alle anderen werden abgewiesen.
Auf dem Slave-DNS ist ein derartiger Filter nicht notwendig, da AXFR nicht notwendig und somit von vornherein nicht erlaubt ist.
Doch mit dieser Einstellung gibt es ein weiteres Problem. Wir zuvor erwähnt, sollen die beiden PowerDNS-Server als Nameserver für die eigenen VMs bzw. die darauf befindlichen Container genutzt werden, d.h. als System-DNS fungieren und die Namensauflösung vornehmen. Der Grund dafür liegt in dem Betrieb von Postfix im VPN mit WireGuard – und zwar sollen auch interne IP-Adressen korrekt aufgelöst werden. Das würde zwar funktionieren, aber jegliche weitere (rekursive) DNS-Abfrage wird vom ersten DNS-Server eben nicht beantwortet, weil die IP-Adresse des zweiten nicht im dnsdist-Recursor-Pool des ersten enthalten ist, weil dann wiederum der Zonentransfer nicht funktionieren würde…
Das Problem mag vielfach zu lösen sein, ein zugegebenermaßen sehr pragmatischer Ansatz ist es, der VM, auf der der Slave-DNS läuft, in den Systemeinstellungen anstatt zwei Nameservern nur einen, und zwar die eigene IP-Adresse zu übergeben. Auf der ersten VM sind hingegen beide Nameserver eingetragen, da auf dem zweiten keine Filterregel enthalten ist, die die Abfrage von der IP der ersten VM verhindern würde.
Die zugrunde liegende Linux-Distribution Ubuntu nutzt Netplan zur Konfiguration des Netzwerks, die Datei /etc/netplan/01-netcfg.yaml
sieht für die erste VM daher wie folgt aus:
# This file describes the network interfaces available on your system # For more information, see netplan(5). network: version: 2 renderer: networkd ethernets: ens3: dhcp4: yes dhcp4-overrides: use-dns: false nameservers: search: [xyzcdn.xyz] addresses: [111.222.333.444,222.333.444.555] [...]
Das Pendant für die zweite VM:
# This file describes the network interfaces available on your system # For more information, see netplan(5). network: version: 2 renderer: networkd ethernets: ens3: dhcp4: yes dhcp4-overrides: use-dns: false nameservers: search: [xyzcdn.xyz] addresses: [222.333.444.555] [...]
Natürlich leidet an dieser Stelle die Redundanz, soll heißen, eine solche ist nicht mehr vorhanden. In diesem Fall habe ich dieses Manko einfach mal erlaubt, da es sich auch um eine vielleicht nicht ganz alltägliche Konfiguration handelt. Vielleicht gibt es auch einen besseren Ansatz – in diesem Fall wäre ich über Hinweise und Kommentare wie immer dankbar!
Teil 3 von ..?
Soviel zum Betrieb von PowerDNS-Auth-Server, PowerDNS-Recursor und dnsdist mit Docker und all den hier noch vergessenen Buzzwords. Die dargestellte Konfiguration läuft seit einigen Wochen im produktiven Betrieb stabil, die nächsten Schritte sind einerseits eine Aktualisierung der zugrunde liegenden Docker-Images, und andererseits der Versuch, dynamisches DNS zu implementieren, voraussichtlich auf Basis des bereits bestehenden WireGuard-VPN. Es bleibt somit spannend…