feat(updateservice): Plattform-Dimension, signierte Releases, Update mit Rollback
Behebt eine Reihe zusammenhaengender Fehler im Update-Weg, die zusammen verhindert haben, fuer mehr als eine Plattform auszuliefern - und die im Fehlerfall halb aktualisierte Installationen hinterliessen. Server - Migration 009: Spalte platform samt neuem Unique-Key. Zuvor verdraengte das zuletzt veroeffentlichte Paket alle anderen Plattformen derselben Version, weil ON DUPLICATE KEY auf (slug, version, channel) griff. Ein Linux-System zog sich damit das Windows-Paket. - Aufloesungsregel: je Version das plattformgenaue Paket, sonst das plattformunabhaengige. Ein Client ohne Plattformangabe sieht ausschliesslich 'any' - lieber kein Update als das falsche. - manifest_json wird endlich befuellt; die Spalte blieb bisher immer leer, wodurch die API nie der Rueckfall sein konnte, als der sie gedacht war. - Releases werden serverseitig mit RSA-SHA256 signiert, neuer Endpunkt /api/updateservice/v1/pubkey. Bewusst kein HMAC: der Pruefende laeuft auf fremden Systemen und darf den Signierschluessel nicht besitzen. Packager - Bricht ab, statt die Versionshistorie zu verlieren. Schlug das Lesen der bestehenden latest.json fehl, ersetzte ein leeres catch die komplette Historie durch einen einzigen Eintrag - ohne jede Meldung. - Echte Glob-Muster. Zuvor trafen "logs/**" und "scratch/**" aus der mitgelieferten Beispielkonfiguration nie zu. - preservePatterns: Konfigurationsvorlagen werden ausgeliefert, ersetzen am Ziel aber keine vorhandene Datei. Eine settings.json mit Zugangsdaten ueberschrieb bisher beim Update die Konfiguration jedes Zielsystems. - Warnt vor Dateien, die nach Zugangsdaten aussehen und auf keiner Liste stehen. - Prueft --version gegen die Hauptassembly. Eine Abweichung fuehrte zu einer Endlosschleife: Clients aktualisieren, melden weiter die alte Version, halten das Release erneut fuer neu. - --platform mit Ableitung aus dem Publish-Pfad. Agent - Anwenden mit Plan, Backup und vollstaendigem Rollback. Die Stelle war als "Atomic Replace with Backup" kommentiert und war eine Kopierschleife. - Verwaiste Dateien werden entfernt, aber nur solche aus dem Manifest der Vorversion. Was nicht aus einem Release stammt, bleibt liegen. - Das laufende Agent-Binary wird zur Seite gelegt statt ueberschrieben. - API-Rueckfall in FetchManifestAsync; bisher nur im SDK vorhanden, weshalb die Anwendung "Update verfuegbar" und der Agent "kein Release" sagen konnte. - Installierte Version aus --current-version oder manifest.json statt des Textes "Unbekannt", der als 0 gelesen wurde und jede Version neuer erscheinen liess. Reparatur funktioniert damit auch ohne manifest.json. - Setzt das Ausfuehrungsbit fuer Linux-Pakete, die unter Windows gebaut wurden. SDK - ResolveAgentPath() liefert den plattformrichtigen Namen; ein fest verdrahtetes "update-agent.exe" wird unter Linux nie gefunden. - LaunchUpdateAgent uebergibt jetzt --restart (wurde nie uebergeben, die Anwendung blieb nach dem Update zu), --wait-for-pid (kein Wettlauf mehr mit dem Herunterfahren) und --platform. Enthaelt ausserdem die bislang nicht committete Arbeit an Watchdog, Lizenz- Client und cli/tick.php samt Migration 008; die betroffenen Dateien liessen sich nicht getrennt stagen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
5f9b0c5596
commit
2388b5abe1
+217
@@ -3,6 +3,16 @@
|
||||
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)**.
|
||||
|
||||
---
|
||||
|
||||
## 1. Vor dem Deployment: Zugangsdaten wechseln
|
||||
@@ -102,10 +112,38 @@ Die Migration ist additiv und legt an bzw. korrigiert:
|
||||
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.
|
||||
|
||||
@@ -279,3 +317,182 @@ 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/net8.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.
|
||||
|
||||
Reference in New Issue
Block a user