Ergebnis einer beauftragten Sicherheitsbegutachtung. Die WebAuthn- Zeremonie selbst ist nicht angefasst worden - sie war in Ordnung. Alle Befunde lagen DANEBEN: Ein zweiter Faktor ist nur so stark wie der schwaechste Weg zum Konto, und Sitzungen, die aus ihm entstehen, erben seinen Schutz nicht. KRITISCH /api/setup/v1/login prueste auf totp_secret - eine Spalte, die kein Anmeldeweg auswertet - und nicht auf dc_user_credentials. Ein Administratorpasswort genuegte dort fuer ein setup:install-Token ohne Hardwarekey. Neu: Konten mit zweitem Faktor abgewiesen, und die Rollentrennung gilt symmetrisch. Der Docblock behauptete sie seit jeher; erzwungen hat sie nichts. HOCH secondFactorRequired(), accountLockoutSeconds() und lockoutSeconds() fingen jedes Throwable und fielen auf "kein Faktor" bzw. "nicht gesperrt" zurueck. Gefangen wurden damit auch Verbindungsabbrueche und Lock-Timeouts - jeder davon schaltete still fuer alle Konten auf Passwort-allein. Toleriert werden jetzt nur noch 42S02 und 42S22. HOCH Fuenf Aktionen aenderten die Anmeldemittel ohne erneute Bestaetigung. Eine uebernommene Sitzung konnte sich damit dauerhaft einnisten. Neu: Authentifizierung aus den letzten zehn Minuten noetig, geprueft an derselben Stelle, an der auch der Leerlauf sperrt. WEITER - Leerlauf 30 statt 120 Minuten, und GESPERRT statt beendet: AAL2 laesst zur Reaktivierung einen Faktor genuegen. Wer alle 30 Minuten den Hardwarekey vorzeigen muss, laesst ihn stecken. - Migration 022: session_epoch. Ein Passwortwechsel beendet die uebrigen Sitzungen des Kontos, die eigene bleibt. - Kontosperre wirkt auf der zweiten Stufe; Zaehler gegen unbegrenztes Raten im Zwischenzustand. - Drosselung auf login.php (10/60s) und /api/auth/v1/ (30/60s). - Anmeldefehler ununterscheidbar; die IP-Sperre darf benannt bleiben, sie sagt nichts ueber ein Konto. - HSTS, TLS-Zwang VOR allen Routen, CSP. Die CSP traegt wegen 93 Inline-Handlern nicht gegen XSS - das steht so im Kommentar. - session.use_strict_mode, CSRF-Token bei der Anmeldung verworfen, Secure-Flag aus app.url statt aus $_SERVER['HTTPS']. - rpId/expectedOrigin nicht mehr aus dem Host-Header. /api/health meldet unter checks.webauthn_anchor, ob der Anker steht. - Notfalltoken laeuft nach 180 Tagen ab, geprueft nach dem Hashvergleich. - config.example.php ohne echtes Passwort, verify_ssl auf true. - Serverdaten.txt.bak mitgesperrt, dc_login_attempts raeumt auf. BELEGT 188 von 188 Testfaellen gruen, darunter drei neue in der Gruppe "haertung". Die Blockade einer veralteten Sitzung liess sich nicht in der Suite pruefen - sie verlangt, das Zehn-Minuten-Fenster wirklich abzuwarten. Dafuer gibt es ein Werkzeug ausserhalb; es belegt, dass der Formularweg auf den Sperrbildschirm fuehrt, die Regel dabei unveraendert bleibt, der JSON-Weg reauth_required meldet und das Passwort allein wieder oeffnet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
885 lines
37 KiB
Markdown
885 lines
37 KiB
Markdown
# Umstellung auf Version 2.0 — Ablaufplan
|
|
|
|
Diese Fassung enthält Sicherheitskorrekturen, die das Verhalten der
|
|
Schnittstellen ändern. Bitte in dieser Reihenfolge vorgehen.
|
|
|
|
> **Version 2.1** ergänzt Korrekturen im .NET-SDK und ein neues Feld am
|
|
> Heartbeat. Sie erfordert keine Konfigurationsänderung, aber eine
|
|
> Datenbankmigration und ein Nachziehen der Clients:
|
|
> **[§14 Umstellung auf 2.1](#14-umstellung-auf-21)**.
|
|
>
|
|
> **Version 2.2** überarbeitet den UpdateService grundlegend: Plattform-Dimension,
|
|
> signierte Releases, Staging-Swap mit Rollback und geschützte Konfigurationsdateien.
|
|
> **Vor dem nächsten Release einzuspielen** —
|
|
> **[§15 Umstellung auf 2.2](#15-umstellung-auf-22)**.
|
|
>
|
|
> **Version 2.4** stellt die Release-Ablage hinter einen Zugangsschutz.
|
|
> **Reihenfolge beachten:** erst das SDK ausliefern, dann scharfschalten —
|
|
> sonst bekommen bestehende Installationen keine Updates mehr.
|
|
> **[§16 Umstellung auf 2.4](#16-umstellung-auf-24)**.
|
|
|
|
---
|
|
|
|
## 1. Vor dem Deployment: Zugangsdaten wechseln
|
|
|
|
`Serverdaten.txt`, `config/config.php`, `config/.htpasswd` und
|
|
`scripts/deploy_config.json` lagen im Git-Repository. Sie sind jetzt per
|
|
`.gitignore` ausgeschlossen und aus dem Index entfernt — **die Git-Historie
|
|
enthält sie aber weiterhin**. Alle betroffenen Zugangsdaten sind daher als
|
|
kompromittiert zu behandeln:
|
|
|
|
- [ ] MySQL-Passwort ändern, danach in `config/config.php` eintragen
|
|
- [ ] FTP-Passwort ändern, danach in `scripts/deploy_config.json` eintragen
|
|
- [ ] `.htpasswd`-Passwort für `deploy` neu setzen
|
|
- [ ] Git-Token `eb429575…` widerrufen und neu ausstellen
|
|
- [ ] Admin-Passwort im WebUI ändern (die alte Fassung setzte es bei jedem
|
|
Aufruf von `install_db.php` auf `Admin1337!Admin1337!` zurück — jeder im Internet
|
|
konnte das auslösen)
|
|
|
|
Wenn die Historie bereinigt werden soll, geht das mit
|
|
[`git filter-repo`](https://github.com/newren/git-filter-repo). Das schreibt
|
|
alle Commit-Hashes um; bei einem Repository mit mehreren Nutzern vorher abstimmen.
|
|
|
|
---
|
|
|
|
## 2. Konfiguration ergänzen
|
|
|
|
`config/config.php` braucht drei neue Schlüssel unter `security`. Die
|
|
mitgelieferte Datei enthält bereits erzeugte Werte; für eine neue Installation:
|
|
|
|
```bash
|
|
cp config/config.example.php config/config.php
|
|
openssl rand -hex 32 # je einmal für shared_key, webhook_key, license_key
|
|
```
|
|
|
|
| Schlüssel | Zweck |
|
|
|---|---|
|
|
| `security.shared_key` | Server-zu-Server-Aufrufe: Evaluator-Cron, Migration, Deaktivierung |
|
|
| `security.webhook_key` | HMAC-Signatur ausgehender Webhooks |
|
|
| `security.license_key` | Signatur der Offline-Lizenzdateien (`.lic`) |
|
|
| `app.debug` | Auf Produktivsystemen `false` — steuert, ob Exception-Texte ausgeliefert werden |
|
|
|
|
> Der bisherige `shared_key` (`DC_MASTER_SECURE_TOKEN_2026_x98f`) stand im
|
|
> Repository und wurde ersetzt. Wer ihn irgendwo eingetragen hat — etwa für
|
|
> `/api/license/v1/deactivate` — muss den neuen Wert nachziehen.
|
|
|
|
---
|
|
|
|
## 3. Deployment
|
|
|
|
```bash
|
|
python scripts/deploy.py
|
|
```
|
|
|
|
Das Skript überträgt unter anderem die neuen Verzeichnisse `var/` (Logs) und
|
|
die zusätzlichen `.htaccess`-Dateien in `config/`, `src/` und `sql/`.
|
|
|
|
---
|
|
|
|
## 4. Migration ausführen
|
|
|
|
Im WebUI anmelden, dann **System → DB-Migration → Migration jetzt ausführen**.
|
|
|
|
Alternativ über die Kommandozeile:
|
|
|
|
```bash
|
|
php public/install_db.php
|
|
```
|
|
|
|
Oder mit dem Shared Key:
|
|
|
|
```bash
|
|
curl -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/install_db.php
|
|
```
|
|
|
|
Die Migration ist additiv und legt an bzw. korrigiert:
|
|
|
|
- `dc_migrations` — vermerkt angewendete Versionen, damit nichts doppelt läuft
|
|
- `dc_login_attempts` — Drosselung fehlgeschlagener Anmeldungen
|
|
- `dc_webhooks` — ausgehende Benachrichtigungen
|
|
- Bugtracker: Claim/Lease, `client_ref`, `dedup_key`, Code-Kontextfelder, Indizes
|
|
- Watchdog: `last_state_change_utc`, `down_since_utc`, Zustand `unknown`
|
|
- **Reparatur von Migration 005** — deren Spalten (`push_id`, `target_agent`,
|
|
`tags`) fehlten bisher auf Datenbanken, die aus Migration 004 stammen. Die
|
|
alte Fassung nutzte dynamisches SQL, dessen Semikolons in String-Literalen
|
|
vom damaligen Installer als Statement-Ende gelesen wurden; die Fehler wurden
|
|
stillschweigend verschluckt.
|
|
- **Reparatur der Token-Hashes** — die Validierung vergleicht jetzt nur noch
|
|
den SHA-256-Hash. Die geseedeten Beispiel-Tokens trugen Hashes, die nicht zu
|
|
ihrem Klartext passten; sie werden korrigiert, damit bestehende Tokens
|
|
weiterhin funktionieren.
|
|
|
|
---
|
|
|
|
## 5. Cron für den Watchdog-Evaluator einrichten
|
|
|
|
**Ohne diesen Schritt sind die Monitor-Zustände wertlos.** Der Evaluator fehlte
|
|
bisher vollständig — der Zustand änderte sich nur beim Eintreffen eines
|
|
Heartbeats, ein ausgefallener Server blieb dauerhaft grün.
|
|
|
|
### Empfohlen: interner Aufruf (seit 2.1)
|
|
|
|
```bash
|
|
* * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
|
|
```
|
|
|
|
Das entspricht dem früheren `watchdog/cli/tick.php`. Gegenüber dem HTTP-Weg:
|
|
|
|
- **Kein Schlüssel im Crontab.** `ps` zeigt die Argumente laufender Prozesse
|
|
jedem Nutzer der Maschine — bei einem `curl -H "Authorization: Bearer …"`
|
|
also auch den Shared Key.
|
|
- **Keine Abhängigkeit von Webserver, TLS oder DNS.** Der Evaluator läuft auch
|
|
dann, wenn der Webserver klemmt — also gerade dann, wenn das Erkennen von
|
|
Ausfällen wichtig ist.
|
|
- **Keine überlappenden Läufe.** Das Skript sperrt sich per `flock` selbst; ein
|
|
langsamer Vorlauf führt nicht zu doppelten Alarmen.
|
|
|
|
`--quiet` gibt nur bei Zustandswechseln und Fehlern etwas aus — sonst erzeugt
|
|
ein minütlicher Job 1440 Cron-Mails am Tag. Rückgabewerte: `0` erfolgreich,
|
|
`1` Fehler, `2` übersprungen (es lief bereits ein Tick).
|
|
|
|
### Alternativ: über die Schnittstelle
|
|
|
|
Sinnvoll, wenn der Cron auf einer anderen Maschine läuft als die Anwendung:
|
|
|
|
```bash
|
|
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
|
|
```
|
|
|
|
Beide Wege rufen denselben Code auf und schreiben denselben Lauf-Vermerk in
|
|
`watchdog_cron_jobs` — nur einen von beiden einrichten.
|
|
|
|
Solange der Job fehlt, zeigt das WebUI oben einen Warnhinweis mit einer
|
|
Schaltfläche für einen einmaligen Lauf.
|
|
|
|
---
|
|
|
|
## 6. Agenten-Tokens ausstellen
|
|
|
|
Die Ingest-Endpunkte verlangen jetzt zwingend ein Token.
|
|
|
|
1. WebUI → **Token-Verwaltung → Master-Token erstellen**
|
|
2. Scopes wählen (für einen Coding-Agenten: `bugtracker:report`,
|
|
`bugtracker:read`, `bugtracker:manage`)
|
|
3. Master-Token einmalig kopieren und auf dem Agenten-Rechner als `DC_TOKEN`
|
|
hinterlegen — oder den Agenten per `/api/tokens/v1/provision` ein eigenes
|
|
Sub-Token ziehen lassen
|
|
|
|
---
|
|
|
|
## 7. Bestehende Integrationen anpassen
|
|
|
|
| Betroffen | Was zu tun ist |
|
|
|---|---|
|
|
| Aufrufe von `/api/bugtracker/v1/report` ohne Token | Token-Header ergänzen |
|
|
| Aufrufe von `/api/bugtracker/v1/projects` ohne Token | Token-Header ergänzen |
|
|
| Skripte, die Releases veröffentlichen | Token mit `updateservice:publish` ergänzen |
|
|
| Auswertung der Antworten | Neues Format: `{"status":"success",…}` bzw. `{"status":"error","error":{"code":…}}` |
|
|
| Watchdog-Agenten mit `wd_live_…`-Token | Laufen unverändert weiter |
|
|
| Clients, die `/api/updateservice/v1/check` aufrufen | Unverändert, weiterhin ohne Token |
|
|
| Clients, die `/api/license/v1/validate` aufrufen | Unverändert, weiterhin ohne Token |
|
|
|
|
---
|
|
|
|
## 8. Zeitzonen
|
|
|
|
Datenbankzeitstempel liegen jetzt durchgängig in **UTC**; das WebUI rechnet für
|
|
die Anzeige in `app.timezone` (Europe/Berlin) um. Vorhandene Datensätze wurden
|
|
in Serverzeit geschrieben und erscheinen daher einmalig um den Zeitzonenversatz
|
|
verschoben. Für Monitoring-Daten ist das ohne Bedeutung, für den Audit-Log
|
|
gegebenenfalls beachten.
|
|
|
|
---
|
|
|
|
## 9. Prüfen, ob alles läuft
|
|
|
|
```bash
|
|
curl https://dc.mhdf.de/api/health -H "Authorization: Bearer <SHARED_KEY>"
|
|
```
|
|
|
|
Erwartet wird `"healthy": true`, eine leere `schema.pending`-Liste und ein
|
|
`checks.evaluator.ok` von `true`.
|
|
|
|
Zusätzlich stichprobenartig im WebUI prüfen:
|
|
|
|
- [ ] Anmeldung funktioniert
|
|
- [ ] Projekt anlegen und wieder löschen
|
|
- [ ] Monitor bearbeiten — das Feld **Betriebssystem** bleibt nach dem
|
|
Speichern erhalten (wurde zuvor bei jedem Speichern geleert)
|
|
- [ ] Bugtracker: „🔍 Details" öffnet den Dialog, „✔" öffnet den
|
|
Lösen-Dialog (dessen HTML fehlte bisher komplett)
|
|
- [ ] Token widerrufen und löschen (warf zuvor `HY093`)
|
|
- [ ] Lizenz-Aktivierung freigeben (warf zuvor `Class "Audit" not found`)
|
|
- [ ] Nach dem Speichern F5 drücken — es entsteht kein zweiter Eintrag mehr
|
|
|
|
---
|
|
|
|
## 10. Fehler-Stream (Migration 007)
|
|
|
|
Neu ist eine eigene Schnittstelle für Laufzeitfehler, gedacht für den globalen
|
|
Exception-Handler einer Anwendung:
|
|
|
|
```
|
|
POST /api/errors/v1/report
|
|
```
|
|
|
|
Gespeichert wird in derselben Tabelle wie der Bugtracker — ein zweiter Speicher
|
|
wäre nur ein zweiter Ort, an dem man suchen müsste. Die Trennung von Rauschen
|
|
und Signal leisten stattdessen **Ignore-Regeln**.
|
|
|
|
### Bekannte, harmlose Fehler stummschalten
|
|
|
|
Im WebUI unter **Bugtracker → 🔇 Ignore-Regeln**. Ein Treffer bedeutet nicht
|
|
„wegwerfen": Der Fehler wird weiterhin erfasst und hochgezählt, bleibt aber aus
|
|
der Übersicht heraus und löst keine Benachrichtigung aus.
|
|
|
|
Der Zähler ist dabei der eigentliche Zweck. Zu jeder Regel lässt sich eine
|
|
Alarmschwelle hinterlegen — dass ein bekannter Fehler auftritt, ist normal;
|
|
dass er plötzlich hundertmal so oft auftritt, bedeutet, dass sich etwas
|
|
geändert hat.
|
|
|
|
Eine deaktivierte Vorlage für den Duplicate-Entry-Fall liegt bereits vor; sie
|
|
lässt sich im WebUI anpassen und einschalten.
|
|
|
|
### Gruppierung
|
|
|
|
Veränderliche Anteile werden beim Zusammenfassen ausgeblendet: Werte in
|
|
Anführungszeichen, die Ziffern enthalten, Speicheradressen, GUIDs, Zeitstempel
|
|
und Zeilennummern. `Duplicate entry 'MKT-88213'` und `Duplicate entry 'AA-1'`
|
|
landen damit in einer Gruppe — ein anderer Unique-Key dagegen nicht.
|
|
|
|
> **Hinweis:** Migration 007 ändert die Berechnung des Gruppenschlüssels.
|
|
> Bereits erfasste Einträge behalten ihren alten Schlüssel; ein erneut
|
|
> auftretender Fehler legt daher einmalig eine neue Gruppe an. Danach ist der
|
|
> Zustand wieder konsistent.
|
|
|
|
## 11. Metrik-Verlauf und Health-Checks
|
|
|
|
Numerische Werte aus dem Heartbeat-Feld `metrics` werden jetzt mit Zeitstempel
|
|
abgelegt (Aufbewahrung 14 Tage) und lassen sich über
|
|
`GET /api/watchdog/v1/metrics` abfragen. Zuvor wurde `metrics_json` bei jedem
|
|
Heartbeat überschrieben — es gab immer nur den letzten Moment.
|
|
|
|
Zusätzlich kann eine Anwendung ihren Gesundheitszustand selbst mitschicken:
|
|
|
|
```json
|
|
{ "source": "polytrader-worker", "status": "ok", "interval": 60,
|
|
"checks": {
|
|
"db": { "ok": true },
|
|
"market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" }
|
|
} }
|
|
```
|
|
|
|
Das Deploymentcenter interpretiert die Namen nicht — es liest nur `ok` und
|
|
`message`. Schlägt eine Prüfung fehl, wird ein als `ok` gemeldeter Heartbeat auf
|
|
`warning` herabgestuft. **Es müssen keine Ports geöffnet werden**, der Weg ist
|
|
ausgehend.
|
|
|
|
Neu sind außerdem die Statuswerte `stopped` und `maintenance`. Ohne sie erzeugte
|
|
jedes geplante Herunterfahren wenige Minuten später einen Fehlalarm — es gab
|
|
schlicht keinen Weg, ein beabsichtigtes Ende mitzuteilen:
|
|
|
|
```json
|
|
{ "source": "polytrader-worker", "status": "stopped",
|
|
"message": "Dienst planmäßig beendet" }
|
|
```
|
|
|
|
Der Evaluator lässt solche Monitore in Ruhe, bis wieder ein normaler Heartbeat
|
|
eintrifft.
|
|
|
|
## 12. Abhängigkeitsbewusste Alarmierung
|
|
|
|
Fällt ein Monitor aus, für den `parent_source` gesetzt ist, und ist der
|
|
übergeordnete Monitor selbst unten, wird der Alarm für das Kind unterdrückt.
|
|
Der Zustand bleibt im Dashboard sichtbar.
|
|
|
|
Vorher erzeugte ein ausgefallener Hypervisor mit zwölf VMs dreizehn Meldungen
|
|
für ein Problem.
|
|
|
|
Damit das greift, muss die Hierarchie gepflegt sein — im WebUI unter
|
|
**WatchDog → System-Hierarchie** über das Feld *Übergeordnete Entität*.
|
|
|
|
## 13. Optional: Webhooks
|
|
|
|
Ereignisgesteuerte Benachrichtigung statt Polling. Ziel direkt in der Datenbank
|
|
eintragen:
|
|
|
|
```sql
|
|
INSERT INTO dc_webhooks (name, url, project_slug, events, secret, enabled)
|
|
VALUES ('Telegram Alarm', 'https://n8n.example.com/webhook/dc',
|
|
NULL, 'bug.critical,monitor.down', 'geheimnis', 1);
|
|
```
|
|
|
|
Verfügbare Ereignisse: `bug.created`, `bug.critical`, `bug.resolved`,
|
|
`feature.created`, `monitor.down`, `monitor.recovered`, `release.published`,
|
|
oder `*` für alle.
|
|
|
|
Jede Zustellung trägt eine Signatur:
|
|
|
|
```
|
|
X-DC-Timestamp: 1754563200
|
|
X-DC-Signature: sha256=<hex(hmac_sha256(secret, timestamp + "." + body))>
|
|
```
|
|
|
|
Nach 20 Fehlversuchen in Folge deaktiviert sich ein Webhook selbst.
|
|
|
|
---
|
|
|
|
## 14. Umstellung auf 2.1
|
|
|
|
Diese Fassung korrigiert Fehler im .NET-SDK, ergänzt ein Feld am Heartbeat und
|
|
vervollständigt die OpenAPI-Beschreibung. **Keine Konfigurationsänderung nötig.**
|
|
|
|
### 14.1 Serverseitig
|
|
|
|
- [ ] Migration **008** ausführen (Spalte `app_version` in `watchdog_monitors`).
|
|
Läuft wie die übrigen über den Migrator, siehe [§4](#4-migration-ausführen).
|
|
- [ ] `/api/openapi.json` abrufen und prüfen, dass die Lizenz-Endpunkte und
|
|
`/api/watchdog/v1/event`, `/events`, `/status` jetzt enthalten sind.
|
|
|
|
Bestehende Agenten laufen unverändert weiter; alle neuen Felder sind optional.
|
|
|
|
### 14.2 Clientseitig (`Deploymentcenter.Client`)
|
|
|
|
Die folgenden Änderungen betreffen jede bestehende Integration:
|
|
|
|
| Änderung | Was zu tun ist |
|
|
|---|---|
|
|
| `unknown_error` entfällt | Wer diesen Status abfängt, prüft stattdessen `IsTransient` |
|
|
| Cache-Rückfall bei jedem HTTP-Fehler | Eigene Behelfslösungen für 429/5xx können entfallen |
|
|
| `cache_ttl_hours` wird ausgewertet | Offline-Gnadenfrist prüfen: Vorgabe je Projekt im WebUI |
|
|
| `app_version` ist Parameter | `LicenseClient.DefaultAppVersion` beim Start setzen |
|
|
| `X-Watchdog-Key` entfällt bei `DeactivateAsync` | Nur noch `Authorization: Bearer` |
|
|
| `BuildInfo.targets` erzeugt in `$(RootNamespace)` | Import ist jetzt möglich; ggf. `using` entfernen |
|
|
| API-Rückfall der Update-Prüfung liefert vollständige Daten | Eigene Umgehungen können entfallen |
|
|
|
|
Ausführlich: **[LICENSE_INTEGRATION_GUIDE §6](./LICENSE_INTEGRATION_GUIDE.md)**
|
|
und **[UPDATESERVICE_INTEGRATION_GUIDE §2B/§2C](./UPDATESERVICE_INTEGRATION_GUIDE.md)**.
|
|
|
|
### 14.3 Cache-Schema
|
|
|
|
`state.dat` trägt jetzt Schema 3. Schema 2 wird weiterhin gelesen — ein
|
|
SDK-Wechsel erzwingt **keine** erneute Online-Prüfung. Ein Rückschritt auf ein
|
|
älteres SDK dagegen schon: das kennt Schema 3 nicht und behandelt den Cache als
|
|
Fehltreffer.
|
|
|
|
---
|
|
|
|
## 15. Umstellung auf 2.2
|
|
|
|
Diese Fassung behebt eine Reihe von Fehlern im Update-Weg, die zusammengenommen
|
|
verhindert haben, dass für mehr als eine Plattform ausgeliefert werden konnte —
|
|
und die im Fehlerfall halb aktualisierte Installationen hinterließen.
|
|
|
|
**Diese Umstellung gehört vor das nächste Release**, nicht danach.
|
|
|
|
### 15.1 Was sich ändert
|
|
|
|
| Bereich | Vorher | Jetzt |
|
|
|---|---|---|
|
|
| Plattformen | nicht vorhanden — `win-x64` und `linux-x64` überschrieben sich | eigene Dimension `platform`, getrennte Ablage |
|
|
| Konfigurationsdateien | wurden beim Update überschrieben | `preservePatterns` schützt sie |
|
|
| Ausschlussmuster | nur `*.endung` und exakte Namen | echte Globs (`**`, `*`, `?`) |
|
|
| Abbruch beim Anwenden | halb aktualisierte Installation | Backup, automatischer Rollback |
|
|
| Entfernte Dateien | blieben für immer liegen | werden aufgeräumt |
|
|
| Agent im Paket | überschrieb sich selbst | wird zur Seite gelegt und ersetzt |
|
|
| Übergabe an den Agenten | Wettlauf, kein Neustart | PID-Handschlag und `--restart` |
|
|
| Versionsangabe | ungeprüft | wird gegen die Assembly gehalten |
|
|
| Paketherkunft | nur SHA256 aus derselben Quelle | RSA-Signatur des Servers |
|
|
| `manifest_json` | Spalte blieb immer leer | wird befüllt, API ist vollwertiger Rückfall |
|
|
|
|
### 15.2 Serverseitig
|
|
|
|
- [ ] Migration **009** ausführen (`platform`, `manifest_signature`, neuer
|
|
Unique-Key). Additiv; bestehende Releases erhalten `platform = 'any'` und
|
|
bleiben für alle Clients erreichbar.
|
|
- [ ] Signierschlüssel erzeugen und in `config/config.php` eintragen:
|
|
|
|
```bash
|
|
openssl genrsa -out /etc/dc/release-signing.pem 2048
|
|
chmod 600 /etc/dc/release-signing.pem
|
|
chown www-data:www-data /etc/dc/release-signing.pem
|
|
```
|
|
|
|
```php
|
|
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),
|
|
```
|
|
|
|
- [ ] Prüfen, dass der öffentliche Schlüssel ausgeliefert wird:
|
|
|
|
```bash
|
|
curl https://dc.mhdf.de/api/updateservice/v1/pubkey
|
|
```
|
|
|
|
> **Bewusst RSA und kein HMAC.** Bei einem HMAC braucht der Prüfende denselben
|
|
> geheimen Schlüssel wie der Signierende. Der Agent läuft auf fremden Systemen;
|
|
> ein dort hinterlegter Schlüssel ließe sich auslesen und zum Signieren
|
|
> beliebiger Pakete verwenden — die Signatur wäre wertlos. Beim Lizenzmodul geht
|
|
> HMAC auf, weil dort der Server prüft.
|
|
|
|
Ohne Schlüssel bleibt alles funktionsfähig; Releases sind dann unsigniert, und
|
|
der Agent weist bei jedem Update darauf hin.
|
|
|
|
### 15.3 Packager
|
|
|
|
- [ ] `packager.config.json` um `preservePatterns` ergänzen (Vorlage in
|
|
`packager.config.example.json`).
|
|
- [ ] **Prüfen, welche Konfigurationsdateien im Publish-Verzeichnis liegen.**
|
|
Der Packager warnt jetzt von sich aus vor Dateien, die nach Zugangsdaten
|
|
aussehen und weder ausgeschlossen noch geschützt sind. Diese Warnung ernst
|
|
nehmen: eine `settings.json` mit Datenbankpasswort, die bisher mitgeliefert
|
|
wurde, hat beim Update die Konfiguration jedes Zielsystems überschrieben.
|
|
- [ ] Aufrufe um `--platform` ergänzen:
|
|
|
|
```bash
|
|
pack-and-deploy --project myapp --version 1.4.3 --channel prod \
|
|
--platform win-x64 \
|
|
--publish-dir ./bin/Release/net10.0/win-x64/publish
|
|
```
|
|
|
|
Ohne `--platform` versucht der Packager, die Kennung aus dem Publish-Pfad
|
|
abzuleiten. Gelingt das nicht, gilt das Release als plattformunabhängig und es
|
|
erscheint eine Warnung.
|
|
|
|
- [ ] Die Version wird jetzt gegen die Hauptassembly geprüft. Weicht sie ab,
|
|
bricht der Vorgang ab. Das ist der Fall, in dem `<Version>` nur in einem
|
|
der beteiligten Projekte steht — der Wert gehört in die
|
|
`Directory.Build.props`, damit alle denselben tragen. Notausgang für
|
|
bewusste Abweichungen: `--ignore-version-mismatch`.
|
|
|
|
### 15.4 Clientseitig
|
|
|
|
`UpdateClient.CheckForUpdateAsync` hat einen zusätzlichen Parameter `platform`
|
|
**vor** dem `CancellationToken`. Wer den Token bisher positionell übergeben hat,
|
|
bekommt einen Übersetzungsfehler — kein stilles Fehlverhalten.
|
|
|
|
Der Aufruf des Agenten sollte umgestellt werden:
|
|
|
|
```csharp
|
|
var agentPath = UpdateClient.ResolveAgentPath(); // plattformrichtiger Name
|
|
|
|
if (agentPath != null && checkResult.UpdateAvailable)
|
|
{
|
|
UpdateClient.LaunchUpdateAgent(
|
|
agentPath: agentPath,
|
|
projectId: "myapp",
|
|
channel: "prod",
|
|
currentVersion: BuildInfo.Version, // für Ziele ohne manifest.json
|
|
exitCurrentApp: true); // Neustart und PID-Handschlag
|
|
// sind jetzt Vorgabe
|
|
}
|
|
```
|
|
|
|
| Änderung | Wirkung |
|
|
|---|---|
|
|
| `ResolveAgentPath()` | Findet `update-agent` bzw. `update-agent.exe` — ein fest verdrahteter `.exe`-Name wird unter Linux nie gefunden |
|
|
| `--restart` wird übergeben | Die Anwendung startet nach dem Update wieder. Bisher blieb sie einfach zu |
|
|
| `--wait-for-pid` wird übergeben | Der Agent wartet auf das Ende der Anwendung, statt über gesperrte Dateien zu kopieren |
|
|
| `--platform` wird übergeben | Es kann kein Paket einer fremden Plattform mehr gezogen werden |
|
|
|
|
### 15.5 Den Agenten aus dem Paket nehmen
|
|
|
|
Liegt `update-agent` im Publish-Verzeichnis, wandert er ins Paket und wird beim
|
|
Update mit ausgeliefert. Das ist zulässig — der Agent legt seine laufende Datei
|
|
inzwischen zur Seite und ersetzt sie —, aber es lohnt sich, bewusst zu
|
|
entscheiden:
|
|
|
|
- **Mitliefern:** Der Agent aktualisiert sich selbst mit. Die alte Fassung bleibt
|
|
als `update-agent.exe.dc-old` liegen und wird beim nächsten Start entfernt.
|
|
- **Ausschließen:** `"update-agent*"` in `excludePatterns`. Dann wird der Agent
|
|
getrennt gepflegt.
|
|
|
|
### 15.6 Prüfen
|
|
|
|
- [ ] `curl "https://dc.mhdf.de/api/updateservice/v1/check?product=myapp&version=0.0.0&channel=prod&platform=win-x64"`
|
|
liefert das Windows-Paket, mit `platform=linux-x64` das Linux-Paket.
|
|
- [ ] Ohne `platform` kommen ausschließlich Releases mit `platform=any`. Das ist
|
|
Absicht: ein Client, der seine Kennung nicht nennt, soll lieber kein Update
|
|
bekommen als das falsche.
|
|
- [ ] Im WebUI zeigt die Release-Übersicht eine Spalte **Plattform** und
|
|
kennzeichnet unsignierte Releases.
|
|
- [ ] Ein Update auf einem Testsystem: `appsettings.json` behält danach die
|
|
eingerichteten Werte, und eine Datei, die es im neuen Release nicht mehr
|
|
gibt, ist verschwunden.
|
|
|
|
---
|
|
|
|
## 16. Umstellung auf 2.4
|
|
|
|
Die Release-Ablage liegt jetzt hinter HTTP-Basic-Auth. Zugang hat, wer einen
|
|
gültigen Lizenzschlüssel für das Produkt besitzt — oder ein Installationskonto.
|
|
|
|
Ausführlich: **[UPDATESERVICE_INTEGRATION_GUIDE §5A](./UPDATESERVICE_INTEGRATION_GUIDE.md#5a-zugangsschutz-der-release-verzeichnisse)**
|
|
|
|
### 16.1 Reihenfolge — das ist der kritische Teil
|
|
|
|
> **Erst ausliefern, dann scharfschalten.** Der Zugangsschutz sperrt jede
|
|
> Anwendung aus, die noch mit dem alten SDK gebaut ist: Sie schickt keine
|
|
> Zugangsdaten und bekommt ab dem Moment nur noch 401. Andersherum sperrst du
|
|
> deine eigene Installationsbasis aus.
|
|
|
|
1. [ ] SDK auf 2.4 heben und `licenseKey` an `CheckForUpdateAsync` und
|
|
`LaunchUpdateAgent` übergeben.
|
|
2. [ ] Ein Release mit dem neuen SDK bauen und veröffentlichen.
|
|
3. [ ] Warten, bis die Installationen dieses Release gezogen haben.
|
|
4. [ ] **Erst dann** den Schutz erzeugen — WebUI → *UpdateService →
|
|
🔒 Zugangsschutz → Zugangsschutz jetzt neu erzeugen*.
|
|
|
|
Läuft `cli/tick.php` als Cron, erzeugt es den Schutz beim ersten Lauf nach dem
|
|
Deployment **von selbst**. Wer die Reihenfolge einhalten will, spielt den
|
|
Serverteil also erst dann ein, wenn Schritt 3 erledigt ist.
|
|
|
|
> **Neue Produkte: die Reihenfolge oben gilt für sie nicht.**
|
|
>
|
|
> `ReleaseGuard` überspringt Verzeichnisse, die es nicht gibt — für ein Produkt
|
|
> ohne Release ist `/releases/<slug>/` schlicht nicht vorhanden und damit auch
|
|
> nicht geschützt. Das Verzeichnis entsteht erst mit dem **ersten** Upload, und
|
|
> der nächste `tick.php`-Lauf legt den Schutz darin an.
|
|
>
|
|
> Es gibt also kein Zeitfenster, in dem sich ein noch ungeschütztes Release
|
|
> herunterladen ließe, um dann das SDK nachzuziehen. **Der erste ausgelieferte
|
|
> Build muss die Zugangsdaten bereits mitbringen** — sonst fällt die Tür hinter
|
|
> ihm zu.
|
|
>
|
|
> Für Neuprodukte lautet die Reihenfolge daher schlicht: SDK 2.4 einbinden,
|
|
> `licenseKey` übergeben, *dann* das erste Release veröffentlichen.
|
|
|
|
### 16.2 Was wo eingetragen wird
|
|
|
|
Je Produktverzeichnis eine `.htpasswd` mit den aktiven, nicht abgelaufenen
|
|
Lizenzen dieses Produkts (Benutzername = Passwort = Schlüssel) und allen
|
|
Installationskonten. Bestehende Lizenzen werden dabei automatisch übernommen —
|
|
es ist nichts von Hand nachzutragen.
|
|
|
|
### 16.3 Neue Aufrufe
|
|
|
|
```bash
|
|
update-agent --project myapp --action update \
|
|
--license-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
|
```
|
|
|
|
```bash
|
|
# Erstinstallation: der Installer fragt die Zugangsdaten ab und benutzt sie
|
|
# auch für den Paketabruf. Der Installer-Download selbst bleibt offen.
|
|
wget -qO- https://dc.mhdf.de/installer/install.sh | sh
|
|
./update-agent --action install
|
|
```
|
|
|
|
### 16.4 Prüfen
|
|
|
|
- [ ] **Selbsttest** im WebUI unter *UpdateService → 🔒 Zugangsschutz* — er ruft
|
|
die eigene Paket-Adresse ohne Zugangsdaten ab und erwartet 401. Die
|
|
Anzeige „GESCHÜTZT" allein beweist nichts: Sie prüft nur, ob die Dateien
|
|
existieren, nicht ob der Server sie auswertet.
|
|
- [ ] `curl -I https://dc.mhdf.de/releases/<produkt>/prod/<rid>/<version>/package.tar.gz`
|
|
→ **401**
|
|
- [ ] Mit `-u "$(printf 'lic_%s' "$(printf %s "<schlüssel>" | sha256sum | cut -c1-16)"):<schlüssel>"` → **200**
|
|
(Benutzername ist die Ableitung, nicht der Schlüssel — siehe
|
|
[§5A](./UPDATESERVICE_INTEGRATION_GUIDE.md#5a-zugangsschutz-der-release-verzeichnisse))
|
|
- [ ] `curl -I https://dc.mhdf.de/releases/<produkt>/.htpasswd` → **403**
|
|
- [ ] `https://dc.mhdf.de/installer/update-agent-linux-x64` → **200**, weiterhin offen
|
|
- [ ] Eine Lizenz widerrufen und erneut mit ihr laden → **401**
|
|
|
|
### 16.5 Wenn etwas klemmt
|
|
|
|
Ein 401 im Client heißt **Lizenz**, nicht Netzwerk. `UpdateCheckResult.Unauthorized`
|
|
unterscheidet beides; der Agent gibt `UNAUTHORIZED: …` aus und liefert
|
|
Rückgabewert 2.
|
|
|
|
Zum Abschalten die erzeugten `.htaccess`-Dateien in den Produktverzeichnissen
|
|
löschen. Sie entstehen beim nächsten Auslöser neu.
|
|
|
|
---
|
|
|
|
## 17. Umstellung auf 2.9
|
|
|
|
Zwei Dinge kommen dazu: ein Hardwarekey als zweite Stufe der WebUI-Anmeldung
|
|
und die Roadmap-Tafel. Nur das Erste braucht Aufmerksamkeit — beim Zweiten
|
|
kann nichts schiefgehen, was sich nicht durch Neuladen beheben ließe.
|
|
|
|
### 17.1 Die Reihenfolge, und warum sie zählt
|
|
|
|
```
|
|
1. Deployment
|
|
2. Migration 019 ausführen
|
|
3. Anmelden, Hardwarekey registrieren
|
|
4. Notfalltoken erzeugen und ausdrucken
|
|
```
|
|
|
|
**Die Pflicht zum zweiten Faktor hängt daran, ob ein Schlüssel registriert
|
|
ist — nicht an einem Schalter.** Das ist Absicht und der Grund, warum diese
|
|
Reihenfolge ungefährlich ist: Solange kein Schlüssel hinterlegt ist, läuft die
|
|
Anmeldung genau wie vorher. Ein Schalter, den man vor der Registrierung
|
|
umlegt, hätte die entgegengesetzte Eigenschaft.
|
|
|
|
Nach Schritt 3 ist das Passwort allein nicht mehr genug. Deshalb gehört
|
|
Schritt 4 unmittelbar dahinter und nicht auf die Liste für nächste Woche.
|
|
|
|
### 17.2 Der Schlüssel gilt nur für eine Adresse
|
|
|
|
Ein auf `dc.mhdf.de` registrierter Yubikey funktioniert auf
|
|
`dc-dev.mhdf.de` **nicht**, und umgekehrt. Das ist keine Einschränkung der
|
|
Umsetzung, sondern der Kern des Verfahrens: Genau deshalb läuft ein
|
|
Anmeldeversuch auf einer nachgebauten Seite ins Leere. Auf beiden Instanzen
|
|
also je einmal registrieren.
|
|
|
|
Registriert wird unter **System → 👤 Benutzer → Meine Hardwarekeys**.
|
|
Registrieren Sie zwei, wenn Sie zwei haben — der Notfalltoken ist der
|
|
Ersatzweg, nicht der zweite Schlüssel.
|
|
|
|
### 17.3 Der Notfalltoken
|
|
|
|
Fünf Gruppen zu vier Zeichen, **einmalig** bei der Erzeugung angezeigt. In der
|
|
Datenbank liegt nur ein Hash; wer sie liest, hält damit keinen Zweitschlüssel
|
|
in der Hand. Er gilt für **genau eine** Anmeldung und ist danach verbraucht —
|
|
die Meldezeile sagt es Ihnen.
|
|
|
|
Beim Eintippen zählen nur Buchstaben und Ziffern. Bindestriche und
|
|
Groß-/Kleinschreibung sind gleichgültig.
|
|
|
|
### 17.4 Wenn Schlüssel *und* Notfalltoken weg sind
|
|
|
|
Dann hilft nur die Datenbank. Der Weg zurück ist, die Schlüssel des Kontos zu
|
|
entfernen — danach genügt wieder das Passwort allein:
|
|
|
|
```sql
|
|
DELETE FROM dc_user_credentials
|
|
WHERE user_id = (SELECT id FROM dc_users WHERE username = '<name>');
|
|
```
|
|
|
|
### 17.5 Konto umbenennen
|
|
|
|
**System → 👤 Benutzer** kann jetzt auch den Anmeldenamen ändern. Registrierte
|
|
Hardwarekeys hängen an der Konto-ID und überstehen das; offene Sperrfenster des
|
|
alten Namens werden dabei aufgeräumt.
|
|
|
|
### 17.6 Was sich an der API ändert
|
|
|
|
- Sitzungsauthentifizierte **Schreibzugriffe** auf die API verlangen jetzt ein
|
|
CSRF-Token (`csrf_token` im Rumpf oder der Kopf `X-CSRF-Token`).
|
|
Token-Clients — Agenten, MCP-Adapter, `scripts/dc.py` — sind **nicht**
|
|
betroffen: Sie schicken keinen Cookie. Betroffen sind nur eigene Skripte,
|
|
die sich auf eine WebUI-Sitzung stützen.
|
|
- `roadmap_rank` gilt **projektweit**, nicht je Bereich (#177). `move` sucht
|
|
den Nachbarn entsprechend ohne Bereichsfilter, und `reorder` zählt nicht mehr
|
|
je Bereich bei 10 neu durch, sondern vertauscht nur die Ränge, die die
|
|
genannten Einträge ohnehin halten. Wer bisher je Bereich ab 10 vergeben hat,
|
|
sollte die Reihenfolge einmal durchsehen.
|
|
|
|
### 17.7 Prüfen, ob alles läuft
|
|
|
|
- [ ] Anmeldung mit Passwort führt auf die zweite Stufe
|
|
- [ ] Hardwarekey schließt sie ab
|
|
- [ ] Notfalltoken schließt sie ebenfalls ab und ist danach verbraucht
|
|
- [ ] Roadmap → 🧭 Tafel zeigt Spalten, Spuren und Kärtchen
|
|
- [ ] Ein Klick auf ein Kärtchen öffnet das Panel, Kommentieren funktioniert
|
|
- [ ] `python tests/e2e/run.py` gegen die Testinstanz: alles grün
|
|
|
|
---
|
|
|
|
## 18. Umstellung auf 2.9.2 — nur echte Hardware als zweiter Faktor
|
|
|
|
### 18.1 Worum es geht
|
|
|
|
Seit 2.9.0 verlangt die Anmeldung einen Hardwarekey. Was sie *nicht* verlangte,
|
|
war, dass es wirklich Hardware ist: Ein Passkey aus einem Passwortverwalter
|
|
(1Password, Bitwarden, iCloud, Google) ließ sich ebenso registrieren wie ein
|
|
Yubikey. Für das Deploymentcenter selbst ist das hinnehmbar. Wo ein
|
|
Sicherheitsprotokoll ausdrücklich Hardware fordert, ist es der Unterschied
|
|
zwischen einer Regel und einem Wunsch.
|
|
|
|
Zwei Dinge, die *wie* eine Prüfung aussehen und keine sind — beide waren in
|
|
2.9.0 bereits gesetzt:
|
|
|
|
- `authenticatorAttachment: "cross-platform"` steuert, was der Browser
|
|
**anbietet**. Ein Passwortverwalter meldet sich ebenfalls als
|
|
cross-platform.
|
|
- `response.authenticatorAttachment` setzt der **Client**, und der Client ist
|
|
genau die Instanz, gegen die sich hier nichts beweisen lässt.
|
|
|
|
Was trägt, ist die Attestation: Der Authenticator unterschreibt bei der
|
|
Aufnahme mit einem Schlüssel, der ab Werk in seinem Sicherheitselement liegt
|
|
und es nie verlässt. Das zugehörige Zertifikat muss auf eine hinterlegte
|
|
Herstellerwurzel ketten.
|
|
|
|
**Ein Zertifikat allein genügt nicht.** Jeder kann sich eines ausstellen, es
|
|
„Yubico U2F EE“ nennen, die AAGUID eines echten YubiKey 5 eintragen und sauber
|
|
unterschreiben. Genau dieser Fall wurde nachgebaut und gegen den Server
|
|
gefahren: Er wird abgewiesen — an der **Kette**, nicht an der Signatur. Was
|
|
Software nicht herstellen kann, ist eine Kette, die bei Yubicos Wurzel endet.
|
|
|
|
### 18.2 Migration
|
|
|
|
```bash
|
|
php cli/migrate.php # oder System → 🗄️ DB Migration im WebUI
|
|
```
|
|
|
|
Migration 021 ergänzt `dc_user_credentials` um `attested`, `attestation_fmt`,
|
|
`attestation_ca` und `attestation_root`. Sie setzt **keine** Regel — ohne
|
|
Umstellen bleibt das Verhalten von 2.9.1.
|
|
|
|
### 18.3 Die drei Stufen
|
|
|
|
| Stufe | Aufnahme neuer Schlüssel | Anmeldung mit vorhandenen |
|
|
|---|---|---|
|
|
| `off` | jeder Authenticator | alle |
|
|
| `register` | nur mit Herstellernachweis | alle |
|
|
| `strict` | nur mit Herstellernachweis | nur nachgewiesene |
|
|
|
|
Einzustellen unter **System → 👤 Benutzer → 🛡️ Welche Schlüssel zugelassen
|
|
sind**.
|
|
|
|
### 18.4 Der Weg nach `strict` — in dieser Reihenfolge
|
|
|
|
> **Alle vor 2.9.2 registrierten Schlüssel gelten als nicht nachgewiesen —
|
|
> auch echte Yubikeys.** Das ist keine Vorsicht, sondern eine Tatsache des
|
|
> Standards: Bei `attestation: "none"` ersetzt der **Browser** die
|
|
> Gerätekennung durch Nullbytes, bevor der Server sie zu sehen bekommt. Es gab
|
|
> also nie eine Herkunft, nicht einmal eine ungeprüfte.
|
|
|
|
1. Stufe auf **`register`** stellen.
|
|
2. Den Hardwarekey **einmal neu aufnehmen**. In der Liste muss danach
|
|
*🔒 Hardware* stehen, mit der erreichten Wurzel.
|
|
3. Den alten, nicht nachgewiesenen Eintrag entfernen.
|
|
4. Erst jetzt auf **`strict`**.
|
|
|
|
Schritt 4 wird abgelehnt, solange das eigene Konto keinen nachgewiesenen
|
|
Schlüssel hat — eine Einstellung, die sich selbst aussperrt, ist keine
|
|
Einstellung, sondern eine Falle.
|
|
|
|
### 18.5 Wenn doch etwas klemmt
|
|
|
|
- **Der Browser fragt nach Marke und Modell.** Das ist der sichtbare Preis von
|
|
`attestation: "direct"` und kein Fehler. Wer ablehnt, liefert keine
|
|
Attestation und wird abgewiesen.
|
|
- **Ausgesperrt.** Der Notfalltoken funktioniert unabhängig von der Regel.
|
|
Danach unter System → Benutzer die Stufe zurückstellen.
|
|
- **Ganz ohne WebUI:**
|
|
```sql
|
|
DELETE FROM dc_settings WHERE skey = 'webauthn_hardware_policy';
|
|
```
|
|
Fehlt der Eintrag, gilt wieder `off`.
|
|
|
|
### 18.6 Andere Hersteller als Yubico
|
|
|
|
Mitgeliefert sind die drei FIDO-Wurzeln von Yubico
|
|
(`src/Core/Webauthn/roots/yubico.pem`). Weitere Wurzeln in der `config.php`:
|
|
|
|
```php
|
|
'security' => [
|
|
'webauthn_root_files' => ['/etc/dc/solokeys.pem'],
|
|
],
|
|
```
|
|
|
|
Bewusst dort und nicht im WebUI: Eine Vertrauenswurzel über dieselbe
|
|
Oberfläche austauschbar zu machen, die sie schützt, hieße die Prüfung dort
|
|
aushebelbar zu machen.
|
|
|
|
### 18.7 Prüfen, ob alles läuft
|
|
|
|
- [ ] Migration 021 ist durch (Spalte `attested` in `dc_user_credentials`)
|
|
- [ ] Stufe `register`: Der Yubikey wird aufgenommen und zeigt *🔒 Hardware*
|
|
- [ ] Stufe `register`: Ein Passwortverwalter wird mit einer Meldung abgewiesen,
|
|
die den Grund nennt
|
|
- [ ] `strict` lässt sich **nicht** einschalten, solange kein nachgewiesener
|
|
Schlüssel am eigenen Konto hängt
|
|
- [ ] Nach `strict`: Anmeldung mit dem Yubikey funktioniert weiter
|
|
- [ ] `python tests/e2e/run.py --gruppe hardware`: alles grün
|
|
|
|
---
|
|
|
|
## 19. Umstellung auf 2.9.3 — Ergebnis der Sicherheitsbegutachtung
|
|
|
|
### 19.1 Was gefunden wurde
|
|
|
|
Die WebAuthn-Zeremonie selbst war in Ordnung. Der zweite Faktor ließ sich
|
|
trotzdem umgehen — **nicht in der Zeremonie, sondern daneben**:
|
|
|
|
| Weg | Was möglich war |
|
|
|---|---|
|
|
| `/api/setup/v1/login` | Passwort allein genügte für ein `setup:install`-Token — der Endpunkt prüfte `totp_secret`, eine Spalte, die kein Anmeldeweg auswertet |
|
|
| Datenbankfehler | `secondFactorRequired()` fing jedes `Throwable` und antwortete „kein zweiter Faktor" — ein Lock-Timeout schaltete die Anlage still auf Passwort-allein |
|
|
| Übernommene Sitzung | Konnte einen eigenen Hardwarekey registrieren, Notfalltokens ausstellen und die Regel abschalten — ohne eine einzige erneute Bestätigung |
|
|
|
|
Der rote Faden: Ein zweiter Faktor ist nur so stark wie der **schwächste Weg
|
|
zum Konto**. Zwei der drei Befunde waren Stellen, an denen ein Kommentar eine
|
|
Zusicherung behauptete, die der Code nicht einlöste.
|
|
|
|
### 19.2 Vor dem Deployment prüfen
|
|
|
|
> **`app.url` muss in der `config.php` gesetzt sein.**
|
|
|
|
`rpId()` und `expectedOrigin()` leiten sich nicht mehr aus dem Host-Header ab —
|
|
aus diesem Wert entstehen die beiden Anker, gegen die die ganze Zeremonie
|
|
geprüft wird. Fehlt `app.url` **und** `security.webauthn_rp_id`, wird der
|
|
Hardwarekey abgewiesen.
|
|
|
|
```bash
|
|
curl -s https://dc.mhdf.de/api/health | grep -o '"webauthn_anchor":{[^}]*}'
|
|
```
|
|
|
|
`"ok":true` — alles in Ordnung. `"ok":false` — vor dem Deployment eintragen:
|
|
|
|
```php
|
|
'app' => ['url' => 'https://dc.mhdf.de'],
|
|
```
|
|
|
|
### 19.3 Migration
|
|
|
|
```bash
|
|
php cli/migrate.php # oder System → 🗄️ DB Migration
|
|
```
|
|
|
|
Migration 022 ergänzt `dc_users.session_epoch`. Bestehende Anmeldungen bleiben
|
|
gültig — eine Sicherheitsverbesserung, die im Moment ihrer Einführung alle
|
|
Angemeldeten hinauswirft, wird beim nächsten Mal abgeschaltet.
|
|
|
|
### 19.4 Was sich im Arbeitsablauf ändert
|
|
|
|
- **Nach 30 Minuten Untätigkeit wird die Sitzung gesperrt**, nicht beendet.
|
|
Der Sperrbildschirm öffnet mit dem **Passwort allein** — der Hardwarekey
|
|
wird erst bei einer neuen Anmeldung wieder verlangt.
|
|
- **Fünf Aktionen verlangen eine Bestätigung**, wenn die letzte
|
|
Authentifizierung über zehn Minuten zurückliegt: Passwort setzen,
|
|
Notfalltoken ausstellen, Hardwarekey aufnehmen oder entfernen, die
|
|
Hardwarekey-Regel umstellen. Es erscheint derselbe Sperrbildschirm.
|
|
- **Ein Passwortwechsel beendet alle anderen Sitzungen** des Kontos. Die
|
|
eigene bleibt bestehen.
|
|
- **Die Anmeldung nennt keine Gründe mehr.** Falscher Name, falsches Passwort,
|
|
deaktiviertes und gesperrtes Konto ergeben dieselbe Meldung. Die Wartezeit
|
|
steht darin, der Grund nicht — gesperrt wird nur, was existiert, und eine
|
|
eigene Meldung dafür bestätigte die Existenz des Namens.
|
|
|
|
### 19.5 Was Sie selbst erledigen müssen
|
|
|
|
- [ ] **Rocket.Chat-Passwort rotieren.** In `config/config.example.php` stand
|
|
ein echt aussehendes Passwort — die Beispieldatei liegt im Repository,
|
|
und damit lag es dort auch. Ersetzt durch `''`; der Wert kommt jetzt aus
|
|
`DC_RC_PASS`.
|
|
- [ ] **`verify_ssl` prüfen.** Die Vorgabe der Beispieldatei stand auf
|
|
`false` und steht jetzt auf `true`. Wer den alten Wert übernommen hat,
|
|
prüfte Zertifikate nicht.
|
|
- [ ] **Installationskonto anlegen**, falls Sie die Setup-API mit einem
|
|
Administratorkonto benutzt haben. Rolle `installer`, unter System →
|
|
Benutzerkonten.
|
|
- [ ] **Notfalltoken erneuern**, wenn er älter als 180 Tage ist. Er läuft
|
|
jetzt ab; die Oberfläche zeigt die Restlaufzeit.
|
|
|
|
### 19.6 Was bewusst offen bleibt
|
|
|
|
| Punkt | Warum nicht jetzt |
|
|
|---|---|
|
|
| CSP ohne `'unsafe-inline'` | Verlangt das Herauslösen von 93 Inline-Handlern und mehreren tausend Zeilen Inline-Skript aus `index.php`. Ein eigenes Vorhaben, kein Header. **Bis dahin schützt die CSP nicht gegen XSS** — sie schützt gegen fremde Skriptquellen, Einbettung und umgebogene Formularziele. |
|
|
| Rollenmodell im WebUI | `ApiAuth` gibt einer Session jeden Scope ohne Prüfung; jeder WebUI-Benutzer ist Volladministrator. Ein echtes Berechtigungsmodell ist ein Vorhaben für sich. |
|
|
| Document Root | Der gesamte Quellbaum inklusive `config/` hängt am Web und wird von `mod_rewrite` plus Verzeichnis-`.htaccess` gehalten. Richtig wäre `public/` als Root — das ist eine Änderung an der Hosting-Konfiguration, nicht am Code. |
|
|
|
|
### 19.7 Prüfen, ob alles läuft
|
|
|
|
- [ ] `/api/health` → `checks.webauthn_anchor.ok` ist `true`
|
|
- [ ] Anmeldung mit Passwort und Hardwarekey funktioniert
|
|
- [ ] `curl -I https://dc.mhdf.de/login.php` zeigt
|
|
`Strict-Transport-Security` und `Content-Security-Policy`
|
|
- [ ] `curl -I http://dc.mhdf.de/login.php` antwortet mit `301` auf `https://`
|
|
- [ ] Setup-API mit einem Administratorkonto → `403 installer_account_required`
|
|
- [ ] `python tests/e2e/run.py` gegen die Testinstanz: alles grün
|