Zwei Beobachtungen aus der laufenden Integration. 1. Der Installer schrieb Geheimnisse weltlesbar SetupWriter benutzte File.WriteAllText ohne Rechteanpassung - unter Linux also die Standardmaske und damit ueblicherweise 644. In genau dieser Datei stehen Lizenzschluessel und Anwendungstoken; jeder Benutzer des Systems konnte sie lesen. Der Lizenz-Cache in StateStore wird aus demselben Grund seit jeher auf 600 gesetzt - der Installer zog nicht nach. Enthaelt ein Ziel mindestens einen geheimen Wert, wird die Datei jetzt auf den eigenen Benutzer beschraenkt. Als geheim gilt type=secret UND source=provision: ein so geholtes Token traegt oft den Typ "string", ist aber genauso schutzbeduerftig. Unter Windows bleibt es beim Profil-ACL. 2. Zielnamen koennen plattformabhaengig unterschiedlich sein %APPDATA%\MeineAnwendung gegen $XDG_CONFIG_HOME/meineanwendung - eine setup.json kannte nur eine Schreibweise. Die kleingeschriebene Form allein traegt, weil NTFS die Schreibweise ignoriert, aber nur solange das Dateisystem tatsaechlich unempfindlich ist; auf APFS mit Beachtung der Schreibweise oder bei groesseren Unterschieden entstuende ein zweites, leeres Verzeichnis neben dem, aus dem die Anwendung liest. Ziele haben deshalb optional fileWindows, fileLinux und fileMacOS; ohne Angabe gilt weiterhin file. Die Dokumentation haelt ausserdem fest, dass der Installer bewusst Klartext schreibt und die Anwendung selbst entscheidet, ob und wie sie ihn danach schuetzt - und dass eine Entschluesselung, die Klartext durchreicht, deshalb kein Altlast-Zweig mehr ist, sondern ein aktiv genutzter Pfad. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
381 lines
15 KiB
Markdown
381 lines
15 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 |
|
|
|
|
### 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.
|
|
|
|
#### Wenn die Schreibweise plattformabhängig ist
|
|
|
|
Konfigurationsordner heißen nicht überall gleich: `%APPDATA%\MeineAnwendung`
|
|
ist unter Windows üblich, `$XDG_CONFIG_HOME/meineanwendung` unter Linux klein.
|
|
|
|
Meistens genügt es, die **kleingeschriebene Form** anzugeben. Sie trifft unter
|
|
Linux exakt und unter Windows ebenfalls, weil NTFS die Schreibweise ignoriert.
|
|
Andersherum funktioniert es nicht — unter Linux entstünde ein zweites, leeres
|
|
Verzeichnis neben dem, aus dem die Anwendung liest.
|
|
|
|
Dieser Kniff trägt aber nur, solange das Dateisystem wirklich unempfindlich
|
|
ist. Auf APFS mit Beachtung der Schreibweise, auf einem per `fsutil`
|
|
umgestellten NTFS-Ordner oder wenn sich die Namen um **mehr als die
|
|
Schreibweise** unterscheiden, braucht es eine eigene Angabe:
|
|
|
|
```json
|
|
{
|
|
"id": "user",
|
|
"file": "meineanwendung/Settings.json",
|
|
"fileWindows": "MeineAnwendung/Settings.json",
|
|
"location": "config"
|
|
}
|
|
```
|
|
|
|
`fileLinux` und `fileMacOS` gibt es ebenso. Ohne Angabe gilt `file`.
|
|
|
|
### Dateirechte
|
|
|
|
Enthält ein Ziel mindestens einen als geheim gekennzeichneten Wert, wird die
|
|
geschriebene Datei auf den eigenen Benutzer beschränkt (`600` unter Linux und
|
|
macOS). Ohne das schriebe .NET mit der Standardmaske, unter Linux üblicherweise
|
|
`644` — und in dieser Datei stehen Lizenzschlüssel und Anwendungstoken.
|
|
|
|
Als geheim gilt jedes Feld mit `type: "secret"` **und** jedes mit
|
|
`source: "provision"` — ein so geholtes Token trägt oft den Typ `string`, ist
|
|
aber genauso schutzbedürftig.
|
|
|
|
Unter Windows geschieht nichts: Dateien in `%APPDATA%` erben die
|
|
Zugriffsrechte des Profils.
|
|
|
|
> **Der Installer verschlüsselt nicht.** Er schreibt Klartext; ob und wie
|
|
> Geheimnisse anschließend geschützt werden, entscheidet die Anwendung — etwa
|
|
> über DPAPI beim ersten Speichern. Das setzt voraus, dass eure
|
|
> Entschlüsselung Klartext unverändert durchreicht, statt an ihm zu scheitern.
|
|
> Wer eine solche Weiche hat, sollte sie im Test als **aktiv genutzt**
|
|
> beschreiben und nicht als Altlast — sonst wird sie eines Tages als tot
|
|
> entfernt und sperrt jede frisch installierte Instanz aus.
|
|
|
|
`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 |
|