Einführung
Deine IPv4/IPv6 soll folgen wie ein treuer Hund, ohne dass du DNS von Hand pflegen musst? Dieses kleine PHP-Skript spielt DynDNS-Server, spricht die Hetzner Console API und versteht sich mit Routern wie einer Fritz!Box. Zuerst kommt die Schritt-für-Schritt-Anleitung für Einsteiger, danach die Nerd-Ecke mit den Details.
Update August 2026: Hetzner hat die alte DNS Console (
dns.hetzner.com) samt Legacy-API im Mai 2026 abgeschaltet; verbliebene Zonen wurden automatisch in die Hetzner Console migriert. Dieses Tutorial und das Skript wurden entsprechend aktualisiert und nutzen ausschließlich die Hetzner Console API. Außerdem neu: Unterstützung für IPv6-only-Anschlüsse (DS-Lite), dediziertemyip6-/myipv6-Parameter und protokollkonformenochg-Antworten. Wer noch eine ältere Version des Skripts einsetzt, findet Migrationshinweise in der Nerd-Ecke.
Update Oktober 2026 (Sicherheit): Die
.htaccessfrüherer Versionen hat die SQLite-Datenbank nicht wirklich gesperrt — ihr Muster passte nur auf eine Datei, die exakt.sqlite3heißt —, sodasshetzner_dyndns.sqlite3(Hostnamen, IP-Adressen und Zeitstempel; keine Zugangsdaten) herunterladbar war. Außerdem ließ sich die Sperre für direkte PHP-Aufrufe mit einem Query-String umgehen, der/nic/updateenthält. Die neue.htaccessverbietet alles außer den beiden DynDNS-Endpunkten und/.well-known/, lehnt alle Request-Methoden außer GET/HEAD ab, und eine neue.user.inihärtet PHP. Bei bestehenden Installationen.htaccessersetzen,.user.inihinzufügen und eine PHP-Version mit den PHP2Shell-Fixes einsetzen (siehe Schritt 3).
Voraussetzungen
- Ein Hetzner-Account mit mindestens einer DNS-Zone in der Hetzner Console.
- PHP mit den Erweiterungen
curlundSQLite3(Webspace oder kleine VM reicht). - Einen Ort für das PHP-Skript und einen Cronjob alle paar Minuten.
- Einen Client, der eine DynDNS-URL aufrufen kann (Router, NAS oder ein einfacher curl-Aufruf).
- Die A-/AAAA-Records, die aktualisiert werden sollen, müssen in der Zone bereits existieren — das Skript ändert nur Werte und legt bewusst keine neuen Records an.
Schritt 0 - Beispiel-Setup auf Debian/Ubuntu
Falls du von einem blanken Debian/Ubuntu startest, bringen dich diese Befehle zu einem testbaren Endpunkt:
- Voraussetzungen installieren und Ordner für Skripte erstellen
sudo apt update sudo apt install -y apache2 libapache2-mod-php php-cli php-curl php-sqlite3 sudo a2enmod rewrite sudo mkdir -p /var/www/hetzner-ddns sudo chown -R www-data:www-data /var/www/hetzner-ddns
-
Apache-Seite erstellen
Passe
ServerNameauf deinen DynDNS-Endpunkt an. In Produktion solltest du HTTPS (z. B. Let's Encrypt) nutzen.cat <<'EOF' | sudo tee /etc/apache2/sites-available/hetzner-ddns.conf <VirtualHost *:80> ServerName ddns.example.com DocumentRoot /var/www/hetzner-ddns <Directory /var/www/hetzner-ddns> AllowOverride All Require all granted </Directory> </VirtualHost> EOF
- Seite aktivieren
sudo a2ensite hetzner-ddns sudo apachectl configtest sudo systemctl reload apache2
Schritt 1 - Dateien besorgen
Nutze die zu diesem Tutorial gebündelten Dateien in tutorials/hetzner-ddns-bridge/scripts:
Gespiegelt von https://github.com/woehrl/hetzner-dyndns, Commit
fbb3728. Kopiere sie auf deinen Webspace oder eine kleine VM.
Für das Beispiel-Setup aus "Schritt 0" müssen die Dateien in /var/www/hetzner-ddns gespeichert werden.
Du brauchst mindestens:
/var/www/hetzner-ddns
├─ hetzner_dyndns.php
├─ hetzner_dyndns.config.php.dist
├─ .htaccess Die Datei von GitHub
├─ .user.ini Die Datei von GitHub
└─ hetzner_dyndns_listhosts.php OptionalBeispiel-Befehle:
export path="https://raw.githubusercontent.com/hetzneronline/community-content/refs/heads/master/tutorials/hetzner-ddns-bridge/scripts"
cd /var/www/hetzner-ddns
# Führe folgenden Befehl im Terminal aus,
# um die Dateien zu bestimmen
files=(
hetzner_dyndns.php
hetzner_dyndns.config.php.dist
.htaccess
.user.ini
hetzner_dyndns_listhosts.php
)
# Führe folgenden Befehl im Terminal aus,
# um die zuvor bestimmten Dateien zu kopieren
for f in "${files[@]}"; do
curl "$path/$f" | sudo tee "$f" >/dev/null
doneSchritt 2 - Config anlegen
sudo cp hetzner_dyndns.config.php.dist hetzner_dyndns.config.phpBearbeite hetzner_dyndns.config.php:
| Beschreibung | |
|---|---|
| auth_user | Optional: Username für HTTP Basic Auth. Wenn leer, wird update verwendet. |
| auth_password | Bestimme ein starkes, geteiltes Passwort. Dein Router nutzt es. |
| console_token | Erstelle in der Hetzner Console ein Projekt-API-Token mit Lese-/Schreibrechten für DNS und trage es hier ein. Pflichtfeld pro Realm. |
| zone_name | Wenn die IP-Adresse für eine Subdomain (z.B. sub.example.com) aktualisiert werden soll, muss hier die Hauptdomain (z.B. example.com) angegeben werden. |
| auth_realm | Optional: Wähle ein Label (z. B. "dynbridge"). |
| history_db | Optional: Zeige auf einen beschreibbaren Pfad (z. B. DIR . '/hetzner_dyndns.sqlite3'). |
| TTL | Optional: Passe die TTL pro Realm an, wenn es schneller oder langsamer propagieren soll. |
Hinweis für Bestandsinstallationen: Die früheren Einstellungen
dns_token,dns_endpointundapi_ordergehörten zur abgeschalteten Legacy-API und werden ignoriert. Es genügt, pro Realm einconsole_tokenzu ergänzen — der Rest der Config kann bleiben.
Schritt 3 - Sicher ins Netz stellen
- Halte
hetzner_dyndns.config.phpaus öffentlichen Repos und Verzeichnislisten heraus. - Nutze die mitgelieferte
.htaccessunverändert. Sie schreibt/nic/updateund/v3/updateauf das Skript um und verbietet alles andere: PHP-Dateien, SQLite-Datenbank, Debug-Log, Config-Vorlage, Dotfiles und Backups antworten mit 403, und jede Request-Methode außer GET/HEAD wird abgelehnt, bevor PHP sie sieht. Nur/.well-known/bleibt für ACME-Challenges erreichbar. Der Webserver muss.htaccessauswerten (AllowOverride Allplusmod_rewrite, siehe Schritt 0); unter nginx musst du diese Pfade selbst sperren. - Lass die mitgelieferte
.user.inineben dem Skript liegen. PHP-FPM- und CGI-Setups (typisches Webhosting) lesen sie: Sie schaltetdisplay_errorsab, deaktiviert den Session-Upload-Progress und begrenzt POST-Größen. Mitlibapache2-mod-phpwie in Schritt 0 wird sie ignoriert — setze dieselben Werte dann in/etc/php/*/apache2/php.ini. - Setze eine PHP-Version ein, die die PHP2Shell-Fixes vom Oktober 2026 enthält (CVE-2026-109618, CVE-2026-109619): 8.2.35, 8.3.36, 8.4.27, 8.5.12 oder neuer. Beim Shared Hosting wählst du die PHP-Version im Kundenmenü des Hosters.
.htaccessund.user.iniverkleinern die Angriffsfläche, ersetzen den Patch aber nicht. - Falls nötig, sorge dafür, dass der PHP-User in die SQLite-Datei und das Debug-Log schreiben darf.
- Prüfe die Sperre einmal von außen (erwartete Statuscodes in den Kommentaren):
curl -sI https://ddns.example.com/hetzner_dyndns.sqlite3 | head -1 # 403 curl -sI "https://ddns.example.com/hetzner_dyndns_listhosts.php?/nic/update" | head -1 # 403 curl -sI https://ddns.example.com/nic/update | head -1 # 401
Schritt 4 - Update testen
- Baue die Test-URL (ersetze Domain und Host):
https://dein-ddns-host.example.com/nic/update?hostname=myhost.example.com&myip=203.0.113.10 - Nutze HTTP Basic Auth mit
auth_user/auth_password(User defaultet aufupdate, wenn leer). DynDNS-Clients senden meist irgendeinen Usernamen plus das Passwort; percurlgeht es so:curl -u user:deinPasswort \ "https://dein-ddns-host.example.com/nic/update?hostname=myhost.example.com&myip=$(curl -4 https://ip.hetzner.com)" - Erfolgreiche Antworten sind
good <ip>(aktualisiert) odernochg <ip>(nichts zu tun). Alles andere: ins Debug-Log schauen. - Kommt „No matching rrset found", existiert der A-/AAAA-Record noch nicht: einmalig in der Hetzner Console anlegen, dann klappt das Update.
Schritt 5 - Automatisieren
- Cron alle 5 Minuten (nach Bedarf anpassen):
*/5 * * * * php /path/to/hetzner_dyndns.php --cron --realm=default - Richte deinen Router oder dein NAS auf dieselbe
nic/update-URL mit dem gesetzten Passwort ein. - IPv4 und IPv6 zusammen: entweder kommagetrennt über
myip=<IPv4>,<IPv6>oder über einen dedizierten Parametermyip6=<IPv6>bzw.myipv6=<IPv6>(Fritz!Box:myip=<ipaddr>&myip6=<ip6addr>in der Update-URL). - IPv6-only/DS-Lite: einfach nur die IPv6 senden (
myip6=...ohnemyip) — das Skript aktualisiert dann ausschließlich den AAAA-Record und schreibt bewusst keine Carrier-IPv4 in den A-Record. - Legacy-Clients dürfen weiterhin
X-Authentication: <passwort>oder?p=<passwort>(nur Passwort) senden, empfohlen ist Basic Auth.
Schritt 6 - Kurzer Troubleshooting-Spickzettel
- 401 oder Auth-Prompt: Passwort stimmt nicht.
- 404 auf
/nic/update:.htaccessgreift nicht (AllowOverride Allundmod_rewritefehlen). Zone not found on console API: falscherzone_name, Zone liegt in einem anderen Console-Projekt oder das Token gehört zum falschen Projekt.No matching rrset found: den A-/AAAA-Record einmalig in der Zone anlegen — das Skript legt bewusst keine Records an.- SQLite-Schreibfehler: Dateirechte anpassen oder DB in einen beschreibbaren Pfad verschieben.
- Nichts ändert sich:
console_tokenpro Realm prüfen (DNS-Schreibrechte!) und mitdebugdas Log ansehen.
Nerd-Ecke (so funktioniert es wirklich)
- Architektur im Überblick
- Ein PHP-Skript, eine Config, eine SQLite-DB. DynDNS-Aufrufe (
/nic/updateoder/v3/update) werden via.htaccessaufhetzner_dyndns.phpumgeschrieben. - Das Skript authentifiziert mit Benutzername/Passwort, parst Hostname und optionales
realm, cached den letzten IP-Stand und die Zonen-ID in SQLite und antwortet sofort mitnochg, wenn sich nichts geändert hat — ganz ohne API-Call.
- Ein PHP-Skript, eine Config, eine SQLite-DB. DynDNS-Aufrufe (
- Konfig-Details
- Gemeinsame Einstellungen:
auth_user,auth_password,auth_realm,history_dbsowie optionaldebug/debug_log. - Benachrichtigungen:
notifications.enabledaktivieren,phpodersmtpwählen, Empfänger setzen und entscheiden, ob bei Erfolg, Fehler oder beidem gemailt wird. - Realms: pro Realm
ttl,zone_nameundconsole_token. Setzezone_name, wenn der DynDNS-Endpunkt auf einer Subdomain liegt, du aber die Hauptzone aktualisierst.
- Gemeinsame Einstellungen:
- API-Ablauf (Hetzner Console)
- Zone über
/zones?name=<zone>mit dem Hetzner Console API-Token finden — die Zonen-ID wird danach in SQLite gecacht, gealterte IDs heilen sich per erneutem Lookup selbst. - RRsets über
/zones/{id}/rrsets(A und AAAA) holen. - Den RRset-Namen exakt mit dem Host abgleichen (inkl.
@am Apex). Es wird nur der exakt passende Record aktualisiert; Fallbacks auf Eltern-Records gibt es absichtlich nicht. set_recordsaufrufen, um die IPs zu ersetzen — und zwar nur für die Adressfamilien, die der Client tatsächlich geliefert hat (IPv4-only, Dual-Stack oder IPv6-only).- Fehler bleiben als
needs_syncmarkiert und werden vom Cron erneut versucht.
- Zone über
- Migration von älteren Skript-Versionen
- Die Legacy-API (
dns.hetzner.com/api/v1) wurde von Hetzner im Mai 2026 abgeschaltet; der zugehörige Code-Pfad ist entfernt. Die Config-Schlüsseldns_token,dns_endpointundapi_orderwerden ignoriert (bei aktivemdebugmit Hinweis im Log). - Pro Realm ist jetzt ein
console_tokenPflicht; die SQLite-DB bleibt kompatibel und muss nicht angefasst werden. - Nach dem Update einmal
php hetzner_dyndns.php --cronausführen, um offene Jobs abzuarbeiten — der Cron-Aufruf über die Kommandozeile benötigt (und akzeptiert) keine HTTP-Zugangsdaten.
- Die Legacy-API (
- Mehrere Zonen und Realms
- Lege pro Zone oder Subdomain einen Realm an. Beispiel: Realm
gjsi.deundddns.gjsi.demit unterschiedlichen Tokens und TTLs. - Nutze
realm=<name>im DynDNS-Query-String, wenn du mehrere Domains über denselben Endpunkt bedienst. - Der optionale CLI-Helfer
hetzner_dyndns_listhosts.phpkann gecachte IPv4/IPv6 pro Realm ausgeben, ohne den HTTP-Endpunkt zu treffen.
- Lege pro Zone oder Subdomain einen Realm an. Beispiel: Realm
- Benachrichtigungen und Observability
- E-Mails zu Erfolgen und Fehlern über PHP
mail()oder SMTP. debugunddebug_logzeichnen alle API-Requests und -Responses plus Benachrichtigungsstatus auf.- Cron-Summen listen Gesamtzahl, Erfolge und Fehler pro Host.
- E-Mails zu Erfolgen und Fehlern über PHP
- Sicherheit und Deployment
.htaccessarbeitet nach dem Prinzip „alles verboten": Nur die beiden DynDNS-Endpunkte und/.well-known/sind erreichbar, und nur per GET/HEAD. Frühere Versionen nutzten ein verankertes Dateinamen-Muster (^\.sqlite3$), das nie aufhetzner_dyndns.sqlite3passte, und ihre Regel gegen direkte PHP-Aufrufe ließ sich mit einem Query-String umgehen, der/nic/updateenthält; beides wurde im Oktober 2026 behoben..user.inideaktiviert den Session-Upload-Progress (die Komponente hinter CVE-2026-109619 und CVE-2026-115560), blendet PHP-Fehler aus und begrenzt POST-Größen. Die Skripte erzwingen zusätzlichdisplay_errorsaus, damit ein unbehandelter Fehler nie Serverpfade verrät.- Tokens nur auf DNS scopen und regelmäßig rotieren.
- Passwortvergleiche laufen timing-sicher über
hash_equals(). - SQLite-DB und Debug-Log müssen für den PHP-User schreibbar sein; nach Möglichkeit außerhalb öffentlicher Webroots ablegen.
- DynDNS-Endpunkt per HTTPS bereitstellen. Reines HTTP würde dein Passwort verraten.
?p=<passwort>landet zudem in Server-Logs — wenn dein Client Basic Auth kann, nutze Basic Auth.
Ergebnis
Die Bridge liefert dir einen freundlichen DynDNS-Endpunkt auf Basis der Hetzner Console API — inklusive IPv6-only-Support, Retry-Logik und sauberem DynDNS2-Protokoll. Starte mit dem Schnellstart, damit Updates laufen, und wirf dann einen Blick in die Nerd-Ecke, wenn du Realms, Benachrichtigungen oder die Migration von einer älteren Skript-Version feintunen willst.