Ein Token für das ganze Dashboard: Das Deployment-Token wird zu einem vollwertigen Credential

Du ziehst das Hermes-Web-Dashboard auf einen internen Server, damit dein Team es im Browser erreichen kann, änderst die Bind-Adresse von 127.0.0.1 auf 0.0.0.0 — und es startet nicht. Im Log steht, es brauche einen Auth-Provider, und bindet nicht. Du hast ein Deployment-Token, injiziert aus deinem Secret Manager, aber nichts behandelt es als Credential.
Die nächste halbe Stunde kommt dir bekannt vor: Du wirst in die Konfiguration von basic auth gedrängt, die Login-Box des Browsers erscheint, Credentials landen in .env, und die Desktop-App meldet „HTTP erreichbar, aber die WebSocket-Verbindung ist fehlgeschlagen“. Ein Schritt zu kurz, und nichts passt zusammen.
Warum es früher scheiterte: ein Gate, das fail-closed arbeitet
Das Auth-Gate des Dashboards wurde im Juni 2026 verschärft: Jedes Binden an eine Nicht-Loopback-Adresse erfordert einen registrierten Auth-Provider, und ohne einen verweigert es das Binden — lieber kein offener Port als ein unbedachter. Dieses Design ist richtig.
Die Lücke lag darin, welche Provider zählten. Nur interaktive taten es: basic auth, ein Nous-Account, selbst gehostetes OIDC. Dabei ist die übliche Form in einem Deployment ein einzelnes geteiltes Token — von einem Secret Manager in eine Datei entschlüsselt und dem Dienst übergeben. Der alte Code ignorierte es nicht nur, er drängte dich auch dazu, einen Satz basic-auth-Credentials in .env zu schreiben, nach dem das Deployment nie gefragt hatte.
Selbst mit provisioniertem Token wiesen beide Beine es ab: Eine REST-Anfrage mit dem Bearer bekam 401 no_cookie, und der Upgrade auf /api/ws bekam ein blankes 403. Die Desktop-App formulierte das als „das Gateway ist per HTTP erreichbar, aber die Live-WebSocket-Verbindung ist fehlgeschlagen“ — verwirrend, denn der HTTP-Check berührt nur das öffentliche /api/status und sieht völlig gesund aus.
Der Fix: Das Token ist selbst ein Provider
Hermes bringt einen gebündelten Provider dashboard_auth/session_token mit. Er hält sich raus: Sobald HERMES_DASHBOARD_SESSION_TOKEN provisioniert ist, registriert er sich, und ohne Token tut er gar nichts.
Zwei Wege, ihn zu provisionieren:
- ein literaler Wert:
HERMES_DASHBOARD_SESSION_TOKEN=<token>; - ein Dateipfad:
HERMES_DASHBOARD_SESSION_TOKEN=file:/run/secrets/hermes-dashboard-token.
Die zweite Form ist das, was Secret Manager brauchen. Ein Pfad kann in einer Environment=-Zeile von systemd stehen, in einer launchd-plist oder in einer von Nix gerenderten Dienstumgebung; das Secret selbst kann das nicht. Die Nix-Module von Hermes exponieren das als backend.sessionTokenFile und rendern es in der file:-Form. Beim Start wird die Datei gelesen (umgebender Whitespace entfernt, BOM toleriert); ist sie unlesbar oder leer, beendet sich der Prozess mit einem expliziten Fehler, statt mit einem leeren Credential weiterzulaufen.
Die Verifikation nutzt einen Vergleich in konstanter Zeit, Antwortzeiten lassen sich also nicht zum Erraten des Tokens verwenden. Beide Beine, die ein Client im Token-Modus nutzt — der REST-Header Authorization: *** und der Parameter ?token= beim Upgrade auf /api/ws — laufen durch dieselbe Naht, sodass ein provisioniertes Token beide autorisiert. Genau das hatte das Feld „Session token“ der Desktop-App immer versprochen.
Eine bewusste Entscheidung: Es gibt keine Login-Seite. /login rendert einen Hinweis, dass dieses Deployment sich mit einem Token authentifiziert, statt eines Buttons, den niemand abschließen kann, und der automatische SSO-Redirect bietet diesen Provider nie an. Token-Modus heißt nicht, dass ein Mensch sich anmeldet; es heißt, dass eine Maschine ein Credential vorzeigt.
Deployment
# 1. ein zufälliges Token mit 32 Byte erzeugen
openssl rand -base64 32 > /etc/hermes/dashboard-token
chmod 600 /etc/hermes/dashboard-token
chown hermes:hermes /etc/hermes/dashboard-token
# 2. dem Dienst einen Pfad übergeben, nicht den Wert
export HERMES_DASHBOARD_SESSION_TOKEN=file:/etc/hermes/dashboard-token
# 3. an eine Nicht-Loopback-Adresse binden (das Gate ist jetzt erfüllbar)
hermes dashboard --host 0.0.0.0 --port 9119
Auf Nix schreibst du die Variable nicht selbst: Die Module nehmen backend.sessionTokenFile, zeigen auf das entschlüsselte Secret und rendern den file:-Eintrag in die Umgebung — der Store sieht nur den Pfad.
Prüfen, ob die Tür wirklich zu ist
# Zustand des Gates und registrierte Provider
curl -s http://YOUR_HOST:9119/api/status | jq '.auth_required, .auth_providers'
# true
# ["session-token"]
# mit Token: 200
curl -s -o /dev/null -w '%{http_code}\n' \
-H "Authorization: Bearer ***" http://YOUR_HOST:9119/api/auth/me
# 200
# ohne Token: das ist der Teil, auf den es ankommt
curl -s -o /dev/null -w '%{http_code}\n' http://YOUR_HOST:9119/api/auth/me
# 401
Für das Remote-Gateway der Desktop-App fügst du dasselbe Token in das Feld „Session token“ ein; es wird sowohl für die HTTP-Probe als auch für den WebSocket-Upgrade verwendet.
Troubleshooting-Tabelle
| Symptom | Ursache | Behebung |
|---|---|---|
| Prozess beendet sich beim Start mit Hinweis auf eine unlesbare oder leere Token-Datei | Falscher file:-Pfad, falsche Rechte oder eine leere Datei |
Die Datei mit dem Account caten, unter dem der Dienst läuft |
auth_providers ist in /api/status leer |
Die Variable hat den Dienstprozess nie erreicht | systemd-Environment=, die plist oder die Container-Env prüfen |
| „HTTP erreichbar, aber die WebSocket-Verbindung scheitert“ | Der HTTP-Check trifft eine öffentliche Route, während /api/ws das Token benötigt — oder das Token wurde rotiert |
Das neue Token eintragen; Rotation bedeutet, das Secret neu zu deployen |
| Du willst basic auth oder OIDC für das Gate und das Token nur intern | Der Provider ist standardmäßig aktiv | dashboard.session_token_auth.enabled: false setzen |
Der Opt-out ausgeschrieben:
dashboard:
session_token_auth:
enabled: false
Wann man es nicht einsetzen sollte
- Ein Dashboard, das mehrere Personen teilen. Hinter diesem Token steckt keine Benutzeridentität — wer es hält, ist Administrator. Wenn du wissen musst, wer was geändert hat, nutze OIDC (Authentik, Keycloak, Zitadel, Authelia, Auth0, Okta und Verwandte) oder den Nous-Provider.
- Alles, was aus dem Internet erreichbar ist. Ein langlebiges Bearer-Token ist einen Leak von Vollzugriff entfernt, und Rotation bedeutet, jeden Client zu aktualisieren. Bevorzuge interaktives Login bei öffentlichen Deployments.
- Lokale Single-User-Setups. Ein Loopback-Bind braucht überhaupt keinen Provider; Desktop-App und lokale SPA nutzen das interne Token. Füge keine Konfiguration hinzu, die du nicht brauchst.
Eine Betriebsregel: Behandle das Token wie ein Passwort. Halte es in einem Secret Manager, nie in einem Image oder Repository, und rotiere, indem du das Secret neu deployst statt eine Umgebungsvariable zu bearbeiten — jede Client-Kopie wird damit auf einmal ungültig, was sich als diese rätselhafte Meldung „WebSocket schlägt fehl“ zeigt.
Für den Hintergrund des Dashboard-Gates lies Dashboard-Login-Härtung. Wenn dein Symptom Desktop-Clients sind, die sich Tokens teilen, sieh dir die Kollision bei Session-Tokens an, und für Auth-Formen über einen Tunnel OAuth über SSH. Flags und Port-Defaults stehen auf der hermes dashboard Referenzseite.
Release-Status
Der Provider ist am 28. Oktober 2026 in main gemerged (PR #146139, aufgebaut auf der Datei-Auflösung aus #146093 und dem Nix-Rendering aus #146104). Das aktuelle Release v0.22.5 (23. Oktober 2026) enthält ihn noch nicht.
Ein Token, beide Beine, keine Login-Seite — ein internes Deployment muss sich kein Passwortsystem mehr ausdenken, nur um durch das Gate zu kommen.