feat(setup): Erstinstallation ueber den Update-Agent, Installationskonto, Downloads

Bisher gab es nur den Update-Weg: eine Anwendung musste bereits installiert und
eingerichtet sein, damit sich etwas aktualisieren liess. Die Erstinstallation
auf einem neuen System war Handarbeit - Paket kopieren, Konfiguration
abtippen, Token besorgen.

Setup-API (neu)
- POST /api/setup/v1/login tauscht Benutzername und Passwort gegen ein Token
  mit 30 Minuten Gueltigkeit und ausschliesslich setup:install. Es wird nicht
  mitgeschrieben und lebt im Installer nur im Speicher.
- GET /api/setup/v1/catalog zeigt nur, was zur Laufzeitkennung des anfragenden
  Systems passt. Ein Projekt mit ausschliesslich Windows-Paket taucht auf einem
  Linux-Rechner gar nicht erst auf.
- POST /api/setup/v1/token stellt das Dauertoken der Anwendung aus. Welche
  Rechte vergeben werden, entscheidet der Server; die Anfrage kann nur
  einschraenken. Sonst waere der Umweg ueber ein kurzlebiges Token wirkungslos.

Rollentrennung (Migration 012)
- dc_users bekommt role, disabled und last_login_at. Die Rolle "installer"
  darf sich ueber den Setup-Weg anmelden und nicht am WebUI. Die Zugangsdaten
  werden auf jedem Zielsystem eingetippt; mit einem Administratorkonto
  verteilte man damit den Zugang zu Tokens, Lizenzen und Monitoren auf jeden
  Rechner, auf dem je etwas installiert wurde.
- Auth::verifyCredentials() prueft sessionfrei, damit Setup- und WebUI-Login
  nicht zwei verschiedene Haertungsgrade haben (Drosselung, Timing-Angleichung,
  Rehash gelten fuer beide).
- Konten mit hinterlegtem TOTP-Geheimnis werden am Setup-Weg mit 501
  abgewiesen. Eine TOTP-Pruefung gibt es im Deploymentcenter noch nicht; sie
  stillschweigend zu uebergehen waere ein Rueckschritt.
- Benutzerverwaltung im WebUI - es gab bisher gar keine, nur den einen von
  install_db.php angelegten Admin. Das letzte aktive Administratorkonto laesst
  sich weder deaktivieren noch loeschen.

Installer
- update-agent --action install fuehrt durch Anmeldung, Auswahl,
  Zielverzeichnis, Installation und Einrichtung. Die Dateien kommen ueber
  denselben Pfad wie ein Update - mit Pruefsumme, Signatur, Staging und
  Rollback. Ein zweiter Download-Weg waere ein zweiter Ort fuer dieselben
  Fehler.
- --action configure holt die Einrichtung nachtraeglich.
- setup.json im Paket beschreibt die benoetigten Werte. Bewusst im Paket und
  nicht zentral: so ist sie mit der Anwendung versioniert.
- Gefragt wird nur, was uebrig bleibt: bereits gesetzt -> detect:... ->
  provision -> fragen. Platzhalter wie changeme oder <dein-wert> gelten dabei
  nicht als eingerichtet, sonst liefe die Anwendung mit der Vorlage los.
- SetupWriter erhaelt vorhandene Inhalte. Eine appsettings.json fuehrt neben
  den abgefragten Werten meist Logging und anderes; sie neu zu erzeugen waere
  bequemer und verloere das - bei einer Neuinstallation ohne Backup.
  int und bool landen als JSON-Typ, nicht als Zeichenkette.

Downloads
- scripts/build_installer.ps1 baut selbstenthaltende Einzeldateien fuer
  win-x64, linux-x64 und linux-arm64 (rund 34 MB, .NET-Laufzeit inbegriffen).
  Ohne NativeAOT und ohne Trimming: Spectre.Console loest ueber Reflexion auf
  und braeche sonst erst beim Anwender.
- scripts/upload_installer.py laedt sie nach /installer/. Getrennt von
  deploy.py, das client-dotnet bewusst ausklammert.
- Bereich "Installer" auf der UpdateService-Seite mit Groessen, Pruefsummen
  und den wget-Befehlen; die Angaben stammen aus installer.json statt aus fest
  eingetragenem Text.
- install.sh und install.ps1 laden, pruefen die Pruefsumme und legen ab -
  sie richten bewusst nichts selbst ein. Das Manifest wird BOM-frei
  geschrieben, sonst scheitert json_decode() daran.

Enthaelt ausserdem die bislang nicht committete Arbeit an den
RocketChat-Benachrichtigungen (Migrationen 010 und 011) sowie die Loesch- und
Editierfunktion des UpdateService; die betroffenen Dateien liessen sich nicht
getrennt stagen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Deploymentcenter Bot
2026-08-13 10:33:26 +02:00
co-authored by Claude Opus 5
parent 2388b5abe1
commit c8f3e78635
34 changed files with 4704 additions and 21 deletions
+285
View File
@@ -0,0 +1,285 @@
# 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 |
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
pwsh 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 |