Stefans Entscheidung: 0.0.0.0-Bind mit unverschluesseltem Basic-Auth war nur Testzustand. Jetzt bindet hermes-agent-dashboard wieder auf 127.0.0.1, ein neuer Caddy-Service (network_mode: host) terminiert TLS auf 443 mit einem manuell erzeugten selbstsignierten Zertifikat (100 Jahre Laufzeit, kein Let's-Encrypt-Domain-Zwang) und erzwingt eine zweite, eigene Basic-Auth-Schicht (bcrypt-Hash in CADDY_BASIC_AUTH_HASH, getrennt von Hermes' pbkdf2-Hash) - noetig weil Hermes' eigenes Auth-Gate laut Fehlermeldung nur bei 0.0.0.0-Binds "engagiert". README + .env.example um die Setup-Schritte (Cert-Erzeugung, Caddy-Hash, $-Escaping-Falle) ergaenzt.
Hermes Agent — Claude-Max-Proxy + Hermes selbst, alles auf einer Maschine
Repliziert den Proxy-Trick, den wir fuer ARIA nutzen: claude-max-api-proxy
(npm) wrappt die Claude Code CLI — angemeldet mit deiner Claude-Max-
Subscription — als OpenAI-kompatiblen /v1/chat/completions-Endpoint.
Hermes Agent (Nous Research,
CLI/Desktop-Agent) redet darueber mit Claude, ohne API-Kosten — laeuft ueber
die Subscription, nicht ueber Anthropic-API-Billing.
Entscheidung (Stefan): Hermes Agent selbst laeuft auf DERSELBEN Maschine
wie dieser Proxy-Stack — alles was zu Hermes gehoert auf eine Box, minimale
Latenz, nur ein Rechner zum Pflegen. ARIA bleibt komplett unangetastet
(eigenes Docker-Netz hermes-net, eigenes Claude-CLI-Login-Volume, kein
gemeinsamer Container, laeuft ja ohnehin auf einer anderen Maschine als
Hermes).
Architektur
hermes-agent (Nous Research, network_mode: host)
| HTTP, Authorization: Bearer <TOKEN>, http://localhost:8447/v1
v
hermes-gateway (Port 8447, NUR auf 127.0.0.1 gebunden)
| prueft Bearer-Token, sonst 401
v
hermes-proxy (Port 3456, NUR docker-intern im hermes-net erreichbar)
| claude-max-api-proxy -> Claude Code CLI (Claude-Max-Login)
v
Anthropic (ueber deine Claude-Max-Subscription)
hermes-agent-dashboard (0.0.0.0:9119, Hermes' eigenes Web-UI/Setup, per
Basic-Auth gesichert seit 19.07.2026 — Stefans
Entscheidung, siehe Sicherheitshinweis)
Warum das Gateway trotzdem noch da ist, obwohl alles auf einer Maschine
laeuft: hermes-proxy selbst hat keine Authentifizierung (genau wie ARIAs
Proxy) — jeder andere Container im selben Docker-Host koennte sonst
unbemerkt Deine Claude-Max-Subscription mitbenutzen. Das Gateway
(proxy-patches/gateway.js, pure Node, keine Extra-Deps) prueft den
Authorization: Bearer <TOKEN>-Header gegen ein Shared Secret, bevor
irgendwas an hermes-proxy weitergeleitet wird. Seit "alles auf einer
Maschine" bindet das Gateway nur noch auf 127.0.0.1, nicht mehr nach
aussen — kein anderer Rechner kommt mehr direkt ran.
Was uebernommen wurde (1:1 aus ARIAs Proxy-Setup)
- Sed-Patches an der npm-CLI-Bridge (host-binding,
--dangerously-skip-permissions, echter--system-prompt-Kanal, 24h-Timeout, Prompt via stdin) — generische Infra-Fixes, nicht ARIA-spezifisch. - Adapter-Patches (
proxy-patches/openai-to-cli.js,cli-to-openai.js) — die OpenAI-tools<-><tool_call>-Text-Bruecke. Generisch, 1:1 uebernommen. - NICHT uebernommen: ARIAs
routes.js-Patch (Live-Tool-Stream/Not-Aus fuers Diagnostic-UI, ARIA-Bridge-spezifisch). Stattdessen gibt's hier das eigenegateway.jsfuer die Auth, die ARIA in der Form nicht braucht.
Setup
-
Hermes-Agent-Sourcecode daneben klonen (das offizielle Docker-Image liegt nicht auf Docker Hub, sondern wird per
build:aus dem Repo gebaut):cd /pfad/zum/deploy-ziel git clone https://github.com/NousResearch/hermes-agent.git hermes-agent-src(dieser Clone ist in
.gitignore— bleibt lokal, wandert nicht in unser Repo) -
.envanlegen:cp .env.example .env openssl rand -hex 32 # Ausgabe in .env bei HERMES_GATEWAY_TOKEN eintragen -
Model-Config fuer Hermes seeden (einmalig, VOR dem ersten Start — danach fasst Hermes die Datei nicht mehr an):
mkdir -p hermes-data/agent-home cp hermes-agent-config/config.yaml.example hermes-data/agent-home/config.yamlDie Datei zeigt Hermes per
provider: customauf unserenhermes-gateway(http://localhost:${HERMES_GATEWAY_PORT}/v1) —${HERMES_GATEWAY_PORT}/${HERMES_GATEWAY_TOKEN}werden von Hermes selbst beim Config-Laden aus den Container-Env-Vars expandiert, die kommen aus Deiner.env. Kein manuelles Ausfuellen noetig. -
Dashboard-Auth einrichten (Pflicht seit 19.07.2026 —
hermes-agent-dashboardlaeuft mit--host 0.0.0.0und Hermes verweigert diesen Bind hart, wenn kein Auth-Provider registriert ist:Refusing to bind dashboard to 0.0.0.0 — the auth gate engages on non-loopback binds, but no auth providers are registered. Ohne diesen Schritt kommt der Dashboard- Container beim naechsten Schritt gar nicht hoch):# HERMES_DASHBOARD_USER in .env ist Klartext, HERMES_DASHBOARD_PASSWORD_HASH # ist NIEMALS das Klartext-Passwort, sondern der Hash daraus. Erzeugen kannst # Du den erst NACHDEM Schritt 5 unten (docker compose up -d) mindestens # hermes-agent hochgefahren hat -- gegen "hermes-agent", NICHT # "hermes-agent-dashboard" (der braucht den Hash schon zum Start, Henne-Ei): docker exec -it hermes-agent python -c \ "from plugins.dashboard_auth.basic import hash_password; print(hash_password('DEIN-PASSWORT'))"ACHTUNG DOLLARZEICHEN-FALLE: der Hash hat das Format
pbkdf2:sha256:<n>$<salt>$<hash>— zwei rohe$-Zeichen drin. Docker Compose interpretiert$xyzin.env-Werten selbst als Variablen- Referenz und ersetzt nicht aufloesbare Teile stillschweigend durch Leerstring (sichtbar alsWARN The "xyz" variable is not setbeimdocker-compose up). Ohne Escaping kommt im Container ein verstuemmelter Hash an und das Dashboard bleibt bei "no auth providers are registered" haengen, obwohl alles "richtig" eingetragen aussieht. Jedes$im Hash durch$$ersetzen, bevor Du ihn in.enveintraegst (z.B.260000$abc$def→260000$$abc$$def). Anfuehrungszeichen um den Wert helfen dabei NICHT — Compose expandiert$in.env-Werten unabhaengig davon ob sie gequotet sind.Am einfachsten den Hash direkt per
sedin der Zeile escapen, statt ihn von Hand nochmal abzutippen (verdoppelt NUR die$in genau dieser einen Zeile):sed -i '/^HERMES_DASHBOARD_PASSWORD_HASH=/ s/\$/$$/g' .envZur Kontrolle, ob noch unverdoppelte
$uebrig sind (sollte leer sein):grep -o '\$[^$]' .envOutput (den escapeten Hash, nicht das Passwort) in
.envbeiHERMES_DASHBOARD_PASSWORD_HASHeintragen,HERMES_DASHBOARD_USERnach Belieben setzen (Defaultadmin). Derdashboard.basic_auth-Block dazu liegt schon fertig inhermes-agent-config/config.yaml.example(Platzhalter, die Hermes selbst aus den Container-Env-Vars expandiert — kein manuelles Ausfuellen noetig, siehe Schritt 3).WICHTIG, wenn Du Schritt 3 schon VOR dem 19.07.2026 gemacht hast: Schritt 3 kopiert
config.yaml.examplenur EINMALIG nachhermes-data/agent-home/config.yaml— kein automatischer Re-Sync danach. Derdashboard:-Block wurde erst nachtraeglich in die.example-Datei ergaenzt, landet also NICHT von selbst in einer bereits existierendenconfig.yaml. Ohne ihn bleibt Hermes bei "no auth providers are registered" haengen, egal wie sauber.envescaped ist (weil Hermes den Block ja gar nicht sieht). Pruefen:grep -A3 '^dashboard:' hermes-data/agent-home/config.yamlKommt nichts zurueck → Block manuell anhaengen:
cat >> hermes-data/agent-home/config.yaml <<'EOF' dashboard: basic_auth: username: "${HERMES_DASHBOARD_USER}" password_hash: "${HERMES_DASHBOARD_PASSWORD_HASH}" EOFDanach neu hochziehen:
docker compose up -d --build hermes-agent-dashboard -
TLS-Reverse-Proxy (Caddy) einrichten — macht Port 443 von aussen erreichbar, Dashboard bleibt intern auf
127.0.0.1:a. Selbstsigniertes Zertifikat erzeugen, 100 Jahre gueltig (bewusst kein Let's Encrypt — kein Domain-/Port-80-Zwang, kein Renewal je wieder noetig). Einmalig auf der Maschine:
mkdir -p hermes-data/caddy-certs openssl req -x509 -nodes -newkey rsa:4096 \ -keyout hermes-data/caddy-certs/dashboard.key \ -out hermes-data/caddy-certs/dashboard.crt \ -days 36500 \ -subj "/CN=hermes-dashboard"-days 36500≈ 100 Jahre./CNist egal (kein Public-DNS-Zwang), kann auch die Server-IP sein wenn Du magst.b. Caddy-Basic-Auth-Hash erzeugen (EIGENER Hash, nicht derselbe wie bei Schritt 4 — Caddy will bcrypt, Hermes' Hash ist pbkdf2):
docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT'Output in
.envbeiCADDY_BASIC_AUTH_HASHeintragen, danach — gleiche Dollarzeichen-Falle wie bei Schritt 4 (bcrypt-Hashes haben auch rohe$-Zeichen drin) — escapen:sed -i '/^CADDY_BASIC_AUTH_HASH=/ s/\$/$$/g' .envHERMES_DASHBOARD_USERaus Schritt 4 wird wiederverwendet (dasselbe Login gilt fuer beide Auth-Schichten). -
Stack starten:
docker compose up -dBaut beim ersten Mal das Hermes-Image aus
hermes-agent-src(dauert ein paar Minuten), startet dannhermes-proxy,hermes-gateway,hermes-agent(Gateway/Messaging-Prozess),hermes-agent-dashboardundcaddy. FallsHERMES_DASHBOARD_PASSWORD_HASHoderCADDY_BASIC_AUTH_HASHnoch leer sind: der jeweilige Service bricht beim Start ab, die anderen laufen trotzdem — Werte nachtragen, danndocker compose up -d hermes-agent-dashboard caddy. -
Einmalig Claude-CLI-Login (oeffnet Browser-OAuth mit deiner Claude-Max-Subscription):
docker exec -it hermes-proxy claudeDanach liegen die Credentials persistent in
hermes-data/claude-config/— ueberlebt Container-Restarts. -
Testen, dass der Proxy laeuft:
curl -s http://127.0.0.1:${HERMES_GATEWAY_PORT:-8447}/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer <HERMES_GATEWAY_TOKEN aus .env>" \ -d '{"model":"sonnet","messages":[{"role":"user","content":"Sag Hallo"}]}'Ohne oder mit falschem Bearer-Token gibt's
401 unauthorized. -
Mit Hermes chatten:
docker exec -it hermes-agent hermes chatoder das Dashboard im Browser oeffnen:
https://<diese-maschine>(Port 443, kein:9119mehr — der ist jetzt nur noch intern). Browser meldet beim ersten Aufruf "Zertifikat nicht vertrauenswuerdig" (weil selbstsigniert, nicht von einer oeffentlichen CA) — einmalig als Ausnahme akzeptieren ("Trotzdem fortfahren" o.ae.), danach fragt Caddy den Basic-Auth-Login aus Schritt 5b ab. Traffic ist ab hier durchgehend TLS-verschluesselt, kein SSH-Tunnel mehr noetig.
Mobiler Client (Android)?
Zwei GRUNDVERSCHIEDENE Richtungen, nicht verwechseln:
- Unser Proxy oben (
hermes-gateway->hermes-proxy): Claude ALS Backend FUER Hermes. Kein Client redet direkt damit ausser Hermes selbst. - Hermes' EIGENER OpenAI-kompatibler API-Server (
API_SERVER_HOST/API_SERVER_KEY, auskommentiert imhermes-agent-Service): Hermes ALS Server FUER einen Chat-Client. Aktivieren +API_SERVER_KEYin.envsetzen, dann kann ein Client wie gpt_mobile oder Maid (Open Source, Android, freierbase_url+Key, kein Messenger — siehe Chat-Verlauf) sich direkt gegen Hermes verbinden. Fuer sowas wie die ARIA-App (Wake-Word, Sprachsteuerung, TTS) braeuchte man dagegen einen eigenen App-Build — kein fertiges Produkt zum Runterladen.
Sicherheitshinweis
hermes-gateway bindet nur auf 127.0.0.1 — von aussen kommt niemand mehr
ran, solange Du nicht selbst was aendertst (z.B. fuer den mobilen API-Server
oben, oder falls Hermes doch mal auf eine zweite Maschine zieht).
hermes-agent-dashboard bindet seit dem 19.07.2026, spaeter Abend, wieder
auf 127.0.0.1 — von aussen nicht mehr direkt erreichbar. Davor steht seitdem
caddy (Service in docker-compose.yml, Config in Caddyfile): terminiert
TLS auf Port 443 mit einem selbstsignierten 100-Jahre-Zertifikat (Setup-Schritt
5a) und erzwingt eine EIGENE Basic-Auth-Schicht (Setup-Schritt 5b, eigener
bcrypt-Hash in CADDY_BASIC_AUTH_HASH — nicht derselbe wie Hermes' pbkdf2-Hash).
Zwei Gruende fuer die doppelte Auth statt nur auf Hermes' eigene zu vertrauen:
- Hermes' Auth-Gate "engagiert" laut eigener Fehlermeldung nur bei einem
0.0.0.0-Bind — auf 127.0.0.1 (das Caddy anspricht) ist nicht garantiert,
dass
dashboard.basic_authnoch durchgesetzt wird. - Selbst wenn doch: zwei unabhaengige Schichten sind ohnehin robuster als eine, falls in Zukunft mal eine davon falsch konfiguriert wird.
Das war vorher (kurzzeitig, testweise) 0.0.0.0 OHNE TLS — Passwort ging als
Basic-Auth im Klartext raus. Ist jetzt behoben: einziger von aussen offene
Port ist 443, durchgehend verschluesselt.
Das Zertifikat ist selbstsigniert (keine oeffentliche CA dahinter) — Browser zeigen beim ersten Aufruf eine Warnung, die Du einmalig als Ausnahme akzeptierst (Details siehe Setup-Schritt 9). Wer eine "echte", warnungsfreie CA-Kette will, braucht dafuer eine oeffentliche Domain + Let's Encrypt — war hier bewusst nicht der Plan.
Generell gilt: wenn Du HERMES_GATEWAY_PORT, API_SERVER_HOST oder den
Dashboard-Port (9119) doch mal direkt nach aussen exposest (statt ueber
Caddy), ist der Traffic unverschluesseltes HTTP, kein TLS. Nicht direkt ins
offene Internet haengen — SSH-Tunnel, WireGuard/Tailscale oder eben den
Caddy-Reverse-Proxy davorschalten.
Verzeichnisse (nicht committet, siehe .gitignore)
hermes-agent-src/— geklonter Hermes-Agent-Sourcecode (Docker-Build-Context)hermes-data/claude-config/— Claude-CLI-Login (Browser-OAuth-Session)hermes-data/agent-home/— Hermes'~/.hermes-Aequivalent: Config (config.yaml, aushermes-agent-config/config.yaml.examplegeseedet), Sessions, Memory, Skills,.envhermes-data/caddy-certs/— selbstsigniertes 100-Jahre-Zertifikat (dashboard.crt/dashboard.key, Setup-Schritt 5a) — Private Key, niemals committenhermes-data/caddy-data/,hermes-data/caddy-config/— Caddys eigener Runtime-State (nicht von Dir angefasst).env— Secrets (Token, ggf. API_SERVER_KEY, Basic-Auth-Hashes)
Wo deployen?
Diese Compose-Datei ist ortsunabhaengig. Stefan deployt sie selbst auf der
Ziel-Maschine (nicht aria-wohnung — komplett getrennt von ARIA). Repo klonen,
die drei Setup-Schritte oben (Hermes-Source klonen, .env, Config seeden),
dann docker compose up -d.