/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>
296 lines
12 KiB
Markdown
296 lines
12 KiB
Markdown
# Deploymentcenter — Erstinstallation von Anwendungen
|
|
|
|
> **Neu in Version 2.3.** Bis dahin gab es nur den Update-Weg: eine Anwendung
|
|
> musste bereits installiert und eingerichtet sein, damit sich etwas
|
|
> aktualisieren ließ. Die Erstinstallation auf einem neuen System war
|
|
> Handarbeit — Paket kopieren, Konfiguration abtippen, Token besorgen,
|
|
> Monitoring nachtragen.
|
|
|
|
Die Erstinstallation läuft jetzt über denselben `update-agent`, der auch
|
|
Updates einspielt. Kein eigener Installer je Anwendung, kein MSI, kein
|
|
Setup-Assistent im Anwendungscode.
|
|
|
|
```bash
|
|
update-agent --action install
|
|
```
|
|
|
|
Mehr braucht es nicht. Der Rest ist ein Dialog.
|
|
|
|
---
|
|
|
|
## 0. Den Installer besorgen
|
|
|
|
Im WebUI unter **UpdateService → ⬇️ Installer**, oder direkt vom Server.
|
|
|
|
**Linux** — lädt das zur Architektur passende Binary, prüft die Prüfsumme und legt es ab:
|
|
|
|
```bash
|
|
wget -qO- https://dc.mhdf.de/installer/install.sh | sh
|
|
```
|
|
|
|
Ohne Skript geht es genauso:
|
|
|
|
```bash
|
|
wget https://dc.mhdf.de/installer/update-agent-linux-x64 -O update-agent
|
|
```
|
|
|
|
**Windows** (PowerShell):
|
|
|
|
```powershell
|
|
irm https://dc.mhdf.de/installer/install.ps1 | iex
|
|
```
|
|
|
|
| Datei | Plattform |
|
|
|---|---|
|
|
| `/installer/update-agent-win-x64.exe` | Windows x64 |
|
|
| `/installer/update-agent-linux-x64` | Linux x64 |
|
|
| `/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
|
|
kann. Die Bootstrap-Skripte tun das automatisch und brechen bei Abweichung ab.
|
|
|
|
Die Skripte richten **nichts** von selbst ein. Sie laden, prüfen, legen ab und
|
|
sagen, wie es weitergeht — ein Skript aus dem Netz, das ungefragt eine
|
|
Anwendung aufsetzt und dabei nach Zugangsdaten fragt, wäre genau das, wovor man
|
|
Nutzer sonst warnt.
|
|
|
|
Rund 34 MB je Plattform: Das Binary bringt die .NET-Laufzeit mit, damit auf
|
|
einem frisch aufgesetzten System nichts vorinstalliert sein muss. Bewusst ohne
|
|
NativeAOT und ohne Trimming — Spectre.Console löst seine Eingabeaufforderungen
|
|
über Reflexion auf; getrimmt baut das zwar, bricht aber erst beim Anwender.
|
|
|
|
### Neu bauen und hochladen
|
|
|
|
```bash
|
|
powershell -File scripts/build_installer.ps1
|
|
python scripts/upload_installer.py ./artifacts/installer
|
|
```
|
|
|
|
`deploy.py` klammert `client-dotnet` bewusst aus — der Quelltext des Agenten
|
|
gehört nicht auf den Webserver, die übersetzten Binaries schon. Deshalb der
|
|
eigene Upload-Weg.
|
|
|
|
---
|
|
|
|
## 1. Der Ablauf
|
|
|
|
```
|
|
update-agent --action install
|
|
├ Deploymentcenter? https://dc.mhdf.de
|
|
├ Benutzer / Passwort → kurzlebiges Token (30 Minuten)
|
|
├ Auswahl aus dem Katalog → nur, was auf dieser Plattform läuft
|
|
├ Kanal (prod / beta / dev)
|
|
├ Zielverzeichnis → vorbelegt mit /opt/<slug> bzw. Programme\<slug>
|
|
├ Paket laden, Prüfsumme, Signatur, Entpacken ← der reguläre Update-Weg
|
|
├ setup.json auflösen → fragt nur, was übrig bleibt
|
|
└ Konfiguration schreiben
|
|
```
|
|
|
|
Die Dateien kommen über **denselben Pfad wie ein Update** ins Zielverzeichnis:
|
|
mit SHA256-Prüfung, Signaturprüfung, Staging und Rollback. Ein zweiter
|
|
Download-Weg wäre ein zweiter Ort, an dem dieselben Fehler wieder entstehen.
|
|
|
|
---
|
|
|
|
## 2. Das Installationskonto
|
|
|
|
Für Zielsysteme gibt es die Rolle **`installer`**. Im WebUI unter
|
|
**System → 👤 Benutzer** anzulegen.
|
|
|
|
Ein solches Konto kann genau zwei Dinge: sich über `/api/setup/v1/login`
|
|
anmelden und Anwendungen einrichten. **Am WebUI kann es sich nicht anmelden** —
|
|
der Versuch wird abgewiesen und protokolliert.
|
|
|
|
Das ist der Kern der Sache: Die Zugangsdaten werden auf jedem Zielsystem
|
|
eingetippt, auf dem je etwas installiert wird. Mit einem Administratorkonto
|
|
verteilte man damit den Zugang zu Tokens, Lizenzen, Monitoren und dem
|
|
Bugtracker auf all diese Rechner.
|
|
|
|
| | admin | installer |
|
|
|---|---|---|
|
|
| WebUI | ja | **nein** |
|
|
| `/api/setup/v1/*` | ja | ja |
|
|
| Token ausstellen | alle Rechte | nur `watchdog:ping`, `watchdog:read`, `bugtracker:report`, `updateservice:read` |
|
|
|
|
> **Noch offen: der zweite Faktor.** `dc_users.totp_secret` existiert als
|
|
> Spalte, wird aber von keinem Anmeldeweg ausgewertet — es gibt bislang keine
|
|
> TOTP-Prüfung im Deploymentcenter. Ein Konto mit hinterlegtem Geheimnis wird
|
|
> von der Setup-Anmeldung deshalb ausdrücklich **abgelehnt** (`501
|
|
> totp_not_supported`), statt den zweiten Faktor stillschweigend zu übergehen.
|
|
> Bis das nachgezogen ist, tragen ein langes Passwort und die enge
|
|
> Rechtevergabe die Sicherheit.
|
|
|
|
---
|
|
|
|
## 3. `setup.json`
|
|
|
|
Die Datei beschreibt, was eine Anwendung zum Laufen braucht. Sie gehört in das
|
|
Publish-Verzeichnis und landet damit im Paket, neben der `manifest.json`.
|
|
|
|
**Bewusst im Paket und nicht zentral im Deploymentcenter.** So ist die
|
|
Beschreibung mit der Anwendung versioniert: Braucht Version 2.0 ein Feld mehr
|
|
als 1.9, stimmt es automatisch. Eine zweite Pflegestelle liefe früher oder
|
|
später auseinander.
|
|
|
|
Vollständige Vorlage: **[setup.example.json](./setup.example.json)**
|
|
|
|
```json
|
|
{
|
|
"schema": 1,
|
|
"displayName": "Beispielanwendung",
|
|
"targets": [
|
|
{ "id": "app", "file": "appsettings.json", "format": "json" }
|
|
],
|
|
"fields": [
|
|
{ "key": "ConnectionStrings:Main", "label": "Datenbank", "type": "secret" },
|
|
{ "key": "Deploymentcenter:BaseUrl", "source": "detect:baseurl", "type": "url" },
|
|
{ "key": "Deploymentcenter:Token", "source": "provision",
|
|
"scopes": ["watchdog:ping"] }
|
|
]
|
|
}
|
|
```
|
|
|
|
Fehlt die Datei, lässt sich die Anwendung trotzdem installieren — der Installer
|
|
entpackt sie dann nur und fragt nichts ab.
|
|
|
|
### Felder
|
|
|
|
| Eigenschaft | Bedeutung |
|
|
|---|---|
|
|
| `key` | Schlüssel im Ziel. Doppelpunkte trennen Ebenen (`ConnectionStrings:Main`) — die Schreibweise von `Microsoft.Extensions.Configuration` |
|
|
| `label` | Beschriftung der Frage |
|
|
| `help` | Erläuterung, die darüber steht |
|
|
| `type` | `string`, `secret`, `url`, `int`, `bool`, `enum` |
|
|
| `required` | Vorgabe `true` |
|
|
| `default` | Vorbelegung der Eingabe |
|
|
| `source` | `ask` (Vorgabe), `detect:…`, `provision` |
|
|
| `scopes` | Rechte, wenn `source: "provision"` |
|
|
| `options` | Auswahl bei `type: "enum"` |
|
|
| `validate` | Regulärer Ausdruck |
|
|
| `target` | `id` des Ziels, wenn es mehrere gibt |
|
|
|
|
`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.
|
|
|
|
### Nur die wirklich fehlenden Informationen
|
|
|
|
Je Feld gilt die erste zutreffende Regel:
|
|
|
|
| Regel | fragt |
|
|
|---|---|
|
|
| Wert steht schon in der Zieldatei und ist kein Platzhalter | nein |
|
|
| `source: "detect:…"` | nein |
|
|
| `source: "provision"` | nein |
|
|
| sonst | **ja**, mit `default` vorbelegt |
|
|
|
|
Erkannte Platzhalter (`changeme`, `TODO`, `your-…`, `<…>`, `example.com` und
|
|
ähnliche) gelten **nicht** als eingerichteter Wert — sonst liefe die Anwendung
|
|
mit der ausgelieferten Vorlage los.
|
|
|
|
Ableitbare Werte: `detect:hostname`, `detect:platform`, `detect:installdir`,
|
|
`detect:project`, `detect:baseurl`, `detect:username`.
|
|
|
|
### Das Token der Anwendung
|
|
|
|
`source: "provision"` lässt den Server ein Dauertoken ausstellen, das an das
|
|
Projekt und den Rechnernamen gebunden ist. Der Installer schreibt es in das
|
|
angegebene Feld; niemand muss es sehen oder kopieren.
|
|
|
|
Welche Rechte vergeben werden, entscheidet **der Server**, nicht die Anfrage.
|
|
Die `scopes` aus der `setup.json` können nur einschränken. Andernfalls wäre der
|
|
Umweg über ein kurzlebiges Setup-Token wirkungslos — wer einmal installieren
|
|
darf, stellte sich sonst ein Token mit allen Rechten aus und behielte es.
|
|
|
|
---
|
|
|
|
## 4. Konfiguration schreiben
|
|
|
|
**Vorhandene Inhalte bleiben erhalten.** Eine `appsettings.json` enthält neben
|
|
den abgefragten Werten fast immer Logging-Einstellungen, Feature-Schalter und
|
|
anderes. Die Datei neu zu erzeugen wäre der bequemere Weg und verlöre all das —
|
|
und zwar bei einer Neuinstallation, bei der niemand ein Backup hat.
|
|
|
|
- **JSON**: Ebenen werden aus dem Schlüssel gebildet, bestehende Zweige bleiben
|
|
stehen. Kommentare und nachgestellte Kommata werden beim Lesen toleriert.
|
|
- **env**: bestehende Zeilen werden ersetzt, unbekannte Schlüssel angehängt.
|
|
Kommentare und Reihenfolge bleiben erhalten. `A:B` wird zu `A__B`.
|
|
|
|
Ist eine vorhandene Datei kein gültiges JSON, wird sie als
|
|
`<name>.unlesbar-<zeitstempel>` zur Seite gelegt statt überschrieben.
|
|
|
|
Zusammenspiel mit `preservePatterns` des Packagers: Die Vorlage wird
|
|
ausgeliefert (Erstinstallation bekommt sie), ein Update ersetzt sie nicht, und
|
|
der Installer schreibt die abgefragten Werte hinein. Siehe
|
|
**[UPDATESERVICE_INTEGRATION_GUIDE §3A](./UPDATESERVICE_INTEGRATION_GUIDE.md#3a-ausschließen-oder-schützen)**.
|
|
|
|
---
|
|
|
|
## 5. Nachträglich einrichten
|
|
|
|
```bash
|
|
update-agent --action configure --target-dir /opt/myapp --project myapp
|
|
```
|
|
|
|
Liest die `setup.json` der bestehenden Installation und fragt nur, was noch
|
|
fehlt. Nach Zugangsdaten wird dabei nur gefragt, wenn ein Feld mit
|
|
`source: "provision"` vorkommt — sonst braucht es den Server gar nicht.
|
|
|
|
---
|
|
|
|
## 6. Schnittstelle
|
|
|
|
| Aufruf | Zweck | Authentifizierung |
|
|
|---|---|---|
|
|
| `POST /api/setup/v1/login` | Benutzername + Passwort → Setup-Token | keine |
|
|
| `GET /api/setup/v1/catalog` | Was ist hier installierbar? | `setup:catalog` |
|
|
| `POST /api/setup/v1/token` | Dauertoken der Anwendung | `setup:install` |
|
|
|
|
```bash
|
|
curl -X POST https://dc.mhdf.de/api/setup/v1/login \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"username":"installer","password":"…","hostname":"srv-07"}'
|
|
```
|
|
|
|
```json
|
|
{ "status": "success", "setup_token": "dc_setup_…", "expires_in": 1800,
|
|
"scopes": ["setup:install"], "role": "installer" }
|
|
```
|
|
|
|
Das Setup-Token lebt **nur im Speicher** des Installers, wird nirgends abgelegt
|
|
und läuft nach 30 Minuten ab. Was auf dem System zurückbleibt, ist das
|
|
Dauertoken der Anwendung mit genau den Rechten, die sie braucht.
|
|
|
|
Der Katalog zeigt ausschließlich, was zur Laufzeitkennung des anfragenden
|
|
Systems passt. Ein Projekt, von dem es nur ein Windows-Paket gibt, taucht auf
|
|
einem Linux-Rechner gar nicht erst auf — alles andere wäre eine Auswahl, die
|
|
beim Anklicken fehlschlägt.
|
|
|
|
---
|
|
|
|
## 7. Was noch fehlt
|
|
|
|
Diese Fassung ist Stufe 1. Bewusst noch nicht enthalten:
|
|
|
|
| Fehlt | Bedeutet |
|
|
|---|---|
|
|
| **Zentrale Geheimnisse** | Datenbankzugang und API-Schlüssel werden je Installation eingetippt. Stufe 2 hinterlegt sie verschlüsselt je *(Projekt, Umgebung)* im Deploymentcenter, sodass `source: "bundle:…"` sie automatisch zieht |
|
|
| **Übernahme früherer Antworten** | Die zweite Installation derselben Anwendung fragt dasselbe noch einmal |
|
|
| **Verbindungstests** | Ob die eingegebene Datenbankverbindung wirklich trägt, zeigt sich erst beim ersten Start |
|
|
| **TOTP** | Siehe [§2](#2-das-installationskonto) |
|
|
| **Dienstregistrierung** | systemd-Unit beziehungsweise Windows-Dienst legt der Installer nicht an |
|
|
| **Watchdog-Eintrag** | Der Monitor entsteht beim ersten Heartbeat, nicht schon bei der Installation |
|