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>
15 KiB
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.
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:
wget -qO- https://dc.mhdf.de/installer/install.sh | sh
Ohne Skript geht es genauso:
wget https://dc.mhdf.de/installer/update-agent-linux-x64 -O update-agent
Windows (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.
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
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_secretexistiert 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
{
"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:
"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,dataundhomebeziehen sich auf das Konto, unter dem der Installer läuft. Wird er alsrootoder 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:
{
"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:Bwird zuA__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.
5. Nachträglich einrichten
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 |
curl -X POST https://dc.mhdf.de/api/setup/v1/login \
-H "Content-Type: application/json" \
-d '{"username":"installer","password":"…","hostname":"srv-07"}'
{ "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 |
| Dienstregistrierung | systemd-Unit beziehungsweise Windows-Dienst legt der Installer nicht an |
| Watchdog-Eintrag | Der Monitor entsteht beim ersten Heartbeat, nicht schon bei der Installation |