feat(releases): Zugangsschutz ueber Lizenzschluessel
/releases/ wurde bisher offen ausgeliefert, damit ausgelieferte Anwendungen ohne Zugangsdaten nach Updates suchen koennen. Das bedeutete aber auch, dass jeder im Internet die vollstaendigen Pakete herunterladen konnte - mitsamt allem, was versehentlich darin liegt. Genau so lag ein echter API-Schluessel in einer mitgelieferten appsettings.json oeffentlich abrufbar. Zugang haengt jetzt am Lizenzschluessel: Wer eine gueltige Lizenz fuer ein Produkt hat, kommt an dessen Updates. Die Anwendung kennt ihren Schluessel ohnehin und versorgt sich damit selbst - es muss nichts verteilt werden. Server - ReleaseGuard erzeugt je Produktverzeichnis .htaccess und .htpasswd. Bewusst getrennt: eine gemeinsame Datei wuerde bedeuten, dass eine Lizenz fuer Produkt A auch Produkt B oeffnet. license_licenses.product_id bindet jeden Schluessel ohnehin an genau ein Projekt. - Eingetragen werden aktive, nicht abgelaufene Lizenzen (Benutzername = Passwort = Schluessel; Basic Auth braucht zwei Felder, es gibt aber nur ein Geheimnis) sowie alle Installationskonten - bei einer Erstinstallation gibt es noch keinen Schluessel, mit dem sich das Paket holen liesse. - Deren Hash wird unveraendert aus dc_users uebernommen: password_hash() erzeugt bcrypt im Format $2y$, genau das versteht Apache. Ein Klartextpasswort wird nirgends gebraucht. Argon2-Hashes werden erkannt und uebersprungen statt eine unbrauchbare Datei zu erzeugen. - Lizenzschluessel werden mit Kosten 8 gehasht statt 12: 29 Zeichen maschineller Zufall sind kein Menschenpasswort, Apache prueft aber bei *jeder* Anfrage neu. - Geschrieben wird ueber eine temporaere Datei mit rename() - ein Abbruch wuerde sonst eine halbe Zugangsdatei hinterlassen und in dem Moment die halbe Kundschaft aussperren. - Neu erzeugt bei jeder Lizenz- und Kontoaenderung. Abgelaufene Lizenzen loesen anders als ein Widerruf nichts aus; dafuer gleicht cli/tick.php nach und erzeugt spaetestens alle sechs Stunden neu. - Statusanzeige und Schaltflaeche im WebUI unter UpdateService. Client - ReleaseCredentials: Lizenzschluessel oder Installationskonto als Basic Auth. - UpdateClient und Agent senden sie fuer latest.json und package.tar.gz. - UpdateCheckResult.Unauthorized trennt "Lizenz traegt nicht mehr" von einem Netzwerkfehler. Ohne diese Unterscheidung sucht man an der falschen Stelle. - LaunchUpdateAgent reicht licenseKey als --license-key durch. - Der Installer benutzt die beim Anmelden eingegebenen Zugangsdaten auch fuer den Paketabruf; das Setup-Token taugt dafuer nicht, weil Apache prueft und nicht die Anwendung. Sonstiges - deploy.py klammert artifacts/ aus. Ohne das landeten die gebauten Installer-Binaries zusaetzlich unter /artifacts/ im Webroot. ACHTUNG Reihenfolge: Der Schutz sperrt jede Anwendung aus, die noch mit dem alten SDK gebaut ist. Erst ausliefern, dann scharfschalten - siehe UPGRADE.md §16. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f41372e5f
commit
ceb977187e
@@ -47,6 +47,16 @@ irm https://dc.mhdf.de/installer/install.ps1 | iex
|
||||
| `/installer/update-agent-linux-arm64` | Linux ARM64 |
|
||||
| `/installer/installer.json` | Version, Größen und Prüfsummen aller Binaries |
|
||||
|
||||
**Der Installer-Download selbst bleibt offen** — die Binaries enthalten keine
|
||||
Zugangsdaten, und eine Anmeldung an dieser Stelle würde nur den Bootstrap
|
||||
verkomplizieren (Zugangsdaten in `wget --user=…` landen in der Shell-History).
|
||||
|
||||
Beim Ausführen fragt der Installer als Erstes nach Benutzername und Passwort.
|
||||
**Dieselben Zugangsdaten öffnen auch die Release-Ablage**, die seit Version 2.4
|
||||
hinter HTTP-Basic-Auth liegt — bei einer Erstinstallation gibt es noch keinen
|
||||
Lizenzschlüssel, mit dem sich das Paket holen ließe. Siehe
|
||||
**[UPDATESERVICE_INTEGRATION_GUIDE §5A](./UPDATESERVICE_INTEGRATION_GUIDE.md#5a-zugangsschutz-der-release-verzeichnisse)**.
|
||||
|
||||
Zu jedem Binary liegt eine `.sha256` daneben. **Die Prüfsumme vergleichen** —
|
||||
„Programm herunterladen und Zugangsdaten eingeben" ist genau das Muster, das
|
||||
Phishing nachahmt; die Prüfsumme ist der Grund, warum man es hier trotzdem tun
|
||||
@@ -65,7 +75,7 @@ NativeAOT und ohne Trimming — Spectre.Console löst seine Eingabeaufforderunge
|
||||
### Neu bauen und hochladen
|
||||
|
||||
```bash
|
||||
pwsh scripts/build_installer.ps1
|
||||
powershell -File scripts/build_installer.ps1
|
||||
python scripts/upload_installer.py ./artifacts/installer
|
||||
```
|
||||
|
||||
|
||||
@@ -573,6 +573,126 @@ 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 | der Lizenzschlüssel | derselbe 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.
|
||||
|
||||
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:
|
||||
|
||||
```bash
|
||||
update-agent --project myapp --action update --license-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
||||
```
|
||||
|
||||
Ersatzweise `--download-user` und `--download-password` für ein
|
||||
Installationskonto — das nutzt der Installer bei der Erstinstallation.
|
||||
|
||||
`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 | alles in Ordnung |
|
||||
| OFFEN | keine `.htaccess` — jeder im Internet kann laden |
|
||||
| GESPERRT | Datei vorhanden, aber leer: weder gültige Lizenzen noch Installationskonten |
|
||||
|
||||
### 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
|
||||
|
||||
@@ -12,6 +12,11 @@ Schnittstellen ändern. Bitte in dieser Reihenfolge vorgehen.
|
||||
> signierte Releases, Staging-Swap mit Rollback und geschützte Konfigurationsdateien.
|
||||
> **Vor dem nächsten Release einzuspielen** —
|
||||
> **[§15 Umstellung auf 2.2](#15-umstellung-auf-22)**.
|
||||
>
|
||||
> **Version 2.4** stellt die Release-Ablage hinter einen Zugangsschutz.
|
||||
> **Reihenfolge beachten:** erst das SDK ausliefern, dann scharfschalten —
|
||||
> sonst bekommen bestehende Installationen keine Updates mehr.
|
||||
> **[§16 Umstellung auf 2.4](#16-umstellung-auf-24)**.
|
||||
|
||||
---
|
||||
|
||||
@@ -496,3 +501,69 @@ entscheiden:
|
||||
- [ ] Ein Update auf einem Testsystem: `appsettings.json` behält danach die
|
||||
eingerichteten Werte, und eine Datei, die es im neuen Release nicht mehr
|
||||
gibt, ist verschwunden.
|
||||
|
||||
---
|
||||
|
||||
## 16. Umstellung auf 2.4
|
||||
|
||||
Die Release-Ablage liegt jetzt hinter HTTP-Basic-Auth. Zugang hat, wer einen
|
||||
gültigen Lizenzschlüssel für das Produkt besitzt — oder ein Installationskonto.
|
||||
|
||||
Ausführlich: **[UPDATESERVICE_INTEGRATION_GUIDE §5A](./UPDATESERVICE_INTEGRATION_GUIDE.md#5a-zugangsschutz-der-release-verzeichnisse)**
|
||||
|
||||
### 16.1 Reihenfolge — das ist der kritische Teil
|
||||
|
||||
> **Erst ausliefern, dann scharfschalten.** Der Zugangsschutz sperrt jede
|
||||
> Anwendung aus, die noch mit dem alten SDK gebaut ist: Sie schickt keine
|
||||
> Zugangsdaten und bekommt ab dem Moment nur noch 401. Andersherum sperrst du
|
||||
> deine eigene Installationsbasis aus.
|
||||
|
||||
1. [ ] SDK auf 2.4 heben und `licenseKey` an `CheckForUpdateAsync` und
|
||||
`LaunchUpdateAgent` übergeben.
|
||||
2. [ ] Ein Release mit dem neuen SDK bauen und veröffentlichen.
|
||||
3. [ ] Warten, bis die Installationen dieses Release gezogen haben.
|
||||
4. [ ] **Erst dann** den Schutz erzeugen — WebUI → *UpdateService →
|
||||
🔒 Zugangsschutz → Zugangsschutz jetzt neu erzeugen*.
|
||||
|
||||
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.
|
||||
|
||||
### 16.2 Was wo eingetragen wird
|
||||
|
||||
Je Produktverzeichnis eine `.htpasswd` mit den aktiven, nicht abgelaufenen
|
||||
Lizenzen dieses Produkts (Benutzername = Passwort = Schlüssel) und allen
|
||||
Installationskonten. Bestehende Lizenzen werden dabei automatisch übernommen —
|
||||
es ist nichts von Hand nachzutragen.
|
||||
|
||||
### 16.3 Neue Aufrufe
|
||||
|
||||
```bash
|
||||
update-agent --project myapp --action update \
|
||||
--license-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
|
||||
```
|
||||
|
||||
```bash
|
||||
# Erstinstallation: der Installer fragt die Zugangsdaten ab und benutzt sie
|
||||
# auch für den Paketabruf. Der Installer-Download selbst bleibt offen.
|
||||
wget -qO- https://dc.mhdf.de/installer/install.sh | sh
|
||||
./update-agent --action install
|
||||
```
|
||||
|
||||
### 16.4 Prüfen
|
||||
|
||||
- [ ] `curl -I https://dc.mhdf.de/releases/<produkt>/prod/<rid>/<version>/package.tar.gz`
|
||||
→ **401**
|
||||
- [ ] Mit `-u "<lizenzschlüssel>:<lizenzschlüssel>"` → **200**
|
||||
- [ ] `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**
|
||||
|
||||
### 16.5 Wenn etwas klemmt
|
||||
|
||||
Ein 401 im Client heißt **Lizenz**, nicht Netzwerk. `UpdateCheckResult.Unauthorized`
|
||||
unterscheidet beides; der Agent gibt `UNAUTHORIZED: …` aus und liefert
|
||||
Rückgabewert 2.
|
||||
|
||||
Zum Abschalten die erzeugten `.htaccess`-Dateien in den Produktverzeichnissen
|
||||
löschen. Sie entstehen beim nächsten Auslöser neu.
|
||||
|
||||
Reference in New Issue
Block a user