diff --git a/README.md b/README.md index f8e2888..c1a3430 100644 --- a/README.md +++ b/README.md @@ -21,13 +21,15 @@ Produktionsnahes internes Tool zur Erstellung von Thomas-Krenn.AG Newslettern au - Plain Text (für Outlook) - Raw HTML - Optionaler SMTP-Versand: - - UI-konfigurierbar - - Verteilerlisten - - flexibler Zeitplan (täglich / wöchentlich an bestimmten Wochentagen / monatlich an einem Tag) über einen Hintergrund-Scheduler + - UI-konfigurierbar (nur für Admins) + - Verteilerlisten mit E-Mail-Validierung + - Versand per **BCC** (Empfänger nicht im sichtbaren `To:`-Header) + - flexibler Zeitplan (täglich / wöchentlich / monatlich) über einen Hintergrund-Scheduler - eigene Inhalts-Vorgaben für den geplanten Versand (Zeitraum, Artikel-Auswahl, Kategorie) - - manueller Versand nur per Button – „Generieren“ verschickt nie automatisch - - Versandprotokoll -- Docker/Compose Betrieb, inkl. automatischem Image-Build & Push in die Gitea Container Registry (per Versions-Tag) + - **„Generieren“ verschickt nie** – manueller Versand nur per Button „Newsletter jetzt senden“ + - automatischer Versand nur mit **zwei** aktiven Häkchen: Zeitplan + Bestätigung „ohne manuelle Freigabe“ + - Versandprotokoll (nur für Editor/Admin sichtbar) +- Docker/Compose Betrieb mit Container-Hardening, inkl. automatischem Image-Build & Push in die Gitea Container Registry (per Versions-Tag) ## Start mit Docker @@ -37,11 +39,17 @@ Produktionsnahes internes Tool zur Erstellung von Thomas-Krenn.AG Newslettern au cp .env.example .env ``` -2. Wichtige Werte in `.env` setzen: - - `HOST_PORT` (z. B. `8080`, falls `8000` bereits belegt ist) - - `SECRET_KEY` - - `ADMIN_BOOTSTRAP_EMAIL` - - `ADMIN_BOOTSTRAP_PASSWORD` +2. Wichtige Werte in `.env` setzen (siehe auch [Konfiguration](#konfiguration-env)): + + | Variable | Lokal (Entwicklung) | Produktion | + |----------|---------------------|------------| + | `ENVIRONMENT` | `development` | `production` | + | `HOST_PORT` | z. B. `8080` | nach Bedarf | + | `SECRET_KEY` | beliebig lang | mind. 32 zufällige Zeichen | + | `ALLOWED_HOSTS` | `localhost,127.0.0.1` | konkreter Hostname | + | `COOKIE_SECURE` | `false` | `true` (mit TLS) | + | `ADMIN_BOOTSTRAP_EMAIL` | Admin-E-Mail | Admin-E-Mail | + | `ADMIN_BOOTSTRAP_PASSWORD` | Bootstrap-Passwort | starkes Passwort | 3. Start: @@ -52,6 +60,30 @@ Produktionsnahes internes Tool zur Erstellung von Thomas-Krenn.AG Newslettern au 4. Zugriff: - [http://localhost:8080/login](http://localhost:8080/login) (oder dein `HOST_PORT`) +> **Hinweis:** Mit `ENVIRONMENT=production` prüft die App beim Start strikt die Konfiguration. Fehlen starke Secrets oder sind unsichere Defaults gesetzt, startet der Container nicht. + +## Konfiguration (`.env`) + +Vollständige Vorlage: `.env.example` + +| Variable | Beschreibung | +|----------|--------------| +| `ENVIRONMENT` | `development` = lokale Entwicklung ohne harte Checks; `production` = erzwingt sichere Einstellungen | +| `SECRET_KEY` | JWT-Signierung und Verschlüsselung von SMTP-Passwörtern in der DB | +| `ALGORITHM` | JWT-Algorithmus (Standard: `HS256`) | +| `ACCESS_TOKEN_EXPIRE_MINUTES` | Gültigkeit der Login-Session in Minuten (Standard: `120`) | +| `JWT_ISSUER` / `JWT_AUDIENCE` | JWT-Claims zur Token-Validierung | +| `HOST_PORT` | Host-Port für Docker Compose (nur Compose, nicht App-intern) | +| `DATABASE_URL` | SQLite (Standard) oder PostgreSQL | +| `WIKI_API_URL` | MediaWiki-API-Endpunkt | +| `ALLOWED_HOSTS` | Kommagetrennte erlaubte Host-Header | +| `COOKIE_SECURE` | `Secure`-Flag für Session- und CSRF-Cookies (Pflicht `true` in Produktion) | +| `HSTS_MAX_AGE` | HSTS-Header-Dauer in Sekunden (nur bei HTTPS) | +| `ADMIN_BOOTSTRAP_EMAIL` / `ADMIN_BOOTSTRAP_PASSWORD` | Erster Admin-Account beim Start | +| `ADMIN_BOOTSTRAP_RESET` | Admin auf `.env`-Werte zurücksetzen (nur Entwicklung/Notfall, in Produktion verboten) | +| `EDITOR_TIP_MAX_LENGTH` | Max. Zeichen für Redaktionsnotiz (Standard: `10000`) | +| `HIGHLIGHTS_MAX_LENGTH` | Max. Zeichen für Top-Highlights (Standard: `5000`) | + ## Container-Image (Gitea Packages) Fertige Images werden automatisch in die Gitea Container Registry veröffentlicht: @@ -71,6 +103,9 @@ docker run -d --name tk-newsletter-admin \ --env-file .env \ -p 8080:8000 \ -v newsletter_data:/app/data \ + --read-only \ + --tmpfs /tmp \ + --security-opt no-new-privileges:true \ git.hexahost.dev/smueller/tk-wiki-newsletter:latest ``` @@ -83,11 +118,14 @@ services: # build: . # <- nicht mehr nötig, wenn das fertige Image genutzt wird ``` +Die mitgelieferte `docker-compose.yml` setzt bereits `read_only`, `no-new-privileges` und ein `tmpfs` für `/tmp`. Persistente Daten liegen im Volume `newsletter_data` unter `/app/data`. + ### Automatischer Build (CI/CD) Der Workflow `.gitea/workflows/docker-build.yml` baut und pusht das Image über Gitea Actions. - **Auslöser**: nur beim Setzen eines Versions-Tags `v*` (kein Build bei normalen Pushes auf `dev`/`main`). +- **Vor dem Build**: `pip-audit` prüft die Python-Abhängigkeiten aus `requirements.txt`. - **Release-Ablauf** (Build bei Merge `dev` → `main` mit Version): ```bash @@ -106,25 +144,74 @@ Der Workflow `.gitea/workflows/docker-build.yml` baut und pusht das Image über ## Admin-Login & Passwort zurücksetzen - Das Login erfolgt über **E-Mail + Passwort** (kein separater Benutzername). Beim ersten Start wird der Admin aus `ADMIN_BOOTSTRAP_EMAIL` / `ADMIN_BOOTSTRAP_PASSWORD` angelegt. +- **Passwortpolitik:** mindestens 10 Zeichen, mindestens ein Großbuchstabe, ein Kleinbuchstabe und eine Ziffer. +- **Rate-Limiting:** nach 5 fehlgeschlagenen Anmeldeversuchen pro IP/E-Mail innerhalb von 5 Minuten wird der Login temporär blockiert. - **Wichtig:** Existiert der Admin bereits (persistente DB im Volume `newsletter_data`), wird eine spätere Passwort-Änderung in der `.env` normalerweise **nicht** übernommen. -- Um Passwort/Rolle/Status auf die `.env`-Werte zurückzusetzen: `ADMIN_BOOTSTRAP_RESET=true` setzen und Container neu starten: +- Um Passwort/Rolle/Status auf die `.env`-Werte zurückzusetzen (**nur Entwicklung**): `ADMIN_BOOTSTRAP_RESET=true` setzen und Container neu starten: ```bash docker compose up -d --force-recreate ``` - Nach erfolgreichem Login `ADMIN_BOOTSTRAP_RESET=false` setzen, damit UI-Passwortänderungen nicht bei jedem Neustart überschrieben werden. + Nach erfolgreichem Login `ADMIN_BOOTSTRAP_RESET=false` setzen. In `ENVIRONMENT=production` ist `ADMIN_BOOTSTRAP_RESET=true` nicht erlaubt – die App startet dann nicht. -## Sicherheits-Hinweise für Produktion - -- Reverse Proxy (z. B. Nginx/Traefik) mit TLS vor den Container setzen. -- `SECRET_KEY` lang und zufällig setzen. -- Bootstrap-Admin-Passwort nach erstem Login ändern. -- Netzwerkzugriff auf interne IPs/Netze beschränken. -- Regelmäßige Backups des `data` Volumes. +- **Passwort ändern** (Profil) oder **Abmelden** beendet alle aktiven Sessions des Benutzers. ## Rollenmodell -- `reader`: darf Ergebnisse sehen -- `editor`: darf Newsletter generieren und sofort versenden -- `admin`: zusätzlich Benutzerverwaltung und SMTP-Konfiguration +| Rolle | Rechte | +|-------|--------| +| `reader` | Newsletter-Vorschau und Artikellisten ansehen | +| `editor` | Newsletter generieren, exportieren und manuell versenden; Versandprotokoll und Empfängerliste einsehen | +| `admin` | Zusätzlich Benutzerverwaltung (anlegen, Rolle ändern, aktivieren/deaktivieren) und SMTP-Konfiguration | + +Admins können in der Benutzerverwaltung (`/admin/users`): + +- neue Benutzer mit Rolle anlegen +- die Rolle bestehender Benutzer ändern (beendet deren Sessions) +- Benutzer aktivieren oder deaktivieren (beendet deren Sessions) + +Das eigene Konto kann weder deaktiviert noch die eigene Rolle geändert werden. + +## Sicherheit + +### Eingebaute Schutzmaßnahmen + +- **bcrypt** für Passwort-Hashes +- **CSRF** Double-Submit-Cookie auf allen POST-Formularen +- **HttpOnly** + **SameSite=Strict** Session-Cookies; `Secure` bei HTTPS (`COOKIE_SECURE=true`) +- **JWT** mit `iss`, `aud`, Ablaufzeit und Session-Version (`session_version`) +- Session-Invalidierung bei Logout, Passwortwechsel, Rollen- oder Statusänderung +- **SMTP-Passwort** verschlüsselt in der Datenbank (Fernet), nicht im HTML-Formular +- **Security-Header:** CSP, `X-Frame-Options`, `X-Content-Type-Options`, `Referrer-Policy`, HSTS (bei HTTPS), `Cache-Control: no-store` auf geschützten Seiten +- **OpenAPI** (`/docs`, `/redoc`) in Produktion deaktiviert +- Abhängigkeiten in `requirements.txt` versioniert; CI führt `pip-audit` aus + +### Produktions-Checkliste + +Bei `ENVIRONMENT=production` erzwingt die App beim Start: + +- `SECRET_KEY` mit mindestens 32 Zeichen (keine bekannten Defaults) +- starkes `ADMIN_BOOTSTRAP_PASSWORD` +- `ADMIN_BOOTSTRAP_RESET=false` +- `ALLOWED_HOSTS` mit konkreten Hostnamen (nicht `*`) +- `COOKIE_SECURE=true` + +Zusätzlich empfohlen: + +- Reverse Proxy (z. B. Nginx/Traefik) mit TLS vor den Container; Proxy muss `X-Forwarded-Proto: https` setzen +- Bootstrap-Admin-Passwort nach erstem Login im Profil ändern +- Netzwerkzugriff auf interne IPs/Netze beschränken +- Regelmäßige Backups des `newsletter_data` Volumes (SQLite-Datei unverschlüsselt) + +### Beispiel `.env` für Produktion + +```env +ENVIRONMENT=production +SECRET_KEY= +ALLOWED_HOSTS=newsletter.intern.firma.de +COOKIE_SECURE=true +ADMIN_BOOTSTRAP_EMAIL=admin@firma.de +ADMIN_BOOTSTRAP_PASSWORD= +ADMIN_BOOTSTRAP_RESET=false +```