Zwei angeforderte Funktionen, dazu zwei Befunde, die ihnen im Weg lagen. == Zweite Stufe der Anmeldung (#666) == Nach korrektem Passwort verlangt das WebUI einen registrierten FIDO2-Schluessel. Die Pflicht haengt je Konto daran, OB ein Schluessel registriert ist - nicht an einem Schalter. Ein Schalter, den man vor der Registrierung umlegt, sperrt den Betreiber aus seiner eigenen Verwaltung aus. Ohne Fremdbibliothek gebaut (das Projekt hat bewusst kein Composer): src/Core/Webauthn/ mit CBOR-Leser, COSE-nach-DER-Wandlung und Pruefung ueber openssl_verify (ES256, RS256). Auth::login() heisst jetzt Auth::startLogin() und liefert drei Zustaende statt eines bool - aus "wahr heisst drin" ist "wahr heisst vielleicht drin" geworden, und ein uebersehener Aufrufer soll an einem fehlenden Methodennamen scheitern statt an einer truthy Zeichenkette. Die Setup-API der Zielsysteme geht ueber verifyCredentials() daran vorbei und bleibt unberuehrt. Notfalltoken als Ersatzweg: nur als Hash in der Datenbank, Klartext einmalig angezeigt, Einmalgebrauch. Laeuft als gewoehnliches Formular ohne JavaScript - ein Ersatzweg mit denselben Voraussetzungen wie der Hauptweg waere keiner. Fehlversuche auf der zweiten Stufe laufen in dieselbe Konto- und IP-Sperre wie das Passwort; zuvor zaehlte nur die erste Stufe. Benutzerverwaltung kann Konten umbenennen. Beim Loeschen eines Kontos und beim Zuruecksetzen der Testinstanz werden seine Schluessel mitgenommen: credential_id ist geraeteweit eindeutig, eine verwaiste Zeile haette denselben Yubikey dauerhaft unbrauchbar gemacht. == Roadmap-Tafel (#664) == Neuer Unterreiter Roadmap -> Tafel. Der Server liefert die fertige Gliederung (RoadmapBoard, ?action=board), die Seite rendert nur - sonst schneidet das WebUI anders als die API und ein spaeterer Export ein drittes Mal anders. Phasen stehen nicht im Code: gepflegt in der neuen Tabelle roadmap_phases, sonst an den groessten Luecken der Rangfolge geschnitten und sichtbar als geraten gekennzeichnet. Links die gekappte Spalte des Erledigten, rechts "Wird ueberlegt". Nachpruefung ist ein eigener Zustand und wird nicht als erledigt gezeichnet. Klick oeffnet ein Seitenpanel: Volltext, Verlauf, Kommentieren mit action_taken, Aendern von Bereich, Rang, Zielfassung, Status und Ausloeser - ohne Seitenwechsel. Das war der eigentliche Gewinn: Wer etwas bemerkte, musste sich bisher die Nummer merken und im Bugtracker danach suchen. Tiefe Verweise #roadmap/<projekt>/item/<id>. Ziehen zwischen Phasen ueber ?action=rank_between. Fremdtext wird ausschliesslich ueber textContent gesetzt, nie ueber innerHTML. == Befund: roadmap_rank war projektweit und bereichslokal zugleich (#177) == Entschieden: projektweit. move() sucht den Nachbarn ohne Bereichsfilter, reorder() schreibt keinen ganzen Bereich mehr auf 10/20/30 um, sondern vertauscht nur die Raenge, die die Eintraege ohnehin halten. rank_between rechnet die Mitte zwischen zwei Nachbarn und nummeriert nur dann einmal neu - unter Erhalt der Reihenfolge -, wenn dort kein Platz mehr ist; die Antwort sagt es dann. == Befund: CSRF an sitzungsauthentifizierten API-Schreibzugriffen == ApiAuth akzeptiert die WebUI-Sitzung, die Endpunkte prueften dabei kein CSRF-Token. Der Kommentar in Http::beginJson - "ausschliesslich per Token-Header, niemals per Cookie" - traf auf die API nie zu. Schreibende Anfragen im Sitzungsweg verlangen jetzt csrf_token bzw. X-CSRF-Token. Token-Clients sind nicht betroffen und wurden gegengeprueft. == Geprueft == tests/e2e/suite_tafel.py neu (16 Faelle), Suite gesamt 172 von 172 gegen die Testinstanz. Die WebAuthn-Zeremonien zusaetzlich gegen einen Software- Authenticator mit echtem P-256 und echtem CBOR: Registrieren, Anmelden, verfaelschte Signatur, veraltete Herausforderung und Zaehler-Rueckfall als Klonverdacht werden je mit 403 abgewiesen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
677 lines
28 KiB
Markdown
677 lines
28 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
|