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:
co-authored by
Claude Opus 5
parent
ceb977187e
commit
7a3a5dad69
@@ -182,6 +182,43 @@ entpackt sie dann nur und fragt nichts ab.
|
||||
| `validate` | Regulärer Ausdruck |
|
||||
| `target` | `id` des Ziels, wenn es mehrere gibt |
|
||||
|
||||
### Wohin geschrieben wird
|
||||
|
||||
Ein Ziel hat neben `file` eine Ortsangabe:
|
||||
|
||||
```json
|
||||
"targets": [
|
||||
{ "id": "app", "file": "appsettings.json", "format": "json" },
|
||||
{ "id": "user", "file": "clawddotnet/Settings.json",
|
||||
"location": "config", "format": "json" }
|
||||
]
|
||||
```
|
||||
|
||||
| `location` | Windows | Linux / macOS |
|
||||
|---|---|---|
|
||||
| `install` (Vorgabe) | Installationsverzeichnis | Installationsverzeichnis |
|
||||
| `config` | `%APPDATA%` | `$XDG_CONFIG_HOME`, sonst `~/.config` |
|
||||
| `data` | `%LOCALAPPDATA%` | `$XDG_DATA_HOME`, sonst `~/.local/share` |
|
||||
| `home` | Benutzerprofil | `$HOME` |
|
||||
|
||||
**Warum das nötig ist:** Eine Anwendung, die sich unter Linux richtig verhält,
|
||||
legt ihre Konfiguration nicht neben das Programm — `/opt/<app>` ist für den
|
||||
Dienstbenutzer typischerweise nicht schreibbar. Ohne Ortsangabe schriebe der
|
||||
Installer die Datei dorthin, wo die Anwendung nie nachsieht.
|
||||
|
||||
In `file` sind Umgebungsvariablen in beiden Schreibweisen erlaubt:
|
||||
`${XDG_CONFIG_HOME}/app/Settings.json` und `%APPDATA%\app\Settings.json`. Ein
|
||||
absoluter Pfad wird unverändert benutzt. Unbekannte Variablen bleiben stehen,
|
||||
statt zu einer leeren Zeichenkette zu werden — ein stehengebliebener
|
||||
Platzhalter fällt auf, ein stillschweigend falscher Pfad nicht.
|
||||
|
||||
> **Wer den Installer startet, entscheidet mit.** `config`, `data` und `home`
|
||||
> beziehen sich auf das Konto, unter dem der Installer läuft. Wird er als
|
||||
> `root` oder Administrator gestartet, der Dienst aber unter einem eigenen
|
||||
> Konto betrieben, landet die Datei im falschen Profil. Der Installer gibt
|
||||
> deshalb den **aufgelösten Pfad** aus — prüfe ihn. Für Dienste ist ein
|
||||
> absoluter Pfad oft die ehrlichere Angabe.
|
||||
|
||||
`type` bestimmt auch, **wie** geschrieben wird: `int` und `bool` landen als
|
||||
JSON-Zahl beziehungsweise -Wahrheitswert, nicht als Zeichenkette. Sonst
|
||||
scheitert die Bindung in der Anwendung.
|
||||
|
||||
@@ -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."
|
||||
|
||||
+22
-1
@@ -529,6 +529,21 @@ Läuft `cli/tick.php` als Cron, erzeugt es den Schutz beim ersten Lauf nach dem
|
||||
Deployment **von selbst**. Wer die Reihenfolge einhalten will, spielt den
|
||||
Serverteil also erst dann ein, wenn Schritt 3 erledigt ist.
|
||||
|
||||
> **Neue Produkte: die Reihenfolge oben gilt für sie nicht.**
|
||||
>
|
||||
> `ReleaseGuard` überspringt Verzeichnisse, die es nicht gibt — für ein Produkt
|
||||
> ohne Release ist `/releases/<slug>/` schlicht nicht vorhanden und damit auch
|
||||
> nicht geschützt. Das Verzeichnis entsteht erst mit dem **ersten** Upload, und
|
||||
> der nächste `tick.php`-Lauf legt den Schutz darin an.
|
||||
>
|
||||
> Es gibt also kein Zeitfenster, in dem sich ein noch ungeschütztes Release
|
||||
> herunterladen ließe, um dann das SDK nachzuziehen. **Der erste ausgelieferte
|
||||
> Build muss die Zugangsdaten bereits mitbringen** — sonst fällt die Tür hinter
|
||||
> ihm zu.
|
||||
>
|
||||
> Für Neuprodukte lautet die Reihenfolge daher schlicht: SDK 2.4 einbinden,
|
||||
> `licenseKey` übergeben, *dann* das erste Release veröffentlichen.
|
||||
|
||||
### 16.2 Was wo eingetragen wird
|
||||
|
||||
Je Produktverzeichnis eine `.htpasswd` mit den aktiven, nicht abgelaufenen
|
||||
@@ -552,9 +567,15 @@ wget -qO- https://dc.mhdf.de/installer/install.sh | sh
|
||||
|
||||
### 16.4 Prüfen
|
||||
|
||||
- [ ] **Selbsttest** im WebUI unter *UpdateService → 🔒 Zugangsschutz* — er ruft
|
||||
die eigene Paket-Adresse ohne Zugangsdaten ab und erwartet 401. Die
|
||||
Anzeige „GESCHÜTZT" allein beweist nichts: Sie prüft nur, ob die Dateien
|
||||
existieren, nicht ob der Server sie auswertet.
|
||||
- [ ] `curl -I https://dc.mhdf.de/releases/<produkt>/prod/<rid>/<version>/package.tar.gz`
|
||||
→ **401**
|
||||
- [ ] Mit `-u "<lizenzschlüssel>:<lizenzschlüssel>"` → **200**
|
||||
- [ ] Mit `-u "$(printf 'lic_%s' "$(printf %s "<schlüssel>" | sha256sum | cut -c1-16)"):<schlüssel>"` → **200**
|
||||
(Benutzername ist die Ableitung, nicht der Schlüssel — siehe
|
||||
[§5A](./UPDATESERVICE_INTEGRATION_GUIDE.md#5a-zugangsschutz-der-release-verzeichnisse))
|
||||
- [ ] `curl -I https://dc.mhdf.de/releases/<produkt>/.htpasswd` → **403**
|
||||
- [ ] `https://dc.mhdf.de/installer/update-agent-linux-x64` → **200**, weiterhin offen
|
||||
- [ ] Eine Lizenz widerrufen und erneut mit ihr laden → **401**
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
"displayName": "Beispielanwendung",
|
||||
"description": "Fuer den Betrieb werden eine Datenbank und ein Zugang zum Kursdatenanbieter benoetigt.",
|
||||
|
||||
"_targets_comment": "Wohin geschrieben wird. Pfade sind relativ zum Installationsverzeichnis. format ist 'json' oder 'env'. Ueber 'id' lassen sich einzelne Felder gezielt einer Datei zuordnen; ohne Angabe schreibt ein Feld in das erste Ziel.",
|
||||
"_targets_comment": "Wohin geschrieben wird. 'location' bestimmt den Bezugspunkt: install (Vorgabe, Installationsverzeichnis), config (%APPDATA% bzw. $XDG_CONFIG_HOME), data, home. In 'file' sind ${VAR} und %VAR% erlaubt, ein absoluter Pfad wird unveraendert benutzt. format ist 'json' oder 'env'. Ueber 'id' lassen sich einzelne Felder gezielt einer Datei zuordnen; ohne Angabe schreibt ein Feld in das erste Ziel.",
|
||||
"targets": [
|
||||
{ "id": "app", "file": "appsettings.json", "format": "json" },
|
||||
{ "id": "env", "file": ".env", "format": "env" }
|
||||
{ "id": "app", "file": "appsettings.json", "location": "install", "format": "json" },
|
||||
{ "id": "user", "file": "beispielanwendung/Settings.json", "location": "config", "format": "json" },
|
||||
{ "id": "env", "file": ".env", "location": "install", "format": "env" }
|
||||
],
|
||||
|
||||
"_fields_comment": "source steuert, ob gefragt wird: 'ask' (Vorgabe) fragt nach, 'detect:...' leitet ab, 'provision' laesst ein Token ausstellen. Ist der Wert in der Zieldatei bereits gesetzt und kein Platzhalter, wird er uebernommen und nicht erneut erfragt.",
|
||||
|
||||
Reference in New Issue
Block a user