Files
dyndns-server/README.md
T
Stefan Hacker c3070469c1 Security-Hardening (Pentest-Findings F-02 bis F-07)
- CSRF-Schutz: session-gebundenes Token in allen POST-Formularen, serverseitig
  per before_request geprueft; /nic/update ausgenommen (Basic-Auth-API)
- Brute-Force-Schutz: DB-gestuetzter Login-Lockout pro Client-IP
  (5 Fehlversuche -> 15 min), echte IP via ProxyFix/X-Forwarded-For
- SSRF: validate_plesk_url() erzwingt http(s) und blockt Link-Local/Metadata,
  Multicast und reservierte Ziele
- Session-Cookies: HttpOnly, SameSite=Lax, Secure (per Env abschaltbar)
- Security-Header: CSP, X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Generische Plesk-Fehlermeldungen (keine internen URLs im UI)
- CSS/JS nach static/ ausgelagert -> strikte CSP ohne 'unsafe-inline'
- login_attempts-Tabelle + README-Security-Abschnitt

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 14:45:27 +02:00

212 lines
8.6 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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
```bash
# 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.home` → `pc.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` weggelassen** → **alle** 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`:
```bash
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`](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_id` →
`dyndns_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)
```bash
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
```