Hält die A-Records der dynamischen Zone (dyn.example) auf der aktuellen öffentlichen IPv4 des Heimanschlusses. Läuft als Container auf der NAS, aktualisiert per RFC 2136 (nsupdate) mit TSIG direkt gegen die BIND-Instanz auf dem Hetzner-VPS. Der Quellcode liegt in Forgejo, Build und Deploy laufen über Komodo (Push → Image in der Forgejo-Registry → Deployment).

Die Seite hält fest, wie er gebaut ist, wie man ihn betreibt und was beim Ausfall im Herbst 2026 schiefging.

Ablauf

 alle 5 min
    │
    ▼
 öffentliche IPv4 holen ──(Dienst 1 → 2 → 3, je validiert)──► keine gültige Antwort? → Fehler, Healthcheck altert
    │
    ▼
 pro Record: aktuelle A-Records beim Nameserver abfragen (dig @ns.example)
    │
    ├─ genau ein Record, gleich der WAN-IP ──► nichts tun
    │
    └─ abweichend / fehlt / doppelt ──► nsupdate (delete + add) ──► erneut abfragen und gegenprüfen

Wichtig: Der Ist-Zustand kommt vom Nameserver, nicht aus einer Statusdatei im Container. Ein neu erzeugter Container ist damit unkritisch, und ein zwischenzeitlich ausgefallener BIND wird beim nächsten Lauf automatisch nachgezogen.

Betrieb

  • Komodo-Deployment (Einzelcontainer), Restart-Policy unless-stopped, Healthcheck im Image (unhealthy, wenn der letzte erfolgreiche Lauf älter als drei Intervalle ist).
  • Konfiguration nur über das Environment-Feld des Deployments (KEY=VALUE pro Zeile). Das Script liest ausschließlich die nummerierten Variablen DDNS_<n>_HOST und DDNS_<n>_SECRET; wie die Secrets in Komodo heißen, ist egal. Vollständige Variablenliste: README im Repo.
  • Secrets nur als Komodo-Variablen, nie im Repo.
  • Änderung am Environment-Feld → Deploy, nicht nur Restart.
  • Logs sind absichtlich still: nur Änderungen und Fehler (CHANGE, UPDATED, ERROR). Mit VERBOSE=1 zusätzlich jeder unveränderte Lauf, mit DRY_RUN=1 wird nichts gesendet.

TSIG-Keys

  • Pro Record ein eigener Key, Key-Name = Record-Name. Die Keys gehören ausschließlich dem Updater.
  • Die Zertifikats-Clients (Let’s Encrypt per DNS-Challenge: Home Assistant, acme.sh, NPM) nutzen einen eigenen, separaten Key. So betrifft eine Rotation immer nur einen Zweck.
  • Algorithmus hmac-sha512. Die alten Keys waren hmac-md5 (der Standard von nsupdate, wenn man nichts angibt) – veraltet.
  • Erzeugen auf dem DNS-Host: tsig-keygen -a hmac-sha512 <keyname>.
  • In BIND immer den kompletten Key-Block ersetzen, nicht nur die secret-Zeile. Bleibt der alte Algorithmus stehen, lehnt der Server mit BADKEY ab.
  • Danach named-checkconf und den Container neu starten. Ein fehlgeschlagenes rndc reload lässt BIND stillschweigend mit der alten Konfiguration weiterlaufen.

Update-Pfad testen

Im Normalbetrieb läuft fast immer nur der Zweig „Record stimmt schon“. Der eigentliche Update-Pfad lässt sich gezielt beweisen: einen Record per nsupdate von Hand auf eine Dokumentations-IP (192.0.2.1) setzen und den Updater ihn zurückholen lassen. Erwartet: CHANGE … 192.0.2.1 -> <WAN-IP> und UPDATED …. Der Record steht dabei kurz falsch (TTL 600) – für einen ruhigen Moment einplanen. Den Test für jeden Key machen: Ein Tippfehler im Key-Namen fällt sonst erst beim nächsten echten IP-Wechsel auf.

🔧 Lessons Learned: Ausfall der dynamischen Zone

Datum: September/Oktober 2026

Symptom: AdGuard-UI und NPM-UI über ihre *.dyn-Namen nicht erreichbar, Komodo zeigt die NAS als Unknown, und <nas>.fritz.box löst nicht mehr auf. Die NAS selbst war per IP und DSM erreichbar, alle Container liefen.

Debugging-Weg (in dieser Reihenfolge):

  1. Ping auf die IP, DSM-UI, Container Manager → NAS gesund. Das Problem liegt bei Namen, nicht beim Host.
  2. dig gegen den Heim-Resolver: andere fritz.box-Namen gehen, der dyn-Name liefert SERVFAIL, normale Domains gehen.
  3. dig SOA <zone> @1.1.1.1: ebenfalls SERVFAIL, mit den erweiterten Fehlercodes 22/23 („No Reachable Authority“, „at delegation“). Damit ist klar: die ganze Zone ist weltweit tot, nicht nur beim Heim-Resolver. Das Lokale war nur ein Symptom (ein dyn-Name der NAS löste kurz weiter, weil AdGuard ihn noch im Cache hatte).
  4. Auf dem VPS fehlte der BIND-Container in docker ps. In Komodo stand er auf Exited, ebenso zwei weitere Deployments.

