Ergebnis einer beauftragten Sicherheitsbegutachtung. Die WebAuthn- Zeremonie selbst ist nicht angefasst worden - sie war in Ordnung. Alle Befunde lagen DANEBEN: Ein zweiter Faktor ist nur so stark wie der schwaechste Weg zum Konto, und Sitzungen, die aus ihm entstehen, erben seinen Schutz nicht. KRITISCH /api/setup/v1/login prueste auf totp_secret - eine Spalte, die kein Anmeldeweg auswertet - und nicht auf dc_user_credentials. Ein Administratorpasswort genuegte dort fuer ein setup:install-Token ohne Hardwarekey. Neu: Konten mit zweitem Faktor abgewiesen, und die Rollentrennung gilt symmetrisch. Der Docblock behauptete sie seit jeher; erzwungen hat sie nichts. HOCH secondFactorRequired(), accountLockoutSeconds() und lockoutSeconds() fingen jedes Throwable und fielen auf "kein Faktor" bzw. "nicht gesperrt" zurueck. Gefangen wurden damit auch Verbindungsabbrueche und Lock-Timeouts - jeder davon schaltete still fuer alle Konten auf Passwort-allein. Toleriert werden jetzt nur noch 42S02 und 42S22. HOCH Fuenf Aktionen aenderten die Anmeldemittel ohne erneute Bestaetigung. Eine uebernommene Sitzung konnte sich damit dauerhaft einnisten. Neu: Authentifizierung aus den letzten zehn Minuten noetig, geprueft an derselben Stelle, an der auch der Leerlauf sperrt. WEITER - Leerlauf 30 statt 120 Minuten, und GESPERRT statt beendet: AAL2 laesst zur Reaktivierung einen Faktor genuegen. Wer alle 30 Minuten den Hardwarekey vorzeigen muss, laesst ihn stecken. - Migration 022: session_epoch. Ein Passwortwechsel beendet die uebrigen Sitzungen des Kontos, die eigene bleibt. - Kontosperre wirkt auf der zweiten Stufe; Zaehler gegen unbegrenztes Raten im Zwischenzustand. - Drosselung auf login.php (10/60s) und /api/auth/v1/ (30/60s). - Anmeldefehler ununterscheidbar; die IP-Sperre darf benannt bleiben, sie sagt nichts ueber ein Konto. - HSTS, TLS-Zwang VOR allen Routen, CSP. Die CSP traegt wegen 93 Inline-Handlern nicht gegen XSS - das steht so im Kommentar. - session.use_strict_mode, CSRF-Token bei der Anmeldung verworfen, Secure-Flag aus app.url statt aus $_SERVER['HTTPS']. - rpId/expectedOrigin nicht mehr aus dem Host-Header. /api/health meldet unter checks.webauthn_anchor, ob der Anker steht. - Notfalltoken laeuft nach 180 Tagen ab, geprueft nach dem Hashvergleich. - config.example.php ohne echtes Passwort, verify_ssl auf true. - Serverdaten.txt.bak mitgesperrt, dc_login_attempts raeumt auf. BELEGT 188 von 188 Testfaellen gruen, darunter drei neue in der Gruppe "haertung". Die Blockade einer veralteten Sitzung liess sich nicht in der Suite pruefen - sie verlangt, das Zehn-Minuten-Fenster wirklich abzuwarten. Dafuer gibt es ein Werkzeug ausserhalb; es belegt, dass der Formularweg auf den Sperrbildschirm fuehrt, die Regel dabei unveraendert bleibt, der JSON-Weg reauth_required meldet und das Passwort allein wieder oeffnet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
37 KiB
Umstellung auf Version 2.0 — Ablaufplan
Diese Fassung enthält Sicherheitskorrekturen, die das Verhalten der Schnittstellen ändern. Bitte in dieser Reihenfolge vorgehen.
Version 2.1 ergänzt Korrekturen im .NET-SDK und ein neues Feld am Heartbeat. Sie erfordert keine Konfigurationsänderung, aber eine Datenbankmigration und ein Nachziehen der Clients: §14 Umstellung auf 2.1.
Version 2.2 überarbeitet den UpdateService grundlegend: Plattform-Dimension, signierte Releases, Staging-Swap mit Rollback und geschützte Konfigurationsdateien. Vor dem nächsten Release einzuspielen — §15 Umstellung auf 2.2.
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.
1. Vor dem Deployment: Zugangsdaten wechseln
Serverdaten.txt, config/config.php, config/.htpasswd und
scripts/deploy_config.json lagen im Git-Repository. Sie sind jetzt per
.gitignore ausgeschlossen und aus dem Index entfernt — die Git-Historie
enthält sie aber weiterhin. Alle betroffenen Zugangsdaten sind daher als
kompromittiert zu behandeln:
- MySQL-Passwort ändern, danach in
config/config.phpeintragen - FTP-Passwort ändern, danach in
scripts/deploy_config.jsoneintragen .htpasswd-Passwort fürdeployneu setzen- Git-Token
eb429575…widerrufen und neu ausstellen - Admin-Passwort im WebUI ändern (die alte Fassung setzte es bei jedem
Aufruf von
install_db.phpaufAdmin1337!Admin1337!zurück — jeder im Internet konnte das auslösen)
Wenn die Historie bereinigt werden soll, geht das mit
git filter-repo. Das schreibt
alle Commit-Hashes um; bei einem Repository mit mehreren Nutzern vorher abstimmen.
2. Konfiguration ergänzen
config/config.php braucht drei neue Schlüssel unter security. Die
mitgelieferte Datei enthält bereits erzeugte Werte; für eine neue Installation:
cp config/config.example.php config/config.php
openssl rand -hex 32 # je einmal für shared_key, webhook_key, license_key
| Schlüssel | Zweck |
|---|---|
security.shared_key |
Server-zu-Server-Aufrufe: Evaluator-Cron, Migration, Deaktivierung |
security.webhook_key |
HMAC-Signatur ausgehender Webhooks |
security.license_key |
Signatur der Offline-Lizenzdateien (.lic) |
app.debug |
Auf Produktivsystemen false — steuert, ob Exception-Texte ausgeliefert werden |
Der bisherige
shared_key(DC_MASTER_SECURE_TOKEN_2026_x98f) stand im Repository und wurde ersetzt. Wer ihn irgendwo eingetragen hat — etwa für/api/license/v1/deactivate— muss den neuen Wert nachziehen.
3. Deployment
python scripts/deploy.py
Das Skript überträgt unter anderem die neuen Verzeichnisse var/ (Logs) und
die zusätzlichen .htaccess-Dateien in config/, src/ und sql/.
4. Migration ausführen
Im WebUI anmelden, dann System → DB-Migration → Migration jetzt ausführen.
Alternativ über die Kommandozeile:
php public/install_db.php
Oder mit dem Shared Key:
curl -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/install_db.php
Die Migration ist additiv und legt an bzw. korrigiert:
dc_migrations— vermerkt angewendete Versionen, damit nichts doppelt läuftdc_login_attempts— Drosselung fehlgeschlagener Anmeldungendc_webhooks— ausgehende Benachrichtigungen- Bugtracker: Claim/Lease,
client_ref,dedup_key, Code-Kontextfelder, Indizes - Watchdog:
last_state_change_utc,down_since_utc, Zustandunknown - Reparatur von Migration 005 — deren Spalten (
push_id,target_agent,tags) fehlten bisher auf Datenbanken, die aus Migration 004 stammen. Die alte Fassung nutzte dynamisches SQL, dessen Semikolons in String-Literalen vom damaligen Installer als Statement-Ende gelesen wurden; die Fehler wurden stillschweigend verschluckt. - Reparatur der Token-Hashes — die Validierung vergleicht jetzt nur noch den SHA-256-Hash. Die geseedeten Beispiel-Tokens trugen Hashes, die nicht zu ihrem Klartext passten; sie werden korrigiert, damit bestehende Tokens weiterhin funktionieren.
5. Cron für den Watchdog-Evaluator einrichten
Ohne diesen Schritt sind die Monitor-Zustände wertlos. Der Evaluator fehlte bisher vollständig — der Zustand änderte sich nur beim Eintreffen eines Heartbeats, ein ausgefallener Server blieb dauerhaft grün.
Empfohlen: interner Aufruf (seit 2.1)
* * * * * /usr/bin/php /pfad/zum/deploymentcenter/cli/tick.php --quiet
Das entspricht dem früheren watchdog/cli/tick.php. Gegenüber dem HTTP-Weg:
- Kein Schlüssel im Crontab.
pszeigt die Argumente laufender Prozesse jedem Nutzer der Maschine — bei einemcurl -H "Authorization: Bearer …"also auch den Shared Key. - Keine Abhängigkeit von Webserver, TLS oder DNS. Der Evaluator läuft auch dann, wenn der Webserver klemmt — also gerade dann, wenn das Erkennen von Ausfällen wichtig ist.
- Keine überlappenden Läufe. Das Skript sperrt sich per
flockselbst; ein langsamer Vorlauf führt nicht zu doppelten Alarmen.
--quiet gibt nur bei Zustandswechseln und Fehlern etwas aus — sonst erzeugt
ein minütlicher Job 1440 Cron-Mails am Tag. Rückgabewerte: 0 erfolgreich,
1 Fehler, 2 übersprungen (es lief bereits ein Tick).
Alternativ: über die Schnittstelle
Sinnvoll, wenn der Cron auf einer anderen Maschine läuft als die Anwendung:
* * * * * curl -fsS -H "Authorization: Bearer <SHARED_KEY>" https://dc.mhdf.de/api/watchdog/v1/evaluate > /dev/null
Beide Wege rufen denselben Code auf und schreiben denselben Lauf-Vermerk in
watchdog_cron_jobs — nur einen von beiden einrichten.
Solange der Job fehlt, zeigt das WebUI oben einen Warnhinweis mit einer Schaltfläche für einen einmaligen Lauf.
6. Agenten-Tokens ausstellen
Die Ingest-Endpunkte verlangen jetzt zwingend ein Token.
- WebUI → Token-Verwaltung → Master-Token erstellen
- Scopes wählen (für einen Coding-Agenten:
bugtracker:report,bugtracker:read,bugtracker:manage) - Master-Token einmalig kopieren und auf dem Agenten-Rechner als
DC_TOKENhinterlegen — oder den Agenten per/api/tokens/v1/provisionein eigenes Sub-Token ziehen lassen
7. Bestehende Integrationen anpassen
| Betroffen | Was zu tun ist |
|---|---|
Aufrufe von /api/bugtracker/v1/report ohne Token |
Token-Header ergänzen |
Aufrufe von /api/bugtracker/v1/projects ohne Token |
Token-Header ergänzen |
| Skripte, die Releases veröffentlichen | Token mit updateservice:publish ergänzen |
| Auswertung der Antworten | Neues Format: {"status":"success",…} bzw. {"status":"error","error":{"code":…}} |
Watchdog-Agenten mit wd_live_…-Token |
Laufen unverändert weiter |
Clients, die /api/updateservice/v1/check aufrufen |
Unverändert, weiterhin ohne Token |
Clients, die /api/license/v1/validate aufrufen |
Unverändert, weiterhin ohne Token |
8. Zeitzonen
Datenbankzeitstempel liegen jetzt durchgängig in UTC; das WebUI rechnet für
die Anzeige in app.timezone (Europe/Berlin) um. Vorhandene Datensätze wurden
in Serverzeit geschrieben und erscheinen daher einmalig um den Zeitzonenversatz
verschoben. Für Monitoring-Daten ist das ohne Bedeutung, für den Audit-Log
gegebenenfalls beachten.
9. Prüfen, ob alles läuft
curl https://dc.mhdf.de/api/health -H "Authorization: Bearer <SHARED_KEY>"
Erwartet wird "healthy": true, eine leere schema.pending-Liste und ein
checks.evaluator.ok von true.
Zusätzlich stichprobenartig im WebUI prüfen:
- Anmeldung funktioniert
- Projekt anlegen und wieder löschen
- Monitor bearbeiten — das Feld Betriebssystem bleibt nach dem Speichern erhalten (wurde zuvor bei jedem Speichern geleert)
- Bugtracker: „🔍 Details" öffnet den Dialog, „✔" öffnet den Lösen-Dialog (dessen HTML fehlte bisher komplett)
- Token widerrufen und löschen (warf zuvor
HY093) - Lizenz-Aktivierung freigeben (warf zuvor
Class "Audit" not found) - Nach dem Speichern F5 drücken — es entsteht kein zweiter Eintrag mehr
10. Fehler-Stream (Migration 007)
Neu ist eine eigene Schnittstelle für Laufzeitfehler, gedacht für den globalen Exception-Handler einer Anwendung:
POST /api/errors/v1/report
Gespeichert wird in derselben Tabelle wie der Bugtracker — ein zweiter Speicher wäre nur ein zweiter Ort, an dem man suchen müsste. Die Trennung von Rauschen und Signal leisten stattdessen Ignore-Regeln.
Bekannte, harmlose Fehler stummschalten
Im WebUI unter Bugtracker → 🔇 Ignore-Regeln. Ein Treffer bedeutet nicht „wegwerfen": Der Fehler wird weiterhin erfasst und hochgezählt, bleibt aber aus der Übersicht heraus und löst keine Benachrichtigung aus.
Der Zähler ist dabei der eigentliche Zweck. Zu jeder Regel lässt sich eine Alarmschwelle hinterlegen — dass ein bekannter Fehler auftritt, ist normal; dass er plötzlich hundertmal so oft auftritt, bedeutet, dass sich etwas geändert hat.
Eine deaktivierte Vorlage für den Duplicate-Entry-Fall liegt bereits vor; sie lässt sich im WebUI anpassen und einschalten.
Gruppierung
Veränderliche Anteile werden beim Zusammenfassen ausgeblendet: Werte in
Anführungszeichen, die Ziffern enthalten, Speicheradressen, GUIDs, Zeitstempel
und Zeilennummern. Duplicate entry 'MKT-88213' und Duplicate entry 'AA-1'
landen damit in einer Gruppe — ein anderer Unique-Key dagegen nicht.
Hinweis: Migration 007 ändert die Berechnung des Gruppenschlüssels. Bereits erfasste Einträge behalten ihren alten Schlüssel; ein erneut auftretender Fehler legt daher einmalig eine neue Gruppe an. Danach ist der Zustand wieder konsistent.
11. Metrik-Verlauf und Health-Checks
Numerische Werte aus dem Heartbeat-Feld metrics werden jetzt mit Zeitstempel
abgelegt (Aufbewahrung 14 Tage) und lassen sich über
GET /api/watchdog/v1/metrics abfragen. Zuvor wurde metrics_json bei jedem
Heartbeat überschrieben — es gab immer nur den letzten Moment.
Zusätzlich kann eine Anwendung ihren Gesundheitszustand selbst mitschicken:
{ "source": "polytrader-worker", "status": "ok", "interval": 60,
"checks": {
"db": { "ok": true },
"market_feed": { "ok": false, "message": "Letzter Tick vor 14 min" }
} }
Das Deploymentcenter interpretiert die Namen nicht — es liest nur ok und
message. Schlägt eine Prüfung fehl, wird ein als ok gemeldeter Heartbeat auf
warning herabgestuft. Es müssen keine Ports geöffnet werden, der Weg ist
ausgehend.
Neu sind außerdem die Statuswerte stopped und maintenance. Ohne sie erzeugte
jedes geplante Herunterfahren wenige Minuten später einen Fehlalarm — es gab
schlicht keinen Weg, ein beabsichtigtes Ende mitzuteilen:
{ "source": "polytrader-worker", "status": "stopped",
"message": "Dienst planmäßig beendet" }
Der Evaluator lässt solche Monitore in Ruhe, bis wieder ein normaler Heartbeat eintrifft.
12. Abhängigkeitsbewusste Alarmierung
Fällt ein Monitor aus, für den parent_source gesetzt ist, und ist der
übergeordnete Monitor selbst unten, wird der Alarm für das Kind unterdrückt.
Der Zustand bleibt im Dashboard sichtbar.
Vorher erzeugte ein ausgefallener Hypervisor mit zwölf VMs dreizehn Meldungen für ein Problem.
Damit das greift, muss die Hierarchie gepflegt sein — im WebUI unter WatchDog → System-Hierarchie über das Feld Übergeordnete Entität.
13. Optional: Webhooks
Ereignisgesteuerte Benachrichtigung statt Polling. Ziel direkt in der Datenbank eintragen:
INSERT INTO dc_webhooks (name, url, project_slug, events, secret, enabled)
VALUES ('Telegram Alarm', 'https://n8n.example.com/webhook/dc',
NULL, 'bug.critical,monitor.down', 'geheimnis', 1);
Verfügbare Ereignisse: bug.created, bug.critical, bug.resolved,
feature.created, monitor.down, monitor.recovered, release.published,
oder * für alle.
Jede Zustellung trägt eine Signatur:
X-DC-Timestamp: 1754563200
X-DC-Signature: sha256=<hex(hmac_sha256(secret, timestamp + "." + body))>
Nach 20 Fehlversuchen in Folge deaktiviert sich ein Webhook selbst.
14. Umstellung auf 2.1
Diese Fassung korrigiert Fehler im .NET-SDK, ergänzt ein Feld am Heartbeat und vervollständigt die OpenAPI-Beschreibung. Keine Konfigurationsänderung nötig.
14.1 Serverseitig
- Migration 008 ausführen (Spalte
app_versioninwatchdog_monitors). Läuft wie die übrigen über den Migrator, siehe §4. /api/openapi.jsonabrufen und prüfen, dass die Lizenz-Endpunkte und/api/watchdog/v1/event,/events,/statusjetzt enthalten sind.
Bestehende Agenten laufen unverändert weiter; alle neuen Felder sind optional.
14.2 Clientseitig (Deploymentcenter.Client)
Die folgenden Änderungen betreffen jede bestehende Integration:
| Änderung | Was zu tun ist |
|---|---|
unknown_error entfällt |
Wer diesen Status abfängt, prüft stattdessen IsTransient |
| Cache-Rückfall bei jedem HTTP-Fehler | Eigene Behelfslösungen für 429/5xx können entfallen |
cache_ttl_hours wird ausgewertet |
Offline-Gnadenfrist prüfen: Vorgabe je Projekt im WebUI |
app_version ist Parameter |
LicenseClient.DefaultAppVersion beim Start setzen |
X-Watchdog-Key entfällt bei DeactivateAsync |
Nur noch Authorization: Bearer |
BuildInfo.targets erzeugt in $(RootNamespace) |
Import ist jetzt möglich; ggf. using entfernen |
| API-Rückfall der Update-Prüfung liefert vollständige Daten | Eigene Umgehungen können entfallen |
Ausführlich: LICENSE_INTEGRATION_GUIDE §6 und UPDATESERVICE_INTEGRATION_GUIDE §2B/§2C.
14.3 Cache-Schema
state.dat trägt jetzt Schema 3. Schema 2 wird weiterhin gelesen — ein
SDK-Wechsel erzwingt keine erneute Online-Prüfung. Ein Rückschritt auf ein
älteres SDK dagegen schon: das kennt Schema 3 nicht und behandelt den Cache als
Fehltreffer.
15. Umstellung auf 2.2
Diese Fassung behebt eine Reihe von Fehlern im Update-Weg, die zusammengenommen verhindert haben, dass für mehr als eine Plattform ausgeliefert werden konnte — und die im Fehlerfall halb aktualisierte Installationen hinterließen.
Diese Umstellung gehört vor das nächste Release, nicht danach.
15.1 Was sich ändert
| Bereich | Vorher | Jetzt |
|---|---|---|
| Plattformen | nicht vorhanden — win-x64 und linux-x64 überschrieben sich |
eigene Dimension platform, getrennte Ablage |
| Konfigurationsdateien | wurden beim Update überschrieben | preservePatterns schützt sie |
| Ausschlussmuster | nur *.endung und exakte Namen |
echte Globs (**, *, ?) |
| Abbruch beim Anwenden | halb aktualisierte Installation | Backup, automatischer Rollback |
| Entfernte Dateien | blieben für immer liegen | werden aufgeräumt |
| Agent im Paket | überschrieb sich selbst | wird zur Seite gelegt und ersetzt |
| Übergabe an den Agenten | Wettlauf, kein Neustart | PID-Handschlag und --restart |
| Versionsangabe | ungeprüft | wird gegen die Assembly gehalten |
| Paketherkunft | nur SHA256 aus derselben Quelle | RSA-Signatur des Servers |
manifest_json |
Spalte blieb immer leer | wird befüllt, API ist vollwertiger Rückfall |
15.2 Serverseitig
- Migration 009 ausführen (
platform,manifest_signature, neuer Unique-Key). Additiv; bestehende Releases erhaltenplatform = 'any'und bleiben für alle Clients erreichbar. - Signierschlüssel erzeugen und in
config/config.phpeintragen:
openssl genrsa -out /etc/dc/release-signing.pem 2048
chmod 600 /etc/dc/release-signing.pem
chown www-data:www-data /etc/dc/release-signing.pem
'release_private_key' => dc_env('DC_RELEASE_SIGNING_KEY', '/etc/dc/release-signing.pem'),
- Prüfen, dass der öffentliche Schlüssel ausgeliefert wird:
curl https://dc.mhdf.de/api/updateservice/v1/pubkey
Bewusst RSA und kein HMAC. Bei einem HMAC braucht der Prüfende denselben geheimen Schlüssel wie der Signierende. Der Agent läuft auf fremden Systemen; ein dort hinterlegter Schlüssel ließe sich auslesen und zum Signieren beliebiger Pakete verwenden — die Signatur wäre wertlos. Beim Lizenzmodul geht HMAC auf, weil dort der Server prüft.
Ohne Schlüssel bleibt alles funktionsfähig; Releases sind dann unsigniert, und der Agent weist bei jedem Update darauf hin.
15.3 Packager
packager.config.jsonumpreservePatternsergänzen (Vorlage inpackager.config.example.json).- Prüfen, welche Konfigurationsdateien im Publish-Verzeichnis liegen.
Der Packager warnt jetzt von sich aus vor Dateien, die nach Zugangsdaten
aussehen und weder ausgeschlossen noch geschützt sind. Diese Warnung ernst
nehmen: eine
settings.jsonmit Datenbankpasswort, die bisher mitgeliefert wurde, hat beim Update die Konfiguration jedes Zielsystems überschrieben. - Aufrufe um
--platformergänzen:
pack-and-deploy --project myapp --version 1.4.3 --channel prod \
--platform win-x64 \
--publish-dir ./bin/Release/net10.0/win-x64/publish
Ohne --platform versucht der Packager, die Kennung aus dem Publish-Pfad
abzuleiten. Gelingt das nicht, gilt das Release als plattformunabhängig und es
erscheint eine Warnung.
- Die Version wird jetzt gegen die Hauptassembly geprüft. Weicht sie ab,
bricht der Vorgang ab. Das ist der Fall, in dem
<Version>nur in einem der beteiligten Projekte steht — der Wert gehört in dieDirectory.Build.props, damit alle denselben tragen. Notausgang für bewusste Abweichungen:--ignore-version-mismatch.
15.4 Clientseitig
UpdateClient.CheckForUpdateAsync hat einen zusätzlichen Parameter platform
vor dem CancellationToken. Wer den Token bisher positionell übergeben hat,
bekommt einen Übersetzungsfehler — kein stilles Fehlverhalten.
Der Aufruf des Agenten sollte umgestellt werden:
var agentPath = UpdateClient.ResolveAgentPath(); // plattformrichtiger Name
if (agentPath != null && checkResult.UpdateAvailable)
{
UpdateClient.LaunchUpdateAgent(
agentPath: agentPath,
projectId: "myapp",
channel: "prod",
currentVersion: BuildInfo.Version, // für Ziele ohne manifest.json
exitCurrentApp: true); // Neustart und PID-Handschlag
// sind jetzt Vorgabe
}
| Änderung | Wirkung |
|---|---|
ResolveAgentPath() |
Findet update-agent bzw. update-agent.exe — ein fest verdrahteter .exe-Name wird unter Linux nie gefunden |
--restart wird übergeben |
Die Anwendung startet nach dem Update wieder. Bisher blieb sie einfach zu |
--wait-for-pid wird übergeben |
Der Agent wartet auf das Ende der Anwendung, statt über gesperrte Dateien zu kopieren |
--platform wird übergeben |
Es kann kein Paket einer fremden Plattform mehr gezogen werden |
15.5 Den Agenten aus dem Paket nehmen
Liegt update-agent im Publish-Verzeichnis, wandert er ins Paket und wird beim
Update mit ausgeliefert. Das ist zulässig — der Agent legt seine laufende Datei
inzwischen zur Seite und ersetzt sie —, aber es lohnt sich, bewusst zu
entscheiden:
- Mitliefern: Der Agent aktualisiert sich selbst mit. Die alte Fassung bleibt
als
update-agent.exe.dc-oldliegen und wird beim nächsten Start entfernt. - Ausschließen:
"update-agent*"inexcludePatterns. Dann wird der Agent getrennt gepflegt.
15.6 Prüfen
curl "https://dc.mhdf.de/api/updateservice/v1/check?product=myapp&version=0.0.0&channel=prod&platform=win-x64"liefert das Windows-Paket, mitplatform=linux-x64das Linux-Paket.- Ohne
platformkommen ausschließlich Releases mitplatform=any. Das ist Absicht: ein Client, der seine Kennung nicht nennt, soll lieber kein Update bekommen als das falsche. - Im WebUI zeigt die Release-Übersicht eine Spalte Plattform und kennzeichnet unsignierte Releases.
- Ein Update auf einem Testsystem:
appsettings.jsonbehä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
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.
- SDK auf 2.4 heben und
licenseKeyanCheckForUpdateAsyncundLaunchUpdateAgentübergeben. - Ein Release mit dem neuen SDK bauen und veröffentlichen.
- Warten, bis die Installationen dieses Release gezogen haben.
- 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.
Neue Produkte: die Reihenfolge oben gilt für sie nicht.
ReleaseGuardüberspringt Verzeichnisse, die es nicht gibt — für ein Produkt ohne Release ist/releases/<slug>/schlicht nicht vorhanden und damit auch nicht geschützt. Das Verzeichnis entsteht erst mit dem ersten Upload, und der nächstetick.php-Lauf legt den Schutz darin an.Es gibt also kein Zeitfenster, in dem sich ein noch ungeschütztes Release herunterladen ließe, um dann das SDK nachzuziehen. Der erste ausgelieferte Build muss die Zugangsdaten bereits mitbringen — sonst fällt die Tür hinter ihm zu.
Für Neuprodukte lautet die Reihenfolge daher schlicht: SDK 2.4 einbinden,
licenseKeyübergeben, dann das erste Release veröffentlichen.
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
update-agent --project myapp --action update \
--license-key XXXXX-XXXXX-XXXXX-XXXXX-XXXXX
# 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
- Selbsttest im WebUI unter UpdateService → 🔒 Zugangsschutz — er ruft die eigene Paket-Adresse ohne Zugangsdaten ab und erwartet 401. Die Anzeige „GESCHÜTZT" allein beweist nichts: Sie prüft nur, ob die Dateien existieren, nicht ob der Server sie auswertet.
curl -I https://dc.mhdf.de/releases/<produkt>/prod/<rid>/<version>/package.tar.gz→ 401- Mit
-u "$(printf 'lic_%s' "$(printf %s "<schlüssel>" | sha256sum | cut -c1-16)"):<schlüssel>"→ 200 (Benutzername ist die Ableitung, nicht der Schlüssel — siehe §5A) curl -I https://dc.mhdf.de/releases/<produkt>/.htpasswd→ 403https://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.
17. Umstellung auf 2.9
Zwei Dinge kommen dazu: ein Hardwarekey als zweite Stufe der WebUI-Anmeldung und die Roadmap-Tafel. Nur das Erste braucht Aufmerksamkeit — beim Zweiten kann nichts schiefgehen, was sich nicht durch Neuladen beheben ließe.
17.1 Die Reihenfolge, und warum sie zählt
1. Deployment
2. Migration 019 ausführen
3. Anmelden, Hardwarekey registrieren
4. Notfalltoken erzeugen und ausdrucken
Die Pflicht zum zweiten Faktor hängt daran, ob ein Schlüssel registriert ist — nicht an einem Schalter. Das ist Absicht und der Grund, warum diese Reihenfolge ungefährlich ist: Solange kein Schlüssel hinterlegt ist, läuft die Anmeldung genau wie vorher. Ein Schalter, den man vor der Registrierung umlegt, hätte die entgegengesetzte Eigenschaft.
Nach Schritt 3 ist das Passwort allein nicht mehr genug. Deshalb gehört Schritt 4 unmittelbar dahinter und nicht auf die Liste für nächste Woche.
17.2 Der Schlüssel gilt nur für eine Adresse
Ein auf dc.mhdf.de registrierter Yubikey funktioniert auf
dc-dev.mhdf.de nicht, und umgekehrt. Das ist keine Einschränkung der
Umsetzung, sondern der Kern des Verfahrens: Genau deshalb läuft ein
Anmeldeversuch auf einer nachgebauten Seite ins Leere. Auf beiden Instanzen
also je einmal registrieren.
Registriert wird unter System → 👤 Benutzer → Meine Hardwarekeys. Registrieren Sie zwei, wenn Sie zwei haben — der Notfalltoken ist der Ersatzweg, nicht der zweite Schlüssel.
17.3 Der Notfalltoken
Fünf Gruppen zu vier Zeichen, einmalig bei der Erzeugung angezeigt. In der Datenbank liegt nur ein Hash; wer sie liest, hält damit keinen Zweitschlüssel in der Hand. Er gilt für genau eine Anmeldung und ist danach verbraucht — die Meldezeile sagt es Ihnen.
Beim Eintippen zählen nur Buchstaben und Ziffern. Bindestriche und Groß-/Kleinschreibung sind gleichgültig.
17.4 Wenn Schlüssel und Notfalltoken weg sind
Dann hilft nur die Datenbank. Der Weg zurück ist, die Schlüssel des Kontos zu entfernen — danach genügt wieder das Passwort allein:
DELETE FROM dc_user_credentials
WHERE user_id = (SELECT id FROM dc_users WHERE username = '<name>');
17.5 Konto umbenennen
System → 👤 Benutzer kann jetzt auch den Anmeldenamen ändern. Registrierte Hardwarekeys hängen an der Konto-ID und überstehen das; offene Sperrfenster des alten Namens werden dabei aufgeräumt.
17.6 Was sich an der API ändert
- Sitzungsauthentifizierte Schreibzugriffe auf die API verlangen jetzt ein
CSRF-Token (
csrf_tokenim Rumpf oder der KopfX-CSRF-Token). Token-Clients — Agenten, MCP-Adapter,scripts/dc.py— sind nicht betroffen: Sie schicken keinen Cookie. Betroffen sind nur eigene Skripte, die sich auf eine WebUI-Sitzung stützen. roadmap_rankgilt projektweit, nicht je Bereich (#177).movesucht den Nachbarn entsprechend ohne Bereichsfilter, undreorderzählt nicht mehr je Bereich bei 10 neu durch, sondern vertauscht nur die Ränge, die die genannten Einträge ohnehin halten. Wer bisher je Bereich ab 10 vergeben hat, sollte die Reihenfolge einmal durchsehen.
17.7 Prüfen, ob alles läuft
- Anmeldung mit Passwort führt auf die zweite Stufe
- Hardwarekey schließt sie ab
- Notfalltoken schließt sie ebenfalls ab und ist danach verbraucht
- Roadmap → 🧭 Tafel zeigt Spalten, Spuren und Kärtchen
- Ein Klick auf ein Kärtchen öffnet das Panel, Kommentieren funktioniert
python tests/e2e/run.pygegen die Testinstanz: alles grün
18. Umstellung auf 2.9.2 — nur echte Hardware als zweiter Faktor
18.1 Worum es geht
Seit 2.9.0 verlangt die Anmeldung einen Hardwarekey. Was sie nicht verlangte, war, dass es wirklich Hardware ist: Ein Passkey aus einem Passwortverwalter (1Password, Bitwarden, iCloud, Google) ließ sich ebenso registrieren wie ein Yubikey. Für das Deploymentcenter selbst ist das hinnehmbar. Wo ein Sicherheitsprotokoll ausdrücklich Hardware fordert, ist es der Unterschied zwischen einer Regel und einem Wunsch.
Zwei Dinge, die wie eine Prüfung aussehen und keine sind — beide waren in 2.9.0 bereits gesetzt:
authenticatorAttachment: "cross-platform"steuert, was der Browser anbietet. Ein Passwortverwalter meldet sich ebenfalls als cross-platform.response.authenticatorAttachmentsetzt der Client, und der Client ist genau die Instanz, gegen die sich hier nichts beweisen lässt.
Was trägt, ist die Attestation: Der Authenticator unterschreibt bei der Aufnahme mit einem Schlüssel, der ab Werk in seinem Sicherheitselement liegt und es nie verlässt. Das zugehörige Zertifikat muss auf eine hinterlegte Herstellerwurzel ketten.
Ein Zertifikat allein genügt nicht. Jeder kann sich eines ausstellen, es „Yubico U2F EE“ nennen, die AAGUID eines echten YubiKey 5 eintragen und sauber unterschreiben. Genau dieser Fall wurde nachgebaut und gegen den Server gefahren: Er wird abgewiesen — an der Kette, nicht an der Signatur. Was Software nicht herstellen kann, ist eine Kette, die bei Yubicos Wurzel endet.
18.2 Migration
php cli/migrate.php # oder System → 🗄️ DB Migration im WebUI
Migration 021 ergänzt dc_user_credentials um attested, attestation_fmt,
attestation_ca und attestation_root. Sie setzt keine Regel — ohne
Umstellen bleibt das Verhalten von 2.9.1.
18.3 Die drei Stufen
| Stufe | Aufnahme neuer Schlüssel | Anmeldung mit vorhandenen |
|---|---|---|
off |
jeder Authenticator | alle |
register |
nur mit Herstellernachweis | alle |
strict |
nur mit Herstellernachweis | nur nachgewiesene |
Einzustellen unter System → 👤 Benutzer → 🛡️ Welche Schlüssel zugelassen sind.
18.4 Der Weg nach strict — in dieser Reihenfolge
Alle vor 2.9.2 registrierten Schlüssel gelten als nicht nachgewiesen — auch echte Yubikeys. Das ist keine Vorsicht, sondern eine Tatsache des Standards: Bei
attestation: "none"ersetzt der Browser die Gerätekennung durch Nullbytes, bevor der Server sie zu sehen bekommt. Es gab also nie eine Herkunft, nicht einmal eine ungeprüfte.
- Stufe auf
registerstellen. - Den Hardwarekey einmal neu aufnehmen. In der Liste muss danach 🔒 Hardware stehen, mit der erreichten Wurzel.
- Den alten, nicht nachgewiesenen Eintrag entfernen.
- Erst jetzt auf
strict.
Schritt 4 wird abgelehnt, solange das eigene Konto keinen nachgewiesenen Schlüssel hat — eine Einstellung, die sich selbst aussperrt, ist keine Einstellung, sondern eine Falle.
18.5 Wenn doch etwas klemmt
- Der Browser fragt nach Marke und Modell. Das ist der sichtbare Preis von
attestation: "direct"und kein Fehler. Wer ablehnt, liefert keine Attestation und wird abgewiesen. - Ausgesperrt. Der Notfalltoken funktioniert unabhängig von der Regel. Danach unter System → Benutzer die Stufe zurückstellen.
- Ganz ohne WebUI:
Fehlt der Eintrag, gilt wieder
DELETE FROM dc_settings WHERE skey = 'webauthn_hardware_policy';off.
18.6 Andere Hersteller als Yubico
Mitgeliefert sind die drei FIDO-Wurzeln von Yubico
(src/Core/Webauthn/roots/yubico.pem). Weitere Wurzeln in der config.php:
'security' => [
'webauthn_root_files' => ['/etc/dc/solokeys.pem'],
],
Bewusst dort und nicht im WebUI: Eine Vertrauenswurzel über dieselbe Oberfläche austauschbar zu machen, die sie schützt, hieße die Prüfung dort aushebelbar zu machen.
18.7 Prüfen, ob alles läuft
- Migration 021 ist durch (Spalte
attestedindc_user_credentials) - Stufe
register: Der Yubikey wird aufgenommen und zeigt 🔒 Hardware - Stufe
register: Ein Passwortverwalter wird mit einer Meldung abgewiesen, die den Grund nennt strictlässt sich nicht einschalten, solange kein nachgewiesener Schlüssel am eigenen Konto hängt- Nach
strict: Anmeldung mit dem Yubikey funktioniert weiter python tests/e2e/run.py --gruppe hardware: alles grün
19. Umstellung auf 2.9.3 — Ergebnis der Sicherheitsbegutachtung
19.1 Was gefunden wurde
Die WebAuthn-Zeremonie selbst war in Ordnung. Der zweite Faktor ließ sich trotzdem umgehen — nicht in der Zeremonie, sondern daneben:
| Weg | Was möglich war |
|---|---|
/api/setup/v1/login |
Passwort allein genügte für ein setup:install-Token — der Endpunkt prüfte totp_secret, eine Spalte, die kein Anmeldeweg auswertet |
| Datenbankfehler | secondFactorRequired() fing jedes Throwable und antwortete „kein zweiter Faktor" — ein Lock-Timeout schaltete die Anlage still auf Passwort-allein |
| Übernommene Sitzung | Konnte einen eigenen Hardwarekey registrieren, Notfalltokens ausstellen und die Regel abschalten — ohne eine einzige erneute Bestätigung |
Der rote Faden: Ein zweiter Faktor ist nur so stark wie der schwächste Weg zum Konto. Zwei der drei Befunde waren Stellen, an denen ein Kommentar eine Zusicherung behauptete, die der Code nicht einlöste.
19.2 Vor dem Deployment prüfen
app.urlmuss in derconfig.phpgesetzt sein.
rpId() und expectedOrigin() leiten sich nicht mehr aus dem Host-Header ab —
aus diesem Wert entstehen die beiden Anker, gegen die die ganze Zeremonie
geprüft wird. Fehlt app.url und security.webauthn_rp_id, wird der
Hardwarekey abgewiesen.
curl -s https://dc.mhdf.de/api/health | grep -o '"webauthn_anchor":{[^}]*}'
"ok":true — alles in Ordnung. "ok":false — vor dem Deployment eintragen:
'app' => ['url' => 'https://dc.mhdf.de'],
19.3 Migration
php cli/migrate.php # oder System → 🗄️ DB Migration
Migration 022 ergänzt dc_users.session_epoch. Bestehende Anmeldungen bleiben
gültig — eine Sicherheitsverbesserung, die im Moment ihrer Einführung alle
Angemeldeten hinauswirft, wird beim nächsten Mal abgeschaltet.
19.4 Was sich im Arbeitsablauf ändert
- Nach 30 Minuten Untätigkeit wird die Sitzung gesperrt, nicht beendet. Der Sperrbildschirm öffnet mit dem Passwort allein — der Hardwarekey wird erst bei einer neuen Anmeldung wieder verlangt.
- Fünf Aktionen verlangen eine Bestätigung, wenn die letzte Authentifizierung über zehn Minuten zurückliegt: Passwort setzen, Notfalltoken ausstellen, Hardwarekey aufnehmen oder entfernen, die Hardwarekey-Regel umstellen. Es erscheint derselbe Sperrbildschirm.
- Ein Passwortwechsel beendet alle anderen Sitzungen des Kontos. Die eigene bleibt bestehen.
- Die Anmeldung nennt keine Gründe mehr. Falscher Name, falsches Passwort, deaktiviertes und gesperrtes Konto ergeben dieselbe Meldung. Die Wartezeit steht darin, der Grund nicht — gesperrt wird nur, was existiert, und eine eigene Meldung dafür bestätigte die Existenz des Namens.
19.5 Was Sie selbst erledigen müssen
- Rocket.Chat-Passwort rotieren. In
config/config.example.phpstand ein echt aussehendes Passwort — die Beispieldatei liegt im Repository, und damit lag es dort auch. Ersetzt durch''; der Wert kommt jetzt ausDC_RC_PASS. verify_sslprüfen. Die Vorgabe der Beispieldatei stand auffalseund steht jetzt auftrue. Wer den alten Wert übernommen hat, prüfte Zertifikate nicht.- Installationskonto anlegen, falls Sie die Setup-API mit einem
Administratorkonto benutzt haben. Rolle
installer, unter System → Benutzerkonten. - Notfalltoken erneuern, wenn er älter als 180 Tage ist. Er läuft jetzt ab; die Oberfläche zeigt die Restlaufzeit.
19.6 Was bewusst offen bleibt
| Punkt | Warum nicht jetzt |
|---|---|
CSP ohne 'unsafe-inline' |
Verlangt das Herauslösen von 93 Inline-Handlern und mehreren tausend Zeilen Inline-Skript aus index.php. Ein eigenes Vorhaben, kein Header. Bis dahin schützt die CSP nicht gegen XSS — sie schützt gegen fremde Skriptquellen, Einbettung und umgebogene Formularziele. |
| Rollenmodell im WebUI | ApiAuth gibt einer Session jeden Scope ohne Prüfung; jeder WebUI-Benutzer ist Volladministrator. Ein echtes Berechtigungsmodell ist ein Vorhaben für sich. |
| Document Root | Der gesamte Quellbaum inklusive config/ hängt am Web und wird von mod_rewrite plus Verzeichnis-.htaccess gehalten. Richtig wäre public/ als Root — das ist eine Änderung an der Hosting-Konfiguration, nicht am Code. |
19.7 Prüfen, ob alles läuft
/api/health→checks.webauthn_anchor.okisttrue- Anmeldung mit Passwort und Hardwarekey funktioniert
curl -I https://dc.mhdf.de/login.phpzeigtStrict-Transport-SecurityundContent-Security-Policycurl -I http://dc.mhdf.de/login.phpantwortet mit301aufhttps://- Setup-API mit einem Administratorkonto →
403 installer_account_required python tests/e2e/run.pygegen die Testinstanz: alles grün