Fuenf von sechs Befunden einer externen Integration. Der sechste - unsignierte Lizenzurteile - ist bestaetigt, aber bewusst nicht Teil dieses Commits. 1. Lizenzschluessel stand in der Kommandozeile Der Agent nahm --license-key nur als Argument und las keine Umgebungsvariablen. "ps" zeigt argv jedem Benutzer der Maschine - exakt die Begruendung, mit der UPGRADE.md §5 den Crontab-Weg verwirft. Damit nahm das SDK einen bereits geloesten Sicherheitsbefund zurueck. Der Agent liest jetzt DC_LICENSE_KEY, DC_DOWNLOAD_USER und DC_DOWNLOAD_PASSWORD, Umgebung vor Argument. LaunchUpdateAgent uebergibt den Schluessel nicht mehr als Argument, sondern setzt die Variable auf dem eigenen Prozess: das Kind erbt den Umgebungsblock, danach wird sie wieder entfernt. Das funktioniert auch mit UseShellExecute=true, wo sich ProcessStartInfo.Environment nicht setzen laesst. 2. --wait-timeout war nicht durchgereicht Der Agent kannte den Parameter, LaunchUpdateAgent hatte keinen dafuer - es galten fest 60 Sekunden. Eine Anwendung, die allein fuer host.StopAsync 30 Sekunden braucht, kommt damit gefaehrlich nah an die Grenze. Neu: waitTimeoutSeconds. Ausserdem ist im Quelltext und in der Doku jetzt festgehalten, dass exitCurrentApp:true ueber Environment.Exit(0) laeuft und damit finally-Bloecke und IHostApplicationLifetime uebergeht - bei offenem Zustand die falsche Wahl. 3. ILicensePrompt war tot Der Konstruktor nahm es entgegen, legte es in _prompt ab und benutzte es nirgends. Wer darauf eine headless-Story aufbaute, baute auf Sand. Neu: EnsureLicensedAsync() - zwischengespeicherten Schluessel nehmen, sonst fragen, pruefen, bei Ablehnung erneut fragen. allowPrompt:false lehnt ohne Cache ab, statt auf eine Eingabe zu warten, die im Dienst nie kommt. Ein voruebergehender Netzfehler fuehrt nicht zur erneuten Abfrage - der Schluessel ist ja nicht falsch. 4. Der Packager warnte nur Er bricht jetzt ab. Anlass war ein echter API-Schluessel in einem oeffentlich abrufbaren Paket - und die Warnung war damals ausgerechnet unterdrueckt, weil die Datei auf der preserve-Liste stand. Zwei Stufen: Dateiname (appsettings.Local.json, master.key, *.pfx, *.db, server_settings.xml) und Inhalt (gefuelltes Password=, sk-, ghp_, dc_master_, AKIA, private Schluessel). Die Inhaltspruefung findet auch Dateien mit unverdaechtigem Namen. Platzhalter loesen bewusst nicht aus: "sk-DEIN-SCHLUESSEL-HIER" haette sonst jede ausgelieferte Vorlage blockiert, und --allow-secrets waere nach einer Woche Gewohnheit. Beim Erproben zuerst genau in diese Falle gelaufen. 5. BuildInfo.targets war nur per Pfad-Import zu haben Die Anleitung empfahl einen <Import> ins Nachbar-Repository - das setzt voraus, dass beide Arbeitskopien nebeneinander liegen und in derselben Fassung stehen. Das Client-Projekt ist jetzt packbar und legt das Target unter build/ ins Paket, wo NuGet es selbst importiert. Ausserdem: Unauthorized wurde nur im statischen Zweig erkannt, im API-Zweig kam ein 401 als gewoehnlicher HTTP-Fehler an. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
948 lines
38 KiB
Markdown
948 lines
38 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)**.
|
|
|
|
|
|
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.0" />
|
|
</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.
|
|
|
|
### 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.
|
|
|
|
---
|
|
|
|
## 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."
|
|
}
|
|
```
|
|
|