Ursache: Die Deployments hatten keine Restart-Policy. Nach einem Neustart des Hosts kam der BIND-Container nicht wieder hoch, und alles unterhalb der dynamischen Zone (Web-UIs über deren Namen, Komodo → NAS, DNS-Challenges) ging mit ihm aus.

Nebenbefund 1 – .fritz.box-Name tot: Die FritzBox führte vier Einträge mit demselben Hostnamen (drei inaktive Karteileichen mit derselben IP und anderen MACs aus früheren Macvlan-Versuchen). Doppelte Hostnamen bringen den FritzBox-eigenen DNS durcheinander. Nach dem Löschen der inaktiven Einträge löste der Name wieder auf.

Nebenbefund 2 – der alte Updater war schon länger kaputt: Er war ein Perl-Script (mit Cron im Container), das die IP per HTML-Scraping bei einem privaten Hobbydienst ermittelte, und ein zweites Script, das eine Datei las, die nur das erste hinterließ. Beide Dienste setzten die Verbindung zurück. Außerdem standen die TSIG-Secrets im Klartext im Repo (und damit in der Git-Historie). Aufgefallen war es nur nicht, weil die WAN-IP wochenlang stabil blieb.

Fix:

  • Restart-Policy unless-stopped an allen betroffenen Deployments.
  • Updater komplett neu gebaut: ein kleines POSIX-Shell-Script (curl, dig, nsupdate), mehrere IP-Dienste mit Fallback und Validierung, Ist-Vergleich beim Nameserver, Gegenprüfung nach dem Update, Eingaben gegen Einschleusen von Zeilen abgesichert, Healthcheck. Getestet mit Mocks für alle Zweige.
  • Beide Updater-Keys rotiert, auf hmac-sha512. Die alten Secrets sind damit wertlos, die Git-Historie musste nicht umgeschrieben werden.

Merke:

  • Nicht der Host, sondern ein Name. Ist die IP erreichbar, aber Namen sterben, zuerst dig gegen den eigenen Resolver und gegen @1.1.1.1 vergleichen. Das trennt „lokaler Cache“ von „Zone tot“ in einer Minute.
  • Cache-Schichten: AdGuard hält Fehlerantworten und alte Treffer. Nach einer Reparatur den AdGuard-Cache leeren, sonst bleibt man auf der Negativantwort sitzen.
  • Komodo-Deployments haben keine Restart-Policy per Default. Bei allem, was die Infrastruktur trägt (DNS!), von Anfang an unless-stopped setzen.
  • Ein Deployment kennt nur sein Environment-Feld. Eine Variable, die dort nicht steht, existiert im Container nicht. Prüfen mit env | grep DDNS im Container.
  • Hostnamen in der FritzBox sind eindeutig zu halten. Alte Geräte mit gleichem Namen (Macvlan-Experimente) aus der Geräteliste entfernen.
  • Wenn eine Dyn-DNS-Kette „funktioniert“, weil sich nichts ändert, ist sie nicht getestet. Den Änderungspfad einmal absichtlich provozieren.

Fehlersuche

Meldung / SymptomUrsache
no records configuredDDNS_1_HOST kommt nicht im Container an (Environment-Feld, env | grep DDNS prüfen)
DDNS_<n>_SECRET is missing or not base64Variable fehlt, ist leer oder falsch benannt
could not determine public IPv4alle IP-Dienste nicht erreichbar oder liefern Unsinn
query to ns… failedNameserver nicht erreichbar: läuft der BIND-Container, Port 53 (UDP und TCP) offen?
NOTAUTH(BADKEY)BIND kennt Key-Name oder Algorithmus nicht: Tippfehler im Namen, alter hmac-md5-Block, Konfiguration nicht neu geladen
tsig verify failure / BADSIGName und Algorithmus stimmen, das Secret nicht
update accepted but … still serves …Update nicht übernommen: Zonenjournal, update-policy prüfen

Offen / Ideen

  • IPv6 (AAAA): bewusst nicht enthalten, solange IPv6 im Heimnetz abgeschaltet ist. Für einen WireGuard-Endpunkt über IPv6 zählt die Adresse der FritzBox selbst, nicht die der NAS – die Adressquelle wäre dann die FritzBox. Erst angehen, wenn Dual-Stack wieder aktiv ist.
  • Engere update-policy: der gemeinsam genutzte ACME-Key sollte nur TXT-Records unter _acme-challenge ändern dürfen, die Updater-Keys nur ihren eigenen A-Record. Noch zu prüfen, was aktuell erlaubt ist.
  • Externer Check auf die SOA der dynamischen Zone (nicht über AdGuard), damit ein Ausfall wie dieser gemeldet wird, bevor man ihn beim Arbeiten bemerkt.

Siehe auch

  • Infrastruktur – Netzwerk, DNS und CI/CD insgesamt
  • Komodo – Deployments, Builds und ihre Eigenheiten
  • AdGuard-Home – der Resolver, der Fehlerantworten cached
  • Docker-Macvlan – Ursprung der alten Geräteeinträge in der FritzBox