fix(releases): Lizenzschluessel nicht mehr im Klartext, Selbsttest, Zielorte

Vier Befunde aus einer externen Durchsicht der 2.4-Integration.

1. Die .htpasswd war eine Klartext-Kundenliste
   Das htpasswd-Format hasht nur die Passwortspalte. Benutzername UND Passwort
   waren der Lizenzschluessel - der Schluessel stand also im Klartext direkt
   neben seinem eigenen bcrypt-Hash, und der Hash war Dekoration. Geschuetzt
   hat das Ganze nur die FilesMatch-Regel in derselben Datei.
   Der Benutzername wird jetzt abgeleitet: lic_<sha256(schluessel), 16 Hex>.
   Die Datei enthaelt damit nur noch eine Einwegableitung und einen Hash ueber
   einen hochentropen Schluessel.
   Server und SDK muessen dabei zeichengenau uebereinstimmen; ein Test prueft
   die C#-Ableitung gegen die PHP-Formel.

2. Ein Formatwechsel blieb unbemerkt liegen
   Beim Umbau auf 1. faellt auf: reconcile() sah keinen Anlass zur
   Neuerzeugung, die Dateien behielten das alte Format, waehrend die Clients
   bereits das neue schickten. Die erzeugten Dateien tragen deshalb jetzt eine
   Formatkennung; weicht sie ab, wird neu erzeugt.

3. Doku beschrieb Nginx, der Schutz ist Apache-only
   .htaccess wird von Nginx ignoriert - dort waeren die Verzeichnisse offen und
   die .htpasswd oeffentlich abrufbar. Die Statusanzeige pruefte nur, ob die
   Dateien existieren, und haette in dem Fall "GESCHUETZT" gemeldet.
   Neu: ein echter Selbsttest ruft die eigene Paket-Adresse OHNE Zugangsdaten
   ab und erwartet 401. Er laeuft beim manuellen Erzeugen und nach jeder
   automatischen Neuerzeugung; das Ergebnis steht in der Oberflaeche, ein
   Fehlschlag im Log. Er findet nebenbei auch abgeschaltetes AllowOverride und
   Tippfehler in der erzeugten Datei. Doku korrigiert, Nginx-Vorlage ergaenzt.

4. Erstinstallation schrieb an einen Ort, an dem Linux-Anwendungen nicht lesen
   setup.json-Ziele waren immer installationsrelativ. Eine Anwendung, die sich
   unter Linux richtig verhaelt, liest aus $XDG_CONFIG_HOME - /opt/<app> ist
   fuer den Dienstbenutzer meist nicht schreibbar. Der Installer legte die
   Datei also dorthin, wo nie jemand nachsieht.
   Ziele haben jetzt ein "location": install (Vorgabe), config, data, home,
   plus ${VAR}- und %VAR%-Ersetzung in "file". Unbekannte Variablen bleiben
   stehen statt leer zu werden - ein Platzhalter faellt auf, ein falscher Pfad
   nicht. Der Installer gibt den aufgeloesten Pfad aus, weil bei config das
   Konto entscheidet, unter dem er laeuft.

Ausserdem
- Doku zeigte "status": "ok" fuer update/delete; Http::ok() erzeugt
  "status": "success".
- UPGRADE §16.1 deckte Neuprodukte nicht ab: Fuer ein Produkt ohne Release
  existiert /releases/<slug>/ nicht und wird uebersprungen. Das Verzeichnis
  entsteht erst mit dem ersten Upload, der naechste Tick schuetzt es. Der erste
  ausgelieferte Build muss die Zugangsdaten also schon mitbringen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-13 11:47:26 +02:00
co-authored by Claude Opus 5
parent ceb977187e
commit 7a3a5dad69
11 changed files with 684 additions and 31 deletions
+74 -9
View File
@@ -27,7 +27,20 @@
> siehe **[§2A](#a-referenz-auf-deploymentcenterclient)**.
Das **UpdateService-Modul** des Deploymentcenters bietet ein unternehmensweites, leichtgewichtiges Update-, Rollback- und Reparatur-Schema auf Basis eines LEMP-Stacks (Nginx Static Files + PHP API).
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.
---
@@ -37,7 +50,8 @@ Das **UpdateService-Modul** des Deploymentcenters bietet ein unternehmensweites,
- **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 LEMP-Verteilung**: Downloads und Versionen-Manifeste (`latest.json`, `manifest.json`, `package.tar.gz`) werden über Nginx extrem performant bereitgestellt.
- **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.
---
@@ -604,12 +618,21 @@ Darin stehen:
| Eintrag | Benutzername | Passwort |
|---|---|---|
| Gültige Lizenz | der Lizenzschlüssel | derselbe Schlüssel |
| Gültige Lizenz | `lic_` + erste 16 Hexzeichen von SHA-256(Schlüssel) | der Schlüssel |
| Installationskonto | DC-Benutzername | dessen Passwort |
Beim Lizenzschlüssel sind Benutzername und Passwort identisch: Basic Auth
verlangt zwei Felder, es gibt aber nur ein Geheimnis, und Benutzernamen müssen
eindeutig sein.
**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
@@ -669,10 +692,52 @@ Produkte geschützt sind und wie viele Zugänge jeweils eingetragen sind.
| Anzeige | Bedeutung |
|---|---|
| GESCHÜTZT | alles in Ordnung |
| 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
@@ -775,7 +840,7 @@ Veröffentlichte Releases können sowohl über das Web-Interface als auch über
- Antwort (200 OK):
```json
{
"status": "ok",
"status": "success",
"release_id": 42,
"updated": true,
"platform": "win-x64",
@@ -791,7 +856,7 @@ Veröffentlichte Releases können sowohl über das Web-Interface als auch über
- Antwort (200 OK):
```json
{
"status": "ok",
"status": "success",
"release_id": 42,
"deleted": true,
"message": "Release v1.2.0 (prod, win-x64) fuer \"myapp\" geloescht."