Stefan Hacker a5787a5393 F-08: Brute-Force-Schutz fuer /nic/update
- Lockout-Mechanismus um 'scope' erweitert (login vs. dyndns getrennt gezaehlt)
- /nic/update sperrt Client-IPs nach 5 fehlgeschlagenen Basic-Auth-Versuchen
  (dyndns2-Antwort 'abuse', 403); erfolgreiche Auth setzt den Zaehler zurueck

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:55:26 +02:00
2026-06-06 12:21:16 +02:00
2026-06-06 12:21:16 +02:00

DynDNS Manager für Plesk

Ein kleiner, selbst gehosteter DynDNS-Server mit Web-Oberfläche. Er nimmt DynDNS-v2-Updates (wie sie z. B. ein Telekom Speedport unter „Anderer Anbieter" sendet) entgegen und trägt die gemeldete IP-Adresse über die Plesk REST-API als A-Record in deine DNS-Zone ein.

  • Mehrere DynDNS-Benutzer, mehrere Subdomains (DNS-Namen) pro Benutzer
  • Admin-Weboberfläche (Flask + Bootstrap) zum Verwalten von Benutzern, Subdomains und Plesk-Einstellungen
  • Update-Log pro Subdomain
  • Läuft als Docker-Container hinter einem nginx-TLS-Proxy

Schnellstart

# 1. Container bauen und starten
docker compose up -d --build

# 2. Weboberfläche öffnen (lokal, hinter nginx -> https)
#    http://127.0.0.1:5080
#    Standard-Login:  admin / admin   (sofort ändern!)

Die SQLite-Datenbank wird beim ersten Start automatisch unter ./data/dyndns.db angelegt (Volume-Mount in docker-compose.yml). Existiert die Datei nicht, wird sie samt Tabellen erzeugt; ein Default-Admin (admin / admin) wird angelegt.


Konfiguration

Umgebungsvariablen (docker-compose.yml)

Variable Bedeutung Default
DB_PATH Pfad zur SQLite-Datei im Container /data/dyndns.db
SECRET_KEY Flask-Session-Key — unbedingt ändern (openssl rand -hex 32) zufällig pro Start
SESSION_COOKIE_SECURE Session-Cookie nur über HTTPS senden. Für lokale http-Tests auf 0 setzen. 1

SECRET_KEY unbedingt setzen: Bleibt er auf dem Default, wird bei jedem Neustart ein zufälliger Key erzeugt und alle Sessions werden ungültig.

Wichtig: Das Volume mountet ein Verzeichnis (./data:/data), nicht die Datei direkt. Würde man die noch nicht existierende Datei mounten (./dyndns.db:/data/dyndns.db), legt Docker sie als Verzeichnis an und SQLite scheitert mit unable to open database file.

Plesk-Einstellungen (in der Weboberfläche → Einstellungen)

Feld Beispiel Bedeutung
Plesk-URL https://plesk.example.com:8443 Basis-URL der Plesk-Installation
API-Key XXXXXXXX-... Plesk REST-API-Key (in Plesk unter API-Schlüssel erzeugen)
Basis-Domain example.com DNS-Zone, in die die A-Records geschrieben werden
SSL verifizieren ☑/☐ bei selbstsigniertem Plesk-Zertifikat abschalten

Mit Verbindung testen lässt sich die API prüfen.


Benutzer & Subdomains anlegen

  1. In der Weboberfläche auf Benutzer → Benutzer anlegen.

  2. DynDNS-Benutzername und Passwort vergeben (das sind die Zugangsdaten, die später im Router eingetragen werden).

  3. Eine oder mehrere Subdomains eintragen — getrennt durch Komma, Leerzeichen oder Zeilenumbruch, z. B.:

    mypc
    nas
    router
    

    Zusammen mit der Basis-Domain (example.com) ergeben sich daraus die Hostnamen mypc.example.com, nas.example.com, router.example.com.

    Auch mehrstufige Namen sind erlaubt (pc.homepc.home.example.com), solange die Records in der DNS-Zone der Basis-Domain liegen.

Weitere Subdomains lassen sich später jederzeit über das +-Symbol in der Benutzerzeile hinzufügen oder per × am Badge entfernen.

Hinweis: Der DNS-A-Record wird nicht schon beim Anlegen in Plesk erstellt, sondern erst beim ersten DynDNS-Update vom Client (lazy). Bis dahin zeigt das Dashboard „noch kein Update".


Router / Speedport einrichten

Im Router unter DynDNS „Anderer Anbieter" konfigurieren:

Feld Wert
Update-URL https://dyndns.example.com/nic/update?hostname=<domain>&myip=<ipaddr>
Domainname z. B. mypc.example.com
Benutzername der angelegte DynDNS-Benutzername
Passwort das zugehörige Passwort

Die Platzhalter <domain> und <ipaddr> füllt der Router automatisch. Authentifiziert wird per HTTP Basic Auth (Benutzername/Passwort).

Update-Endpoint /nic/update

Standard-DynDNS-v2-Protokoll:

GET /nic/update?hostname=mypc.example.com&myip=203.0.113.7
Authorization: Basic <user:pass>

Verhalten:

  • hostname angegeben → nur die passende(n) Subdomain(s) dieses Benutzers werden aktualisiert. Erlaubt ist der volle FQDN (mypc.example.com) oder nur das Label (mypc); mehrere durch Komma getrennt.
  • hostname weggelassenalle Subdomains des Benutzers werden auf die gemeldete IP gesetzt.
  • myip (oder ip) bestimmt die Adresse; fehlt sie, wird die Quell-IP des Requests verwendet.

Antworten (eine Zeile pro Subdomain):

Antwort Bedeutung
good <ip> Record erfolgreich gesetzt
nochg <ip> IP unverändert, nichts zu tun
nohost Benutzer hat keine (passende) Subdomain
badauth Benutzername/Passwort falsch
911 Plesk nicht konfiguriert (URL/Key/Domain fehlt)
dnserr Fehler beim Schreiben in Plesk (siehe Log)

Beispiel-Test mit curl:

curl -u stefan:geheim \
  "https://dyndns.example.com/nic/update?hostname=mypc.example.com&myip=203.0.113.7"

TLS / Reverse Proxy

Der Container lauscht nur lokal (127.0.0.1:5080). Die TLS-Terminierung übernimmt nginx — eine Beispielkonfiguration liegt in nginx-subdomai-example.conf. Domain in Plesk anlegen, Let's-Encrypt-Zertifikat ausstellen, dann die Datei als zusätzliche nginx-Direktiven einbinden.


Sicherheit

  • CSRF-Schutz: Alle ändernden POST-Formulare tragen ein Session-gebundenes Token (csrf_token), das serverseitig per before_request geprüft wird. Der Router-Endpoint /nic/update ist ausgenommen (reine API mit Basic-Auth).
  • Brute-Force-Schutz: Nach LOGIN_MAX_FAILS (5) Fehlversuchen pro Client-IP wird der Login für LOGIN_LOCK_MINUTES (15) gesperrt. Die echte Client-IP wird über X-Forwarded-For (nginx, via ProxyFix) ermittelt.
  • Session-Cookies: HttpOnly, SameSite=Lax und (per Default) Secure.
  • Security-Header: Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Referrer-Policy: no-referrer.
  • SSRF-Schutz: Die admin-konfigurierte Plesk-URL muss http(s) sein; Ziele in Link-Local-/Cloud-Metadata- (169.254.0.0/16), Multicast- und reservierten Bereichen werden abgelehnt. Private-/Loopback-Adressen bleiben erlaubt, da Plesk häufig intern oder am selben Host läuft.
  • Generische Fehlermeldungen: Verbindungsfehler zu Plesk leaken keine internen URLs mehr; Details landen nur im Server-Log.

Erste Maßnahme nach dem Setup: Das Default-Login admin / admin unter Einstellungen → Admin-Passwort ändern sofort ersetzen.

Architektur

app/
├── main.py        Flask-Routen: Login, Dashboard, Benutzer/Subdomains, /nic/update
├── database.py    SQLite-Schema, Migration, Settings-Helfer
├── plesk.py       Plesk-REST-API: Verbindungstest + A-Record anlegen/aktualisieren
├── wsgi.py        gunicorn-Einstieg (ruft init_db beim Start)
├── templates/     Bootstrap-Oberfläche
└── static/        ausgelagertes CSS/JS (ermöglicht strikte CSP ohne 'unsafe-inline')

Datenmodell

  • dyndns_users — Zugangsdaten (Benutzername/Passwort), aktiv-Flag
  • subdomains — beliebig viele DNS-Namen je Benutzer (dyndns_user_iddyndns_users.id), inkl. aktueller IP und Zeitpunkt des letzten Updates
  • update_log — Verlauf pro Subdomain
  • admin_users, settings — Admin-Login und Plesk-Konfiguration

Beim Start migriert init_db() automatisch ältere Datenbanken, die noch eine einzelne subdomain-Spalte in dyndns_users hatten, in die neue subdomains-Tabelle.


Lokale Entwicklung (ohne Docker)

cd app
pip install -r requirements.txt
export DB_PATH=./dev.db
export SECRET_KEY=dev
python main.py        # http://127.0.0.1:5000
S
Description
No description provided
Readme 115 KiB
Languages
Python 50%
HTML 42.4%
Shell 4.7%
CSS 1.9%
JavaScript 0.6%
Other 0.4%