Der Schalter gab es bisher nur auf der Kommandozeile. LaunchUpdateAgent, der vom Leitfaden empfohlene Weg, hatte dafuer keinen Parameter - jede Anwendung, die diesem Weg folgte, aktualisierte damit ungeprueft, waehrend derselbe Vorgang von Hand geschuetzt gewesen waere. Neuer optionaler Parameter requireSignature (Vorgabe false, keine Verhaltensaenderung fuer bestehende Aufrufer). Der oeffentliche Schluessel muss dafuer nicht separat verwaltet werden, der Agent holt und cached ihn selbst. SDK auf 2.5.2, Changelog- und Leitfaden-Eintrag ergaenzt.
1025 lines
42 KiB
Markdown
1025 lines
42 KiB
Markdown
# Deploymentcenter — UpdateService Integration & Deployment Guide
|
|
|
|
> **⚠️ Geändert in Version 2.0** — Das Veröffentlichen eines Releases läuft jetzt
|
|
> über `POST /api/updateservice/v1/publish` und verlangt ein Token mit dem Scope
|
|
> `updateservice:publish` (zuvor völlig ungeschützt). Der Versionsvergleich folgt
|
|
> jetzt der semantischen Versionsordnung, `1.10.0` gilt also korrekt als neuer
|
|
> als `1.9.0`. Umstellungsschritte: **[UPGRADE.md](./UPGRADE.md)**.
|
|
|
|
> **⚠️ Geändert in Version 2.1**
|
|
> - `Deploymentcenter.BuildInfo.targets` erzeugt die Klasse jetzt im Namensraum
|
|
> des einbindenden Projekts. Die vorherige Fassung war nicht einbindbar
|
|
> (CS0433), siehe **[§2B](#b-msbuild-buildinfo-generierung)**.
|
|
> - Der API-Rückfall in `CheckForUpdateAsync` liest die Antwort jetzt korrekt.
|
|
> Fehlte die `latest.json`, kamen zuvor weder Download-Adresse noch Prüfsumme,
|
|
> Changelog oder Kritikalität an, siehe **[§2C](#c-zwei-quellen-zwei-formate)**.
|
|
|
|
> **⚠️ Geändert in Version 2.2 — bitte vollständig lesen, bevor das nächste
|
|
> Release gebaut wird.** Umstellungsschritte: **[UPGRADE.md §15](./UPGRADE.md#15-umstellung-auf-22)**.
|
|
> - **Releases tragen eine Plattform.** Ohne sie überschrieben sich `win-x64`
|
|
> und `linux-x64` gegenseitig, siehe **[§1A](#1a-plattformen)**.
|
|
> - **Konfigurationsdateien überleben ein Update.** Bisher überschrieb jedes
|
|
> Update die eingerichteten Werte des Zielsystems, siehe **[§3A](#3a-ausschließen-oder-schützen)**.
|
|
> - **Anwenden mit Rollback.** Ein Abbruch hinterlässt keine halbe Installation
|
|
> mehr, und entfernte Dateien werden aufgeräumt, siehe **[§4B](#4b-wie-ein-update-angewendet-wird)**.
|
|
> - **Releases sind signiert**, siehe **[§6](#6-signatur-der-releases)**.
|
|
> - `LaunchUpdateAgent` übergibt jetzt Neustart, Prozesskennung und Plattform,
|
|
> siehe **[§2A](#a-referenz-auf-deploymentcenterclient)**.
|
|
|
|
> **⚠️ Geändert in Version 2.5.2**
|
|
> - **`LaunchUpdateAgent` reicht `--require-signature` durch.** Zuvor gab es
|
|
> den Schalter nur auf der Kommandozeile — der empfohlene Weg über das SDK
|
|
> aktualisierte damit ungeprüft, siehe
|
|
> **[§6, "Erzwingen über LaunchUpdateAgent"](#erzwingen-über-launchupdateagent)**.
|
|
|
|
|
|
Das **UpdateService-Modul** des Deploymentcenters bietet ein unternehmensweites, leichtgewichtiges Update-, Rollback- und Reparatur-Schema: statisch ausgelieferte Pakete plus eine PHP-API.
|
|
|
|
> **⚠️ Der Webserver ist nicht beliebig.** Ältere Fassungen dieser Anleitung
|
|
> beschrieben den Stack durchgehend als „LEMP (Nginx + PHP)". Der
|
|
> Zugangsschutz aus **[§5A](#5a-zugangsschutz-der-release-verzeichnisse)**
|
|
> beruht auf `.htaccess` und wird **von Nginx vollständig ignoriert** — dort
|
|
> wären die Release-Verzeichnisse offen und die `.htpasswd` sogar öffentlich
|
|
> abrufbar, während die Oberfläche „geschützt" meldete.
|
|
>
|
|
> `dc.mhdf.de` läuft auf **Apache** mit aktivem `AllowOverride`, dort trägt es.
|
|
> Wer auf Nginx ausrollt, muss den Schutz in der Serverkonfiguration
|
|
> nachbilden — die Vorlage steht in [§5A](#nginx-statt-apache). Verlass dich
|
|
> nicht auf die Anzeige, sondern auf den **Selbsttest**: er ruft die eigene
|
|
> Paket-Adresse ohne Zugangsdaten ab und erwartet 401.
|
|
|
|
---
|
|
|
|
## 1. Übersicht & Architektur
|
|
|
|
- **Kein dauerhafter Background-Dienst**: Hauptanwendungen prüfen beim Start einmalig schnell und netzwerktolerant auf verfügbare Updates und Dateiintegrität.
|
|
- **Entkoppelte Ausführung**: Bei Handlungsbedarf beendet sich die Hauptanwendung sauber und übergibt die Kontrolle an den eigenständigen Console Agent (`update-agent.exe` / `update-agent`).
|
|
- **3-Kanal-System**: Kanäle `prod` (Produktiv), `beta` (Vorab-Test), `dev` (Entwicklung).
|
|
- **Plattform-Dimension**: je Kanal getrennte Pakete für `win-x64`, `linux-x64` usw.
|
|
- **Statische Verteilung**: Downloads und Versionen-Manifeste (`latest.json`, `manifest.json`, `package.tar.gz`) liefert der Webserver direkt aus, ohne PHP im Weg.
|
|
- **Zugangsschutz über den Lizenzschlüssel** — setzt Apache voraus, siehe Kasten oben.
|
|
|
|
---
|
|
|
|
## 1A. Plattformen
|
|
|
|
Ein Release wird durch **vier** Angaben bestimmt: Projekt, Kanal, Version und
|
|
Plattform. Die Plattform ist eine .NET-Laufzeitkennung (`win-x64`,
|
|
`linux-x64`, `linux-musl-arm64`, `osx-arm64`, …) oder `any` für Pakete, die
|
|
überall laufen.
|
|
|
|
Zuvor gab es diese Dimension nicht. Wer für zwei Plattformen baute, veröffentlichte
|
|
beide Pakete unter derselben Version im selben Kanal — das zweite überschrieb
|
|
das erste, und ein Linux-System zog sich das Windows-Paket. Behelfe waren
|
|
getrennte Projekt-Slugs (`myapp-win`, `myapp-linux`) oder zweckentfremdete
|
|
Kanäle; beides trug nicht weit.
|
|
|
|
### Auswahlregel
|
|
|
|
| Client schickt | bekommt |
|
|
|---|---|
|
|
| `platform=win-x64` | Pakete mit `win-x64`, ersatzweise `any` |
|
|
| `platform=linux-x64` | Pakete mit `linux-x64`, ersatzweise `any` |
|
|
| nichts | **ausschließlich** `any` |
|
|
|
|
Der letzte Fall ist Absicht. Ein Client, der seine Kennung nicht nennt, soll
|
|
lieber kein Update bekommen als das Paket einer fremden Plattform. Alle vor
|
|
Version 2.2 veröffentlichten Releases stehen auf `any` und bleiben damit für
|
|
bereits ausgelieferte Anwendungen erreichbar.
|
|
|
|
Je Version gewinnt das plattformgenaue Paket; gibt es keines, wird das
|
|
plattformunabhängige genommen.
|
|
|
|
### Ablage
|
|
|
|
Plattformunabhängige Releases behalten den bisherigen Pfad, alle anderen
|
|
bekommen eine Zwischenebene:
|
|
|
|
```text
|
|
/releases/myapp/prod/1.4.3/package.tar.gz ← platform = any
|
|
/releases/myapp/prod/win-x64/1.4.3/package.tar.gz ← platform = win-x64
|
|
/releases/myapp/prod/linux-x64/1.4.3/package.tar.gz ← platform = linux-x64
|
|
```
|
|
|
|
Ohne diese Rücksicht wären alle bestehenden Installationen von einem Tag auf
|
|
den anderen von ihren Updates abgeschnitten gewesen.
|
|
|
|
---
|
|
|
|
## 2. Integration in .NET Client-Anwendungen
|
|
|
|
### A. Referenz auf `Deploymentcenter.Client`
|
|
|
|
Binde das Projekt oder Paket `Deploymentcenter.Client` in deine Anwendung ein.
|
|
|
|
```csharp
|
|
using Deploymentcenter.Client;
|
|
using Deploymentcenter.Client.Models;
|
|
|
|
// Early Start Hook in Program.cs (oder App.xaml.cs)
|
|
var updateClient = new UpdateClient();
|
|
var checkResult = await updateClient.CheckForUpdateAsync(
|
|
baseUrl: "https://dc.mhdf.de",
|
|
projectId: "myapp",
|
|
currentVersion: BuildInfo.Version,
|
|
channel: "prod"
|
|
// platform: entfällt - ohne Angabe die Kennung des laufenden Systems
|
|
);
|
|
|
|
if (checkResult.UpdateAvailable)
|
|
{
|
|
Console.WriteLine($"[UPDATE] Neues Release v{checkResult.LatestRelease.Version} verfügbar!");
|
|
|
|
var agentPath = UpdateClient.ResolveAgentPath();
|
|
if (agentPath == null)
|
|
{
|
|
Console.WriteLine("[UPDATE] Kein update-agent gefunden.");
|
|
return;
|
|
}
|
|
|
|
UpdateClient.LaunchUpdateAgent(
|
|
agentPath: agentPath,
|
|
projectId: "myapp",
|
|
channel: "prod",
|
|
action: "update",
|
|
version: "latest",
|
|
currentVersion: BuildInfo.Version,
|
|
exitCurrentApp: true
|
|
);
|
|
}
|
|
```
|
|
|
|
**Nicht mehr `"update-agent.exe"` fest verdrahten.** `ResolveAgentPath()`
|
|
liefert den plattformrichtigen Namen — unter Linux und macOS trägt das Binary
|
|
keine Endung, ein fester `.exe`-Name wird dort nie gefunden, und die Anwendung
|
|
meldet stumm „kein Agent vorhanden".
|
|
|
|
Drei Dinge erledigt `LaunchUpdateAgent` seit 2.2 von selbst:
|
|
|
|
| Was | Warum |
|
|
|---|---|
|
|
| `--restart` mit dem Pfad der eigenen Anwendung | Zuvor wurde der Parameter nie übergeben. Der Agent unterstützte ihn, bekam ihn aber nie zu sehen — die Anwendung schloss sich und blieb zu |
|
|
| `--wait-for-pid` mit der eigenen Prozesskennung | Zuvor wurde der Agent gestartet und sofort `Environment.Exit(0)` gerufen. Bei langsamem Herunterfahren (Kestrel, EF, Log-Flush) kopierte er über noch gesperrte Dateien |
|
|
| `--platform` mit der Kennung des Systems | Verhindert, dass ein Paket der falschen Plattform gezogen wird |
|
|
|
|
Abschaltbar über `restartPath: ""` bzw. `waitForCurrentProcess: false`, wenn
|
|
ein Dienst-Manager den Neustart übernimmt.
|
|
|
|
`currentVersion` ist wichtig für Installationen ohne `manifest.json` — siehe
|
|
**[§4A](#4a-welche-version-ist-installiert)**.
|
|
|
|
### B. MSBuild BuildInfo Generierung
|
|
|
|
Version, UTC-Build-Datum und Git-Commit entstehen automatisch zur
|
|
Übersetzungszeit. **Seit Version 2.5 genügt dafür der Paketverweis:**
|
|
|
|
```xml
|
|
<PropertyGroup>
|
|
<Version>1.4.3</Version>
|
|
</PropertyGroup>
|
|
|
|
<ItemGroup>
|
|
<PackageReference Include="Deploymentcenter.Client" Version="2.5.1" />
|
|
</ItemGroup>
|
|
```
|
|
|
|
Das Targets-File liegt im Paket unter `build/` und wird von NuGet selbst
|
|
importiert — ein `<Import>` von Hand entfällt.
|
|
|
|
> **Der bisherige Weg koppelte zwei Repositories.** Die Anleitung empfahl
|
|
> `<Import Project="..\Deploymentcenter.Client\Deploymentcenter.BuildInfo.targets" />`
|
|
> — das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in
|
|
> derselben Fassung stehen. Wer nur das Paket bezieht, hatte das Target nicht.
|
|
> Der Import per Pfad funktioniert weiterhin, ist aber nicht mehr nötig.
|
|
|
|
Die Klasse entsteht im Namensraum deines Projekts (`$(RootNamespace)`), nicht im
|
|
SDK. Du erreichst sie also ohne `using`:
|
|
|
|
```csharp
|
|
Console.WriteLine(BuildInfo.Version); // "1.4.3" aus <Version>
|
|
Console.WriteLine(BuildInfo.Summary); // v1.4.3 (5f9b0c5) built on ... [prod]
|
|
```
|
|
|
|
Verfügbare Werte: `Version`, `GitCommit`, `GitCommitShort`, `BuildDateUtc`,
|
|
`Channel`, `Summary`. Ohne Git-Arbeitskopie stehen die Commit-Felder auf
|
|
`UNKNOWN`, der Build läuft trotzdem durch.
|
|
|
|
Überschreibbare MSBuild-Eigenschaften:
|
|
|
|
| Eigenschaft | Wirkung |
|
|
|---|---|
|
|
| `DeploymentcenterBuildInfoNamespace` | Zielnamensraum (Vorgabe: `$(RootNamespace)`) |
|
|
| `DeploymentcenterBuildInfoClass` | Klassenname (Vorgabe: `BuildInfo`) |
|
|
| `BuildChannel` | `prod`, `beta`, `dev` (Vorgabe: `prod`) |
|
|
| `GenerateDeploymentcenterBuildInfo` | auf `false` setzen, um die Erzeugung abzuschalten |
|
|
|
|
> **Nicht auf `Deploymentcenter.Client.Models` zeigen lassen.** Dort liefert das
|
|
> SDK bereits eine gleichnamige Klasse aus; `partial` verbindet Teilklassen nur
|
|
> innerhalb derselben Assembly. Genau daran scheiterte die vorherige Fassung des
|
|
> Targets: sie erzeugte fest in diesen Namensraum, wodurch im Consumer ein
|
|
> zweiter Typ mit demselben vollen Namen entstand (CS0433) und der generierte
|
|
> statische Konstruktor Eigenschaften setzte, die es dort nicht gab (CS0103).
|
|
> Ein Import war damit unmöglich.
|
|
|
|
### C. Zwei Quellen, zwei Formate
|
|
|
|
`CheckForUpdateAsync` fragt zuerst die statische
|
|
`/releases/{projectId}/{channel}/latest.json` ab und fällt auf
|
|
`/api/updateservice/v1/check` zurück. **Die beiden liefern unterschiedliche
|
|
Feldnamen:**
|
|
|
|
| Bedeutung | `latest.json` (Packager) | API-Antwort (Datenbank) |
|
|
|---|---|---|
|
|
| Download-Adresse | `packageUrl` | `download_url` |
|
|
| Prüfsumme | `sha256` | `sha256_hash` |
|
|
| Änderungshinweise | `changelog` | `release_notes` |
|
|
| Größe | `sizeBytes` | `size_bytes` |
|
|
| Kritisch | `isCritical` | `is_critical` **auf oberster Ebene** |
|
|
|
|
Das SDK bildet beide ab: `VersionInfo` für die `latest.json`, `ApiReleaseInfo`
|
|
für die API-Antwort. `UpdateCheckResult.LatestRelease` ist in beiden Fällen ein
|
|
`VersionInfo` — für die API wird übersetzt.
|
|
|
|
> Wer die API selbst anspricht, muss das berücksichtigen. Die vorherige SDK-Fassung
|
|
> deserialisierte die API-Antwort direkt nach `VersionInfo`; von beiden Formaten
|
|
> stimmt nur `version` überein. Über den API-Weg kam deshalb nichts weiter an —
|
|
> und `IsCritical` wurde aus dem Release-Objekt statt vom Wurzelfeld gelesen und
|
|
> war damit immer `false`. Da dieser Zweig genau der Rückfall ist, wenn die
|
|
> `latest.json` fehlt, degradierte die Update-Prüfung still.
|
|
|
|
---
|
|
|
|
## 3. Packaging & Deployment CLI (`pack-and-deploy`)
|
|
|
|
> **Woher das Werkzeug kommt.** Frühere Fassungen dieser Anleitung benutzten
|
|
> `pack-and-deploy`, als läge es im PATH — beziehbar war es nirgends. Es steht
|
|
> jetzt unter `/installer/` bereit:
|
|
>
|
|
> ```bash
|
|
> wget https://dc.mhdf.de/installer/pack-and-deploy-linux-x64 -O pack-and-deploy
|
|
> chmod +x pack-and-deploy
|
|
> ```
|
|
>
|
|
> Wer nicht von Hand aufrufen will, nimmt die **Release-Vorlage**: ein Skript
|
|
> zum Kopieren ins eigene Projekt, das je Plattform `dotnet publish` und
|
|
> `pack-and-deploy` verkettet und sich das Werkzeug selbst holt. Siehe
|
|
> **[Release-Anleitung für Agenten](../public/docs/release.md)**.
|
|
|
|
Das Packaging-Tool verpackt den `dotnet publish`-Output, berechnet Hashes, erzeugt das `manifest.json` und lädt alles per FTP auf den LEMP-Server.
|
|
|
|
### Aufruf-Beispiel:
|
|
|
|
```bash
|
|
pack-and-deploy --project myapp --version 1.4.0 --channel prod \
|
|
--platform win-x64 \
|
|
--publish-dir ./bin/Release/net8.0/win-x64/publish \
|
|
--changelog "Fehlerbehebungen und Performance-Optimierung"
|
|
```
|
|
|
|
Ohne `--platform` versucht der Packager, die Kennung aus dem Publish-Pfad zu
|
|
lesen (`.../net8.0/linux-x64/publish` → `linux-x64`). Gelingt das nicht, gilt
|
|
das Release als plattformunabhängig und es erscheint eine Warnung.
|
|
|
|
### Konfiguration (`packager.config.json`)
|
|
|
|
> Diese Datei enthält Zugangsdaten und ist per `.gitignore` von der
|
|
> Versionskontrolle ausgeschlossen. Vorlage: `packager.config.example.json`.
|
|
> In der vorherigen Fassung standen die echten FTP-Zugangsdaten sowohl hier in
|
|
> der Anleitung als auch als Standardwerte im Quelltext von `Program.cs`.
|
|
|
|
```json
|
|
{
|
|
"ftpHost": "ftp.example.com",
|
|
"ftpPort": 21,
|
|
"ftpUser": "ftp-user",
|
|
"ftpPass": "ftp-password",
|
|
"ftpRemoteBaseDir": "/public_html/releases",
|
|
"apiBaseUrl": "https://dc.mhdf.de",
|
|
"apiToken": "dc_sub_...",
|
|
"excludePatterns": [
|
|
"*.pdb",
|
|
"*.xml",
|
|
"appsettings.Development.json",
|
|
"*.log",
|
|
"logs/**"
|
|
],
|
|
"preservePatterns": [
|
|
"appsettings.json",
|
|
"settings.json",
|
|
".env"
|
|
]
|
|
}
|
|
```
|
|
|
|
`apiToken` braucht das Recht `updateservice:publish`. Ohne Token baut und lädt
|
|
der Packager das Paket zwar hoch, meldet es aber nicht beim Deploymentcenter an
|
|
und beendet sich mit Rückgabewert 2. Ohne Registrierung entsteht auch **keine
|
|
Signatur**.
|
|
|
|
### Alternative: Umgebungsvariablen
|
|
|
|
Für CI-Läufe, in denen keine Datei abgelegt werden soll — sie haben Vorrang vor
|
|
der Konfigurationsdatei:
|
|
|
|
```bash
|
|
export DC_FTP_HOST=ftp.example.com
|
|
export DC_FTP_USER=ftp-user
|
|
export DC_FTP_PASS='...'
|
|
export DC_TOKEN='dc_sub_...'
|
|
|
|
pack-and-deploy --project myapp --version 1.4.0 --channel prod \
|
|
--platform linux-x64 \
|
|
--publish-dir ./bin/Release/net8.0/linux-x64/publish
|
|
```
|
|
|
|
### Rückgabewerte
|
|
|
|
| Wert | Bedeutung |
|
|
|---|---|
|
|
| `0` | Paket gebaut, hochgeladen und im Deploymentcenter registriert |
|
|
| `1` | Konfiguration unvollständig, Publish-Verzeichnis fehlt oder Versionskonflikt — nichts wurde ausgeführt |
|
|
| `2` | Teilweise fehlgeschlagen: FTP-Upload oder Registrierung ging schief |
|
|
|
|
Zuvor lieferte das Werkzeug in allen Fällen `0` und meldete „successfully
|
|
published", selbst wenn FTP-Upload und API-Aufruf beide fehlgeschlagen waren.
|
|
|
|
---
|
|
|
|
## 3A. Ausschließen oder schützen
|
|
|
|
Das sind zwei verschiedene Dinge, und die Unterscheidung ist der Grund, warum
|
|
Updates bisher Konfigurationen zerstört haben.
|
|
|
|
| | `excludePatterns` | `preservePatterns` |
|
|
|---|---|---|
|
|
| Im Paket? | nein | ja |
|
|
| Bei der Erstinstallation? | fehlt | wird geschrieben |
|
|
| Beim Update? | — | vorhandene Datei bleibt unangetastet |
|
|
| Wofür | Build-Artefakte, Logs, Entwicklungs-Einstellungen | Konfigurationsvorlagen |
|
|
|
|
Eine `appsettings.json` gehört ins Paket — sonst ist eine Erstinstallation
|
|
unvollständig. Sie darf beim Update nur nicht über die eingerichteten Werte des
|
|
Zielsystems geschrieben werden. Genau dafür ist `preservePatterns` da; die
|
|
Liste wandert ins `manifest.json` und wird vom Agenten ausgewertet.
|
|
|
|
> **Vorher:** `IsExcluded` verstand ausschließlich `*.endung` und exakte
|
|
> Namen. Die mitgelieferte Beispielkonfiguration enthielt `logs/**` und
|
|
> `scratch/**` — beides traf **nie** zu. Und eine `settings.json` mit
|
|
> Datenbankpasswort und DC-Token stand auf keiner der beiden Listen: sie wurde
|
|
> mitgeliefert und überschrieb beim Update die Konfiguration jedes Zielsystems.
|
|
|
|
Die Muster sind jetzt echte Globs:
|
|
|
|
| Muster | trifft |
|
|
|---|---|
|
|
| `*.pdb` | jede `.pdb` in jedem Unterverzeichnis |
|
|
| `logs/**` | alles unterhalb von `logs/` |
|
|
| `wwwroot/*.css` | nur direkt in `wwwroot/`, nicht darunter |
|
|
| `wwwroot/**/*.css` | auch in Unterverzeichnissen |
|
|
| `appsettings*.json` | `appsettings.json`, `appsettings.Production.json`, … |
|
|
|
|
### Der Packager bricht bei Zugangsdaten ab
|
|
|
|
Seit Version 2.5 nicht mehr nur eine Warnung. Anlass war ein echter
|
|
API-Schlüssel, der so in ein öffentlich abrufbares Paket geriet — und die
|
|
Warnung wurde damals ausgerechnet unterdrückt, weil die Datei auf der
|
|
`preserve`-Liste stand.
|
|
|
|
| Stufe | Findet |
|
|
|---|---|
|
|
| Dateiname | `appsettings.Local.json`, `secrets.json`, `master.key`, `*.pfx`, `*.db`, `server_settings.xml`, … |
|
|
| Inhalt | gefüllte `Password=`-Felder, `sk-…`, `ghp_…`, `dc_master_…`, `AKIA…`, private Schlüssel |
|
|
|
|
Die Inhaltsprüfung findet auch Dateien mit unverdächtigem Namen. Platzhalter
|
|
wie `sk-DEIN-SCHLUESSEL-HIER` oder `Password=changeme` lösen **nicht** aus —
|
|
sonst würde jede ausgelieferte Vorlage den Build blockieren, und
|
|
`--allow-secrets` wäre nach einer Woche Gewohnheit.
|
|
|
|
**Der Zugangsschutz macht das nicht harmlos.** Ein Schlüssel im Paket ist dann
|
|
für jeden Lizenznehmer lesbar statt für alle — besser, aber immer noch falsch.
|
|
|
|
Notausgang für bewusste Fälle: `--allow-secrets`.
|
|
|
|
### Versionsgegenprobe
|
|
|
|
Der Packager liest die Version aus der Hauptassembly und bricht bei einer
|
|
Abweichung zu `--version` ab:
|
|
|
|
```
|
|
[FEHLER] Versionskonflikt:
|
|
--version sagt : 1.0.2
|
|
MyApp.dll sagt : 1.0.1
|
|
```
|
|
|
|
Der Grund dafür ist unangenehm genug, um dafür abzubrechen: Wird `1.0.1` als
|
|
`1.0.2` veröffentlicht, aktualisieren alle Clients, melden danach weiterhin
|
|
`1.0.1`, halten das Release erneut für neu — und aktualisieren bei jedem Start
|
|
wieder. Eine Endlosschleife über die gesamte Installationsbasis.
|
|
|
|
Üblicher Auslöser: `<Version>` steht nur in einem der beteiligten Projekte. Der
|
|
Wert gehört in die `Directory.Build.props`. Notausgang für bewusste
|
|
Abweichungen: `--ignore-version-mismatch`. Lässt sich die Assembly nicht
|
|
bestimmen, wird nur gewarnt — `--main-assembly` gibt sie gezielt an.
|
|
|
|
---
|
|
|
|
## 4. Standalone UpdateAgent (`update-agent`)
|
|
|
|
Der `update-agent` kann sowohl interaktiv (Spectre.Console Terminal UI) als auch im Headless CLI-Modus betrieben werden.
|
|
|
|
### CLI Modus Befehle:
|
|
|
|
```bash
|
|
# Nach Updates suchen
|
|
update-agent --project myapp --channel prod --action check
|
|
|
|
# Auf neueste Version aktualisieren
|
|
update-agent --project myapp --channel prod --action update --version latest --target-dir /opt/myapp
|
|
|
|
# Rollback auf ältere Version
|
|
update-agent --project myapp --channel prod --action update --version 1.3.2
|
|
|
|
# Integritäts-Reparatur der aktuellen Installation
|
|
update-agent --project myapp --channel prod --action repair
|
|
|
|
# Alle verfügbaren Versionen auflisten
|
|
update-agent --project myapp --channel prod --action list
|
|
```
|
|
|
|
Zusätzliche Parameter seit 2.2:
|
|
|
|
| Parameter | Wirkung |
|
|
|---|---|
|
|
| `--platform <rid>` | Laufzeitkennung; Vorgabe ist die des laufenden Systems |
|
|
| `--current-version <ver>` | Installierte Version, wenn keine `manifest.json` vorliegt |
|
|
| `--wait-for-pid <pid>` | Vor dem Anwenden auf das Ende dieses Prozesses warten |
|
|
| `--wait-timeout <sek>` | Geduld dabei (Vorgabe 60). Läuft der Prozess danach noch, wird **nichts** verändert |
|
|
| `--pubkey <datei>` | Öffentlicher Schlüssel zur Signaturprüfung |
|
|
| `--require-signature` | Ohne gültige Signatur nicht installieren |
|
|
|
|
---
|
|
|
|
## 4A. Welche Version ist installiert?
|
|
|
|
Es gab zwei Antworten darauf, und sie widersprachen sich: Die Anwendung
|
|
verglich `BuildInfo.Version` (einkompiliert), der Agent las `manifest.json` im
|
|
Zielverzeichnis. Fehlte diese Datei — etwa bei einer von Hand aufgesetzten
|
|
Installation — meldete der Agent „Unbekannt" und hielt **jede** Version für
|
|
neuer. Die Reparatur suchte dann auf dem Server nach einer Version namens
|
|
„Unbekannt" und brach genau dann ab, wenn man sie braucht.
|
|
|
|
Die Reihenfolge ist jetzt:
|
|
|
|
1. `--current-version`, falls übergeben — die Anwendung kennt ihre eigene Version am sichersten
|
|
2. `manifest.json` im Zielverzeichnis
|
|
3. sonst `0.0.0`, und die Reparatur greift auf `latest` zurück
|
|
|
|
Deshalb sollte `LaunchUpdateAgent` immer `currentVersion: BuildInfo.Version`
|
|
mitgeben.
|
|
|
|
---
|
|
|
|
## 4B. Wie ein Update angewendet wird
|
|
|
|
Die Stelle war als „Atomic Replace with Backup" kommentiert und war
|
|
tatsächlich eine Kopierschleife: kein Backup, kein Rollback, kein Aufräumen.
|
|
Brach sie in der Mitte ab — gesperrte Datei, volle Platte —, blieb eine halb
|
|
aktualisierte Installation zurück, aus der kein Weg zurückführte.
|
|
|
|
Der Ablauf ist jetzt:
|
|
|
|
1. **Plan bilden.** Welche Dateien werden geschrieben, welche sind geschützt,
|
|
welche gehören nicht mehr zum Release?
|
|
2. **Sichern.** Jede Datei, die überschrieben oder entfernt wird, wandert
|
|
vorher nach `.dc-update-backup/`.
|
|
3. **Anwenden.** Schreiben, dann verwaiste Dateien entfernen.
|
|
4. **Bei einem Fehler:** vollständiger Rollback aus dem Backup, danach wird die
|
|
Ursache gemeldet. Die Installation bleibt auf dem alten Stand lauffähig.
|
|
5. **Bei Erfolg:** Backup löschen, leer gewordene Verzeichnisse entfernen.
|
|
|
|
### Verwaiste Dateien
|
|
|
|
Eine DLL, die es im neuen Release nicht mehr gibt, blieb bisher für immer im
|
|
Verzeichnis liegen — bei .NET ein realer Weg in kaputte Assembly-Auflösung.
|
|
Sie wird jetzt entfernt, aber **nur**, wenn sie in der `manifest.json` der
|
|
Vorversion stand. Ohne dieses Wissen wird nichts gelöscht; Dateien, die nicht
|
|
aus einem Release stammen, bleiben in jedem Fall unangetastet.
|
|
|
|
### Der Agent im Paket
|
|
|
|
`--target-dir` zeigt in der Vorgabe auf das Verzeichnis des Agenten selbst.
|
|
Liegt der Agent im Paket, kopierte er sich also unter laufendem Betrieb über
|
|
sich selbst — unter Windows eine Zugriffsverletzung mitten im Update.
|
|
|
|
Eine laufende ausführbare Datei lässt sich unter Windows nicht überschreiben,
|
|
aber umbenennen. Der Agent legt sich deshalb als `update-agent.exe.dc-old` zur
|
|
Seite, schreibt die neue Fassung und entfernt den Rest beim nächsten Start.
|
|
|
|
### Ausführungsrechte
|
|
|
|
Wird unter Windows für `linux-x64` gebaut, kennt das tar-Archiv keine
|
|
Unix-Rechte und alles landet als `644` — die Anwendung ließe sich auf dem
|
|
Zielsystem nicht starten. Der Agent setzt das Ausführungsbit beim Anwenden für
|
|
Dateien ohne Endung (der .NET-Apphost) und für `*.sh`.
|
|
|
|
---
|
|
|
|
## 4C. Prüf-Endpunkte (für eigene Anbindungen)
|
|
|
|
Die Lese-Endpunkte sind bewusst **ohne Token** erreichbar, damit ausgelieferte
|
|
Anwendungen ohne Anpassung weiter nach Updates suchen können. Sie liefern nur
|
|
Release-Metadaten, die über die Download-URL ohnehin öffentlich sind.
|
|
|
|
```bash
|
|
GET /api/updateservice/v1/check?product=myapp&version=1.4.2&channel=prod&platform=win-x64
|
|
```
|
|
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"update_available": true,
|
|
"current_version": "1.4.2",
|
|
"latest_version": "1.4.3",
|
|
"platform": "win-x64",
|
|
"is_critical": false,
|
|
"latest_release": {
|
|
"version": "1.4.3",
|
|
"platform": "win-x64",
|
|
"download_url": "https://dc.mhdf.de/releases/myapp/prod/win-x64/1.4.3/package.tar.gz",
|
|
"sha256_hash": "e3b0c442...",
|
|
"git_commit": "a21536f",
|
|
"size_bytes": 8412160,
|
|
"release_notes": "Behebt den Login-Fehler.",
|
|
"manifest_signature": "hsuQVhef...",
|
|
"is_critical": 0
|
|
}
|
|
}
|
|
```
|
|
|
|
> **Ohne `platform` werden ausschließlich Releases mit `platform=any`
|
|
> berücksichtigt.** Wer die Endpunkte selbst anspricht und für mehrere
|
|
> Plattformen ausliefert, muss den Parameter mitschicken.
|
|
|
|
Weitere Endpunkte:
|
|
|
|
| Aufruf | Zweck |
|
|
|---|---|
|
|
| `GET .../latest?product=myapp&channel=prod&platform=win-x64` | Höchstes Release, unabhängig von der Client-Version |
|
|
| `GET .../releases?product=myapp` | Alle Releases, nach Versionsordnung sortiert. `platform` filtert hier **exakt** — der Endpunkt listet den Bestand, er wählt kein Paket aus |
|
|
| `GET .../pubkey` | Öffentlicher Schlüssel zur Signaturprüfung |
|
|
|
|
### Versionsvergleich
|
|
|
|
Der Vergleich folgt der semantischen Versionsordnung. Konkret bedeutet das:
|
|
|
|
| Installiert | Verfügbar | Update? |
|
|
|---|---|---|
|
|
| `1.9.0` | `1.10.0` | ja — zweistellige Minor ist höher |
|
|
| `1.10.0` | `1.9.0` | nein |
|
|
| `1.0.0-rc.1` | `1.0.0` | ja — Release schlägt Vorabversion |
|
|
| `1.0.0` | `1.0.0-rc.1` | nein |
|
|
| `v1.4.2` | `v1.4.3` | ja — führendes `v` wird ignoriert |
|
|
|
|
> Zuvor verglich der Server lexikografisch. `1.9.0` galt dadurch als neuer als
|
|
> `1.10.0`, und Clients bekamen ein Downgrade als Update angeboten. Derselbe
|
|
> Fehler steckte im .NET-Client bei `v`-präfigierten Versionen und ist dort
|
|
> ebenfalls behoben.
|
|
|
|
### Verknüpfung mit dem Bugtracker
|
|
|
|
Beim Veröffentlichen schließen sich alle Bugtracker-Items, deren
|
|
`resolved_in_build` der veröffentlichten Version entspricht, automatisch. Die
|
|
Antwort nennt die Anzahl:
|
|
|
|
```json
|
|
{ "status": "success", "release_id": 12, "created": true, "auto_resolved": 3,
|
|
"message": "Release 1.4.3 (prod) für \"myapp\" veröffentlicht. 3 Bugtracker-Item(s) automatisch geschlossen." }
|
|
```
|
|
|
|
Damit schließt sich der Kreis: Ein Agent markiert einen Bug als „gelöst in
|
|
v1.4.3", der Packager veröffentlicht v1.4.3, und das Item schließt sich selbst.
|
|
|
|
---
|
|
|
|
## 5. LEMP Verzeichnisstruktur auf dem Server
|
|
|
|
```text
|
|
/var/www/releases/ (oder /public_html/releases/)
|
|
└── {ProjectId}/ # z.B. myapp, polytrader
|
|
├── prod/
|
|
│ ├── latest.json # nur platform = any
|
|
│ ├── 1.4.0/ # nur platform = any
|
|
│ │ ├── package.tar.gz
|
|
│ │ ├── package.tar.gz.sha256
|
|
│ │ └── manifest.json # Einzeldateien, Hashes, preserve-Liste
|
|
│ ├── win-x64/
|
|
│ │ ├── latest.json # eigene Historie je Plattform
|
|
│ │ └── 1.4.0/
|
|
│ │ ├── package.tar.gz
|
|
│ │ ├── package.tar.gz.sha256
|
|
│ │ └── manifest.json
|
|
│ └── linux-x64/
|
|
│ ├── latest.json
|
|
│ └── 1.4.0/
|
|
├── beta/
|
|
└── dev/
|
|
```
|
|
|
|
Jede Plattform führt ihre eigene `latest.json`. Der Agent fragt zuerst den
|
|
plattformspezifischen Pfad ab und fällt auf den plattformlosen zurück — Pakete
|
|
einer fremden Plattform werden dabei verworfen.
|
|
|
|
### Aufbewahrung
|
|
|
|
`latest.json` führt die letzten 15 Versionen. Ältere Versionsverzeichnisse
|
|
bleiben auf dem Server liegen, sind über den Agenten aber nicht mehr
|
|
auswählbar. Der Packager weist beim Herausfallen einer Version ausdrücklich
|
|
darauf hin; wer weiter zurück muss, holt das Paket von Hand.
|
|
|
|
---
|
|
|
|
## 5A. Zugangsschutz der Release-Verzeichnisse
|
|
|
|
> **Neu in Version 2.4.** Zuvor war `/releases/` offen — damit jede
|
|
> ausgelieferte Anwendung ohne Zugangsdaten nach Updates suchen konnte. Das
|
|
> bedeutete aber auch: **jeder im Internet konnte die vollständigen Pakete
|
|
> herunterladen**, mitsamt allem, was versehentlich darin lag.
|
|
|
|
Der Zugang hängt jetzt am Lizenzschlüssel. Wer eine gültige Lizenz für ein
|
|
Produkt hat, kommt an dessen Updates; wer keine hat, braucht auch keine. Die
|
|
Anwendung kennt ihren Schlüssel ohnehin und versorgt sich damit selbst — es
|
|
muss nichts zusätzlich verteilt werden.
|
|
|
|
### Wie es aufgebaut ist
|
|
|
|
Je Produktverzeichnis eine eigene `.htpasswd`:
|
|
|
|
```text
|
|
/releases/predictalytics/.htaccess ← AuthType Basic, Require valid-user
|
|
/releases/predictalytics/.htpasswd ← nur Schlüssel für predictalytics
|
|
/releases/warenwirtschaft/.htpasswd ← nur Schlüssel für warenwirtschaft
|
|
```
|
|
|
|
**Bewusst getrennt.** Eine gemeinsame Datei würde bedeuten, dass eine Lizenz für
|
|
Produkt A auch Produkt B öffnet. Da `license_licenses.product_id` jeden
|
|
Schlüssel ohnehin an genau ein Projekt bindet, fällt die Trennung beim Erzeugen
|
|
von selbst ab.
|
|
|
|
Darin stehen:
|
|
|
|
| Eintrag | Benutzername | Passwort |
|
|
|---|---|---|
|
|
| Gültige Lizenz | `lic_` + erste 16 Hexzeichen von SHA-256(Schlüssel) | der Schlüssel |
|
|
| Installationskonto | DC-Benutzername | dessen Passwort |
|
|
|
|
**Der Benutzername ist eine Ableitung, nicht der Schlüssel selbst.** Das
|
|
htpasswd-Format hasht nur die Passwortspalte. Stünde der Lizenzschlüssel auch
|
|
als Benutzername darin, wäre die Datei eine vollständige Klartext-Kundenliste
|
|
und der bcrypt-Hash daneben bloße Dekoration — ein einziger
|
|
Konfigurationsfehler vom Leak entfernt. So enthält sie nur eine
|
|
Einwegableitung und einen Hash über einen hochentropen Schlüssel; selbst
|
|
offengelegt ist damit nichts anzufangen.
|
|
|
|
Die Ableitung muss auf beiden Seiten zeichengenau übereinstimmen:
|
|
`ReleaseGuard::licenseUsername()` serverseitig,
|
|
`ReleaseCredentials.UsernameForLicenseKey()` im SDK. Weichen sie voneinander
|
|
ab, kommt niemand mehr an seine Updates.
|
|
|
|
Die Installationskonten stehen in **jeder** Datei — bei einer Erstinstallation
|
|
gibt es noch keinen Lizenzschlüssel, mit dem sich das Paket holen ließe. Ihr
|
|
Hash wird unverändert aus `dc_users` übernommen: PHPs `password_hash()` erzeugt
|
|
bcrypt im Format `$2y$`, und genau das versteht Apache. Ein Klartextpasswort
|
|
wird nirgends gebraucht.
|
|
|
|
### Woher die Anwendung den Schlüssel nimmt
|
|
|
|
Die häufigste Rückfrage bei der Integration: Der Schlüssel steht **im
|
|
Lizenz-Cache**, den das Lizenzmodul ohnehin führt. Es muss nichts zusätzlich
|
|
gespeichert werden.
|
|
|
|
```csharp
|
|
// Beim Start einmal: Lizenz prüfen (legt den Schlüssel im Cache ab)
|
|
var license = await new LicenseClient().EnsureLicensedAsync(
|
|
productSlug: "myapp",
|
|
serverBaseUrl: "https://dc.mhdf.de",
|
|
allowPrompt: false); // im Dienst: nicht nach einer Eingabe warten
|
|
|
|
if (!license.IsValid) { /* Sperrbetrieb, siehe Lizenz-Leitfaden §6 */ }
|
|
|
|
// Danach jederzeit für den Update-Weg:
|
|
string? key = LicenseClient.TryGetCachedKey("myapp");
|
|
|
|
var check = await new UpdateClient().CheckForUpdateAsync(
|
|
baseUrl: "https://dc.mhdf.de",
|
|
projectId: "myapp",
|
|
currentVersion: BuildInfo.Version,
|
|
channel: "prod",
|
|
credentials: ReleaseCredentials.FromLicenseKey(key));
|
|
|
|
if (check.Unauthorized)
|
|
{
|
|
// Kein Netzwerkfehler: Die Lizenz trägt nicht mehr.
|
|
log.Warn(check.Message);
|
|
}
|
|
```
|
|
|
|
`TryGetCachedKey` ist statisch und liest den verschlüsselten Cache aus
|
|
`LicenseConfig.GetStorageDirectory(slug)`. Liefert er `null`, wurde noch nie
|
|
erfolgreich validiert — dann gibt es auch keinen Update-Zugang.
|
|
|
|
> **Im Dienst ohne Heimatverzeichnis** (systemd `User=` ohne `$HOME`) fällt der
|
|
> Cache auf `/var/lib/<slug>/license` zurück. Ist auch das nicht beschreibbar,
|
|
> gibt es keinen Offline-Cache — dann muss `DEPLOYMENTCENTER_STORAGE_DIR` auf
|
|
> ein beschreibbares Verzeichnis zeigen.
|
|
|
|
### Was sich für Clients ändert
|
|
|
|
**Ohne Nachziehen bekommt keine bestehende Installation mehr Updates.**
|
|
|
|
```csharp
|
|
var checkResult = await updateClient.CheckForUpdateAsync(
|
|
baseUrl: "https://dc.mhdf.de",
|
|
projectId: "myapp",
|
|
currentVersion: BuildInfo.Version,
|
|
channel: "prod",
|
|
credentials: ReleaseCredentials.FromLicenseKey(meineLizenz));
|
|
|
|
if (checkResult.Unauthorized)
|
|
{
|
|
// Kein Netzwerkfehler: die Lizenz trägt nicht mehr.
|
|
Console.WriteLine(checkResult.Message);
|
|
}
|
|
|
|
UpdateClient.LaunchUpdateAgent(
|
|
agentPath: UpdateClient.ResolveAgentPath()!,
|
|
projectId: "myapp",
|
|
licenseKey: meineLizenz); // reicht --license-key an den Agenten durch
|
|
```
|
|
|
|
Auf der Kommandozeile — **bevorzugt über die Umgebung**:
|
|
|
|
```bash
|
|
DC_LICENSE_KEY=XXXXX-XXXXX-XXXXX-XXXXX-XXXXX \
|
|
update-agent --project myapp --action update
|
|
```
|
|
|
|
`--license-key` gibt es weiterhin, ist aber die schlechtere Wahl: Was in `argv`
|
|
steht, zeigt `ps` jedem Benutzer der Maschine. Aus genau diesem Grund verwirft
|
|
[UPGRADE.md §5](./UPGRADE.md#5-cron-für-den-watchdog-evaluator-einrichten) den
|
|
Weg, den Shared Key im Crontab zu hinterlegen — für einen Lizenzschlüssel gilt
|
|
dasselbe. Umgebungsvariablen haben Vorrang vor den Argumenten.
|
|
|
|
`LaunchUpdateAgent` übergibt den Schlüssel deshalb **nicht** als Argument: Es
|
|
setzt `DC_LICENSE_KEY` auf dem eigenen Prozess, der Agent erbt den
|
|
Umgebungsblock beim Start, und danach wird die Variable wieder entfernt.
|
|
|
|
Ersatzweise `DC_DOWNLOAD_USER` und `DC_DOWNLOAD_PASSWORD` (oder
|
|
`--download-user` / `--download-password`) für ein Installationskonto — das
|
|
nutzt der Installer bei der Erstinstallation.
|
|
|
|
### Wartezeit beim Übergeben
|
|
|
|
`LaunchUpdateAgent` gibt dem Agenten mit, wie lange er auf das Ende der
|
|
Anwendung warten soll. Vorgabe sind 60 Sekunden:
|
|
|
|
```csharp
|
|
UpdateClient.LaunchUpdateAgent(
|
|
agentPath: agentPath,
|
|
projectId: "myapp",
|
|
licenseKey: meineLizenz,
|
|
waitTimeoutSeconds: 180, // reicht auch für ein langsames StopAsync
|
|
exitCurrentApp: false); // siehe unten
|
|
```
|
|
|
|
Läuft die Zeit ab, **bricht der Agent ab, ohne etwas zu verändern** — sicher,
|
|
aber eben auch kein Update. Wer allein für `host.StopAsync` 30 Sekunden
|
|
braucht, sollte den Wert heraufsetzen.
|
|
|
|
> **`exitCurrentApp: true` beendet den Prozess mit `Environment.Exit(0)`.**
|
|
> Laufende `finally`-Blöcke anderer Threads, `IHostApplicationLifetime` und
|
|
> Destruktoren kommen dann nicht mehr zum Zug. Für eine Anwendung mit offenem
|
|
> Zustand — Positionen, Transaktionen, ungeschriebene Puffer — ist das die
|
|
> falsche Wahl. Dort `exitCurrentApp: false` setzen und anschließend den
|
|
> eigenen geordneten Weg gehen; der Agent wartet ohnehin auf das Prozessende.
|
|
|
|
`UpdateCheckResult.Unauthorized` ist bewusst von einem gewöhnlichen Fehler
|
|
getrennt. Ohne diese Unterscheidung sieht ein abgelaufener Vertrag aus wie eine
|
|
Netzwerkstörung, und man sucht an der falschen Stelle.
|
|
|
|
### Wann neu erzeugt wird
|
|
|
|
Automatisch beim Anlegen, Bearbeiten, Widerrufen und Sperren einer Lizenz, beim
|
|
Löschen eines Projekts und bei jeder Änderung an einem Installationskonto.
|
|
|
|
**Abgelaufene Lizenzen lösen von sich aus nichts aus** — der Zeitpunkt vergeht
|
|
einfach. Dafür gleicht `cli/tick.php` nach: Es prüft, ob seit dem letzten Lauf
|
|
etwas abgelaufen ist, und erzeugt spätestens alle sechs Stunden ohnehin neu.
|
|
Ein minütlicher Aufruf ist unbedenklich, im Normalfall passiert nichts.
|
|
|
|
Von Hand: WebUI → **UpdateService → 🔒 Zugangsschutz**. Dort steht auch, welche
|
|
Produkte geschützt sind und wie viele Zugänge jeweils eingetragen sind.
|
|
|
|
| Anzeige | Bedeutung |
|
|
|---|---|
|
|
| GESCHÜTZT | die Dateien liegen vor — **das allein beweist nichts** |
|
|
| OFFEN | keine `.htaccess` — jeder im Internet kann laden |
|
|
| GESPERRT | Datei vorhanden, aber leer: weder gültige Lizenzen noch Installationskonten |
|
|
|
|
### Der Selbsttest ist die einzige belastbare Aussage
|
|
|
|
Dass `.htaccess` und `.htpasswd` existieren, sagt nichts darüber, ob sie
|
|
ausgewertet werden. Unter Nginx werden sie ignoriert, bei abgeschaltetem
|
|
`AllowOverride` ebenso, und ein Tippfehler in der Datei führt zu 500 statt 401.
|
|
In allen drei Fällen stünde in der Übersicht „GESCHÜTZT", während die Pakete
|
|
offen im Netz lägen.
|
|
|
|
Der Selbsttest ruft deshalb die **eigene Paket-Adresse ohne Zugangsdaten** ab
|
|
und erwartet 401. Er läuft bei jedem manuellen Erzeugen mit und nach jeder
|
|
automatischen Neuerzeugung durch `cli/tick.php`; das Ergebnis steht in der
|
|
Oberfläche und bei Fehlschlag im Log.
|
|
|
|
Von Hand nachprüfen:
|
|
|
|
```bash
|
|
curl -I https://dc.mhdf.de/releases/<produkt>/prod/<rid>/<version>/package.tar.gz # 401
|
|
curl -I https://dc.mhdf.de/releases/<produkt>/.htpasswd # 403
|
|
```
|
|
|
|
### Nginx statt Apache
|
|
|
|
Dort greift `.htaccess` nicht. Der Schutz muss in die Serverkonfiguration:
|
|
|
|
```nginx
|
|
location ^~ /releases/ {
|
|
# Je Produkt eine eigene Datei - sonst öffnet eine Lizenz für A auch B.
|
|
# $1 ist der Produkt-Slug aus dem Pfad.
|
|
location ~ ^/releases/([^/]+)/ {
|
|
auth_basic "Deploymentcenter Releases";
|
|
auth_basic_user_file /pfad/zum/webroot/releases/$1/.htpasswd;
|
|
}
|
|
|
|
# Die Zugangsdateien selbst nie ausliefern.
|
|
location ~ /\.ht { deny all; }
|
|
}
|
|
```
|
|
|
|
`ReleaseGuard` erzeugt die `.htpasswd`-Dateien unverändert weiter — nur die
|
|
`.htaccess` bleibt dort wirkungslos. Der Selbsttest bestätigt anschließend,
|
|
dass es trägt.
|
|
|
|
### Grenzen
|
|
|
|
**Das macht Pakete nicht sicher.** Jeder lizenzierte Kunde kann sie weiterhin
|
|
auspacken. Ein eigener API-Schlüssel in einer mitgelieferten `appsettings.json`
|
|
wäre danach statt öffentlich eben für alle Kunden sichtbar — besser, aber immer
|
|
noch falsch. Was nicht ins Paket gehört, gehört auch mit Zugangsschutz nicht
|
|
hinein; siehe **[§3A](#3a-ausschließen-oder-schützen)**.
|
|
|
|
Die Lese-Endpunkte der API bleiben offen. Sie verraten weiterhin, welche
|
|
Produkte und Versionen es gibt — die Download-Adressen sind ohne Zugangsdaten
|
|
aber wertlos.
|
|
|
|
### Abschalten
|
|
|
|
Die erzeugten `.htaccess`-Dateien in den Produktverzeichnissen löschen. Beim
|
|
nächsten Auslöser entstehen sie neu; wer es dauerhaft will, muss den Aufruf von
|
|
`ReleaseGuard` entfernen.
|
|
|
|
---
|
|
|
|
## 6. Signatur der Releases
|
|
|
|
Der SHA256 eines Pakets stammt aus derselben Quelle wie das Paket selbst. Wer
|
|
den Webroot oder die FTP-Zugangsdaten kontrolliert, tauscht beide gemeinsam
|
|
aus — der Hash schützt dann gegen Übertragungsfehler, nicht gegen Manipulation.
|
|
Ausgerechnet auf dem Pfad, der fremden Code ausführt.
|
|
|
|
### Warum kein HMAC
|
|
|
|
Beim Lizenzmodul wird mit HMAC signiert, und das geht dort auf, weil der
|
|
**Server** prüft. Ein Update wird auf dem Zielsystem geprüft. Ein HMAC bräuchte
|
|
dort denselben geheimen Schlüssel wie auf dem Server; wer ihn ausliest, kann
|
|
beliebige Pakete signieren — die Signatur verlöre genau die Eigenschaft, wegen
|
|
der es sie gibt.
|
|
|
|
Deshalb asymmetrisch: der Server signiert mit einem privaten RSA-Schlüssel, der
|
|
Agent prüft mit dem öffentlichen.
|
|
|
|
### Einrichten
|
|
|
|
```bash
|
|
openssl genrsa -out /etc/dc/release-signing.pem 2048
|
|
chmod 600 /etc/dc/release-signing.pem
|
|
```
|
|
|
|
```php
|
|
// config/config.php
|
|
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),
|
|
```
|
|
|
|
Signiert wird beim Veröffentlichen, serverseitig. **Der Packager bekommt den
|
|
Schlüssel nicht zu sehen** — er läuft auf Entwicklerrechnern, und der Schlüssel
|
|
wäre so gut geschützt wie das schwächste dieser Systeme.
|
|
|
|
Signiert wird eine kanonische Zeile, nicht das Manifest-JSON: JSON-Ausgabe ist
|
|
nicht bytestabil (Schlüsselreihenfolge, Escaping, Zahlenformat), eine Signatur
|
|
darüber wäre unzuverlässig prüfbar.
|
|
|
|
```
|
|
dc-release-v1\n{product_slug}\n{version}\n{channel}\n{platform}\n
|
|
{sha256_hash klein}\n{download_url}\n{size_bytes}
|
|
```
|
|
|
|
### Prüfen
|
|
|
|
Der Agent holt den öffentlichen Schlüssel einmalig von
|
|
`/api/updateservice/v1/pubkey` und legt ihn als `dc-release-pubkey.pem` neben
|
|
sich ab. Meldet der Server später einen **anderen** Schlüssel, wird gewarnt und
|
|
weiterhin der hinterlegte benutzt — ein untergeschobener Server fällt damit
|
|
auf. War der Wechsel beabsichtigt, die Datei löschen.
|
|
|
|
| Lage | Verhalten |
|
|
|---|---|
|
|
| Signatur gültig | Installation läuft |
|
|
| Signatur ungültig | **Abbruch**, immer |
|
|
| Release unsigniert | Hinweis, Installation läuft |
|
|
| Kein öffentlicher Schlüssel | Hinweis, Installation läuft |
|
|
| `--require-signature` gesetzt | Die letzten beiden Fälle brechen ebenfalls ab |
|
|
|
|
Ohne hinterlegten Schlüssel bleibt also alles funktionsfähig — es fehlt nur die
|
|
Vertrauenskette, und darauf wird bei jedem Update hingewiesen.
|
|
|
|
### Erzwingen über `LaunchUpdateAgent`
|
|
|
|
**Seit 2.5.2.** `--require-signature` war vorher nur über die Kommandozeile
|
|
erreichbar — `LaunchUpdateAgent`, der empfohlene Weg aus
|
|
**[§2A](#a-referenz-auf-deploymentcenterclient)**, hatte dafür keinen
|
|
Parameter. Eine Anwendung, die dem Leitfaden folgte, aktualisierte damit
|
|
ungeprüft, während derselbe Vorgang von Hand auf der Kommandozeile geschützt
|
|
gewesen wäre — ausgerechnet bei dem Feature, das genau diesen Pfad absichern
|
|
soll.
|
|
|
|
```csharp
|
|
UpdateClient.LaunchUpdateAgent(
|
|
agentPath: agentPath,
|
|
projectId: "myapp",
|
|
licenseKey: meineLizenz,
|
|
requireSignature: true);
|
|
```
|
|
|
|
Um den öffentlichen Schlüssel muss sich niemand zusätzlich kümmern: der Agent
|
|
holt und cached ihn ohnehin selbst von `/api/updateservice/v1/pubkey` (siehe
|
|
oben, "Prüfen"). `--pubkey` bleibt nötig, wenn ein Schlüssel ohne
|
|
Netzwerkzugriff hinterlegt werden soll.
|
|
|
|
**Erst scharfschalten, wenn wirklich alles signiert ist.** Ist
|
|
`security.release_private_key` gesetzt, signiert der Server nur *neue*
|
|
Veröffentlichungen — bereits abgelegte Releases bleiben unsigniert, bis sie
|
|
erneut veröffentlicht werden. Mit `requireSignature: true` bricht der Agent
|
|
bei jedem noch unsignierten Release ab, auch bei einem sonst harmlosen
|
|
Downgrade-Schutz oder einer Reparatur auf eine ältere Version.
|
|
|
|
---
|
|
|
|
## 7. Release-Verwaltung (Bearbeiten & Löschen)
|
|
|
|
Veröffentlichte Releases können sowohl über das Web-Interface als auch über die API nachträglich bearbeitet oder gelöscht werden.
|
|
|
|
### Web-Interface (`public/index.php`)
|
|
- Im Tab **UpdateService** → **📦 Veröffentlichte Software Releases** enthält jede Zeile die Aktionen `✏️ Bearbeiten` und `🗑️ Löschen`.
|
|
- **Bearbeiten**: Öffnet einen Modal-Dialog, in dem Release-Notes, Download-URL, SHA256-Hash, Git Commit, Plattform, Kanal, Version und IsCritical angepasst werden können. Beim Speichern wird die kanonische Signatur serverseitig neu berechnet.
|
|
- **Löschen**: Entfernt nach Bestätigung den Release-Datensatz aus der Datenbank.
|
|
|
|
### API-Endpunkte
|
|
|
|
#### Release Bearbeiten
|
|
- **POST** `/api/updateservice/v1/update` (oder `action=update` / `action=edit`)
|
|
- Scope: `updateservice:publish`
|
|
- Parameter: `release_id` (oder `id`), optional updated `product_slug`, `version`, `channel`, `platform`, `download_url`, `sha256_hash`, `git_commit`, `release_notes`, `size_bytes`, `is_critical`.
|
|
- Antwort (200 OK):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"release_id": 42,
|
|
"updated": true,
|
|
"platform": "win-x64",
|
|
"signed": true,
|
|
"message": "Release 1.2.0 (prod, win-x64) fuer \"myapp\" erfolgreich aktualisiert."
|
|
}
|
|
```
|
|
|
|
#### Release Löschen
|
|
- **POST / DELETE** `/api/updateservice/v1/delete` (oder `action=delete`)
|
|
- Scope: `updateservice:publish`
|
|
- Parameter: `release_id` (oder `id`) ODER die Kombination aus `product_slug`, `version`, `channel`, `platform`.
|
|
- Antwort (200 OK):
|
|
```json
|
|
{
|
|
"status": "success",
|
|
"release_id": 42,
|
|
"deleted": true,
|
|
"message": "Release v1.2.0 (prod, win-x64) fuer \"myapp\" geloescht."
|
|
}
|
|
```
|
|
|