c3070469c1
- 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>
212 lines
8.6 KiB
Markdown
212 lines
8.6 KiB
Markdown
# 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
|
||
```
|