diff --git a/.env.example b/.env.example index 2e47563..27ef1ab 100644 --- a/.env.example +++ b/.env.example @@ -20,49 +20,17 @@ HERMES_GATEWAY_PORT=8447 HERMES_UID=10000 HERMES_GID=10000 -# NICHT MEHR NOETIG im Normalbetrieb (seit dem Caddy-Umbau, 19.07.2026 spaeter -# Abend): das Dashboard bindet nur noch auf 127.0.0.1, Hermes' eigenes -# Auth-Gate engagiert sich laut eigener Fehlermeldung NUR bei einem 0.0.0.0- -# Bind. Caddy (siehe CADDY_BASIC_AUTH_HASH unten) macht die eigentliche -# Absicherung nach aussen. Einfach leer lassen. -# -# Nur falls der Dashboard-Bind mal wieder auf 0.0.0.0 gestellt wird, ist ein -# Wert hier noetig (sonst verweigert Hermes den Start hart mit "no auth -# providers are registered") — dann zusaetzlich den dashboard:-Block in -# hermes-agent-config/config.yaml.example (bzw. der echten config.yaml) -# einkommentieren. HERMES_DASHBOARD_USER ist Klartext (frei waehlbar), -# HERMES_DASHBOARD_PASSWORD_HASH ist NIEMALS das Klartext-Passwort, sondern -# der Hash daraus. Erzeugen (einmalig, gegen den "hermes-agent"-Container, -# NICHT "hermes-agent-dashboard" — der braucht den Hash schon zum Hochfahren, -# Henne-Ei-Problem): -# docker exec -it hermes-agent python -c \ -# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('DEIN-PASSWORT'))" -# Den Output (nicht das Passwort selbst!) hier eintragen. -# -# ACHTUNG DOLLARZEICHEN-FALLE falls Du's doch brauchst: der erzeugte Hash hat -# das Format "pbkdf2:sha256:$$" - also ZWEI rohe "$"-Zeichen -# drin. Docker Compose interpretiert "$xyz" in .env-Werten selbst als -# Variablen-Referenz und ersetzt nicht aufloesbare Teile stillschweigend -# durch Leerstring (sichtbar als "WARN The "xyz" variable is not set" -# beim "docker-compose up"). Ergebnis: ein verstuemmelter Hash, das -# Dashboard startet trotzdem nicht ("no auth providers are registered"), -# obwohl alles "richtig" eingetragen aussieht. -# FIX: jedes einzelne "$" im Hash durch "$$" ersetzen bevor Du ihn hier -# eintraegst, z.B. aus -# pbkdf2:sha256:260000$f2kwriK6aYVRG5$BRps7npu2Tzg... -# wird -# pbkdf2:sha256:260000$$f2kwriK6aYVRG5$$BRps7npu2Tzg... -# Am schnellsten mit dem sed-Trick: -# sed -i '/^HERMES_DASHBOARD_PASSWORD_HASH=/ s/\$/$$/g' .env +# HERMES_DASHBOARD_USER ist der Login-Username fuer Caddys Basic-Auth vor dem +# Dashboard (siehe CADDY_BASIC_AUTH_HASH unten) — Klartext, frei waehlbar. +# Kein Hermes-eigener Auth-Provider mehr noetig: das Dashboard bindet fest auf +# 127.0.0.1, Hermes' eingebautes Auth-Gate engagiert sich laut eigener +# Fehlermeldung nur bei einem 0.0.0.0-Bind, greift hier also gar nicht. HERMES_DASHBOARD_USER=admin -HERMES_DASHBOARD_PASSWORD_HASH= -# Seit 19.07.2026, spaeter Abend: Caddy (Service "caddy" in docker-compose.yml) -# macht TLS-Termination auf Port 443 + eine EIGENE Basic-Auth-Schicht vor dem -# Dashboard (das selbst nur noch auf 127.0.0.1 bindet). Selber HERMES_DASHBOARD_USER -# wie oben, aber ein EIGENER Hash noetig — Caddy will bcrypt, Hermes' Hash ist -# pbkdf2, die Formate sind nicht austauschbar. Erzeugen (einmalig, gleiches -# Klartext-Passwort wie beim Dashboard-Hash oben verwenden, muss aber nicht): +# Caddy (Service "caddy" in docker-compose.yml) macht TLS-Termination auf Port +# 443 + eine eigene Basic-Auth-Schicht vor dem Dashboard (das selbst nur noch +# auf 127.0.0.1 bindet). Nutzt HERMES_DASHBOARD_USER von oben als Login-Name, +# aber einen eigenen Hash — Caddy will bcrypt. Erzeugen (einmalig): # docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT' # Output hier eintragen. # diff --git a/README.md b/README.md index 23043d7..3e2ac38 100644 --- a/README.md +++ b/README.md @@ -28,9 +28,13 @@ hermes-agent (Nous Research, network_mode: host) 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) +hermes-agent-dashboard (127.0.0.1:9119, Hermes' eigenes Web-UI/Setup, nur + intern erreichbar) + ^ + | reverse_proxy + v + caddy (0.0.0.0:443, TLS + Basic-Auth — einzige Auth-/TLS-Schicht, + siehe Sicherheitshinweis) ``` Warum das Gateway trotzdem noch da ist, obwohl alles auf einer Maschine @@ -83,28 +87,18 @@ aussen — kein anderer Rechner kommt mehr direkt ran. beim Config-Laden aus den Container-Env-Vars expandiert, die kommen aus Deiner `.env`. Kein manuelles Ausfuellen noetig. -4. **Dashboard-Auth: NICHT mehr noetig, ueberspringen.** (Historie: war - zwischenzeitlich Pflicht, solange `hermes-agent-dashboard` mit `--host - 0.0.0.0` lief — Hermes verweigert diesen Bind hart ohne registrierten - Auth-Provider: `Refusing to bind dashboard to 0.0.0.0 — the auth gate - engages on non-loopback binds, but no auth providers are registered`. - Seit Schritt 5 unten (Caddy) bindet das Dashboard aber nur noch auf - `127.0.0.1` — Hermes' Auth-Gate engagiert sich laut eigener Fehlermeldung - NUR bei einem 0.0.0.0-Bind, hier greift's also gar nicht. Caddy macht die - eigentliche Absicherung nach aussen. `HERMES_DASHBOARD_USER`/ - `_PASSWORD_HASH` in `.env` koennen leer bleiben, kein Hash-Erzeugen mehr - noetig.) - - Nur falls Du den Dashboard-Bind mal wieder direkt auf `0.0.0.0` stellst - (z.B. um ohne Caddy zu testen), brauchst Du das wieder — dann in - `hermes-agent-config/config.yaml.example` (bzw. Deiner echten - `config.yaml`) den auskommentierten `dashboard:`-Block reaktivieren und - `HERMES_DASHBOARD_PASSWORD_HASH` in `.env` setzen (Anleitung dazu direkt - im Kommentar über dem Block, inkl. Dollarzeichen-Escaping-Falle). - -5. **TLS-Reverse-Proxy (Caddy) einrichten — macht Port 443 von aussen +4. **TLS-Reverse-Proxy (Caddy) einrichten — macht Port 443 von aussen erreichbar, Dashboard bleibt intern auf `127.0.0.1`:** + Hermes' eigenes Dashboard-Auth-Gate (`dashboard.basic_auth` in + `config.yaml` + `HERMES_DASHBOARD_PASSWORD_HASH`) ist bewusst NICHT mehr + Teil des Setups (aufgeraeumt am 19.07.2026, spaeter Abend/20.07.2026) — + das Gate engagiert sich laut eigener Fehlermeldung ohnehin nur bei einem + `0.0.0.0`-Bind, und der Dashboard-Container bindet fest auf `127.0.0.1` + (siehe `command:` im `hermes-agent-dashboard`-Service). Die komplette + Absicherung nach aussen laeuft ausschliesslich ueber Caddy unten — + ein Auth-Layer statt zwei, weniger Setup-Schritte, weniger Verwirrung. + a. Selbstsigniertes Zertifikat, 100 Jahre gueltig (bewusst kein Let's Encrypt — kein Domain-/Port-80-Zwang, kein Renewal je wieder noetig). **Erzeugt sich automatisch** — kein manueller Schritt mehr noetig: der @@ -128,21 +122,22 @@ aussen — kein anderer Rechner kommt mehr direkt ran. -subj "/CN=hermes-dashboard" ``` - b. Caddy-Basic-Auth-Hash erzeugen (EIGENER Hash, nicht derselbe wie bei - Schritt 4 — Caddy will bcrypt, Hermes' Hash ist pbkdf2): + b. Caddy-Basic-Auth-Hash erzeugen (bcrypt, das einzige Auth-Secret das es + jetzt noch braucht): ```bash docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT' ``` Output in `.env` bei `CADDY_BASIC_AUTH_HASH` eintragen, danach — - **gleiche Dollarzeichen-Falle wie bei Schritt 4** (bcrypt-Hashes haben - auch rohe `$`-Zeichen drin) — escapen: + **Dollarzeichen-Falle** (bcrypt-Hashes haben rohe `$`-Zeichen drin, + Docker Compose interpretiert `$xyz` in `.env`-Werten sonst als eigene + Variablen-Referenz und ersetzt still durch Leerstring) — escapen: ```bash sed -i '/^CADDY_BASIC_AUTH_HASH=/ s/\$/$$/g' .env ``` - `HERMES_DASHBOARD_USER` aus Schritt 4 wird wiederverwendet (dasselbe - Login gilt fuer beide Auth-Schichten). + `HERMES_DASHBOARD_USER` aus Schritt 2 (`.env`) wird als Login-Name + wiederverwendet. -6. **Stack starten:** +5. **Stack starten:** ```bash docker compose up -d ``` @@ -150,18 +145,17 @@ aussen — kein anderer Rechner kommt mehr direkt ran. paar Minuten), startet dann `hermes-proxy`, `hermes-gateway`, `hermes-agent` (Gateway/Messaging-Prozess), `hermes-agent-dashboard` und `caddy`. Falls `CADDY_BASIC_AUTH_HASH` noch leer ist: `caddy` bricht beim - Start ab, die anderen laufen trotzdem — Wert nachtragen (Schritt 5b), dann - `docker compose up -d caddy`. `HERMES_DASHBOARD_PASSWORD_HASH` darf leer - bleiben (siehe Schritt 4). + Start ab, die anderen laufen trotzdem — Wert nachtragen (Schritt 4b), dann + `docker compose up -d caddy`. -7. **Einmalig Claude-CLI-Login** (oeffnet Browser-OAuth mit deiner Claude-Max-Subscription): +6. **Einmalig Claude-CLI-Login** (oeffnet Browser-OAuth mit deiner Claude-Max-Subscription): ```bash docker exec -it hermes-proxy claude ``` Danach liegen die Credentials persistent in `hermes-data/claude-config/` — ueberlebt Container-Restarts. -8. **Testen, dass der Proxy laeuft:** +7. **Testen, dass der Proxy laeuft:** ```bash curl -s http://127.0.0.1:${HERMES_GATEWAY_PORT:-8447}/v1/chat/completions \ -H "Content-Type: application/json" \ @@ -170,7 +164,7 @@ aussen — kein anderer Rechner kommt mehr direkt ran. ``` Ohne oder mit falschem Bearer-Token gibt's `401 unauthorized`. -9. **Mit Hermes chatten:** +8. **Mit Hermes chatten:** ```bash docker exec -it hermes-agent hermes chat ``` @@ -179,7 +173,7 @@ aussen — kein anderer Rechner kommt mehr direkt ran. 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 + Schritt 4b ab. Traffic ist ab hier durchgehend TLS-verschluesselt, kein SSH-Tunnel mehr noetig. ## Mobiler Client (Android)? @@ -203,18 +197,17 @@ Zwei GRUNDVERSCHIEDENE Richtungen, nicht verwechseln: 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: -1. 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_auth` noch durchgesetzt wird. -2. Selbst wenn doch: zwei unabhaengige Schichten sind ohnehin robuster als - eine, falls in Zukunft mal eine davon falsch konfiguriert wird. +`hermes-agent-dashboard` bindet fest auf `127.0.0.1` — von aussen nicht mehr +direkt erreichbar. Davor steht `caddy` (Service in `docker-compose.yml`, +Config in `Caddyfile`): terminiert TLS auf Port 443 mit einem selbstsignierten +100-Jahre-Zertifikat (Setup-Schritt 4a) und erzwingt eine Basic-Auth-Schicht +(Setup-Schritt 4b, bcrypt-Hash in `CADDY_BASIC_AUTH_HASH`). Caddy ist die +EINZIGE Auth-/TLS-Schicht — Hermes' eigenes Dashboard-Auth-Gate +(`dashboard.basic_auth`/`HERMES_DASHBOARD_PASSWORD_HASH`) wurde bewusst +komplett rausgenommen (aufgeraeumt 20.07.2026): es engagiert sich laut eigener +Fehlermeldung ohnehin nur bei einem 0.0.0.0-Bind, der Dashboard-Container +bindet aber fest auf 127.0.0.1 — ein zweiter, nie greifender Auth-Layer haette +nur Setup-Schritte und Verwirrung gekostet, ohne echten Sicherheitsgewinn. 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 @@ -222,7 +215,7 @@ 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 +akzeptierst (Details siehe Setup-Schritt 8). Wer eine "echte", warnungsfreie CA-Kette will, braucht dafuer eine oeffentliche Domain + Let's Encrypt — war hier bewusst nicht der Plan. @@ -240,7 +233,7 @@ Caddy-Reverse-Proxy davorschalten. (`config.yaml`, aus `hermes-agent-config/config.yaml.example` geseedet), Sessions, Memory, Skills, `.env` - `hermes-data/caddy-certs/` — selbstsigniertes 100-Jahre-Zertifikat - (`dashboard.crt`/`dashboard.key`, Setup-Schritt 5a) — Private Key, niemals + (`dashboard.crt`/`dashboard.key`, Setup-Schritt 4a) — Private Key, niemals committen - `hermes-data/caddy-data/`, `hermes-data/caddy-config/` — Caddys eigener Runtime-State (nicht von Dir angefasst) diff --git a/docker-compose.yml b/docker-compose.yml index e6e2b4f..87d3d06 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -187,18 +187,13 @@ services: environment: - HERMES_UID=${HERMES_UID:-10000} - HERMES_GID=${HERMES_GID:-10000} - # NICHT MEHR PFLICHT seit dem Caddy-Umbau (19.07.2026, spaeter Abend): - # Hermes' eigenes Auth-Gate "engagiert sich" laut seiner eigenen - # Fehlermeldung NUR bei einem 0.0.0.0-Bind ("the auth gate engages on - # non-loopback binds"). Der Service unten bindet per `command:` auf - # 127.0.0.1 — das Gate greift also gar nicht, Caddy (siehe unten) macht - # die eigentliche Absicherung nach aussen (TLS + eigene Basic-Auth). - # HERMES_DASHBOARD_PASSWORD_HASH ist daher optional: leer lassen ist ok, - # kein Hash-Erzeugen mehr noetig fuer den Normalbetrieb. Nur falls der - # Dashboard-Bind mal wieder auf 0.0.0.0 wandert, braucht's wieder einen - # Wert (siehe .env.example fuer die Erzeugung). + # HERMES_DASHBOARD_USER wird hier NICHT fuer Hermes' eigenes Auth-Gate + # gebraucht (das Gate engagiert sich laut eigener Fehlermeldung nur bei + # einem 0.0.0.0-Bind, der Service unten bindet aber fest auf 127.0.0.1 + # per `command:`) — steht nur der Vollstaendigkeit halber mit drin, + # tatsaechlich genutzt wird die Variable von Caddy (siehe unten) fuer + # dessen Basic-Auth. Absicherung nach aussen laeuft komplett ueber Caddy. - HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin} - - HERMES_DASHBOARD_PASSWORD_HASH=${HERMES_DASHBOARD_PASSWORD_HASH:-} # Kurskorrektur 19.07.2026, spaeter Abend: Stefan wollte erst 0.0.0.0 # (SSH-Tunnel machte mit SSL Probleme), jetzt aber die richtige Loesung: # Dashboard bleibt auf 127.0.0.1 (Hermes' eigenes Auth-Gate verlangt dann @@ -210,16 +205,16 @@ services: # ─── Caddy (TLS-Reverse-Proxy + Basic-Auth vor dem Dashboard) ────────── # Stefans Entscheidung 19.07.2026: Dashboard bleibt intern auf 127.0.0.1, - # Caddy uebernimmt TLS-Termination auf Port 443 UND eine zweite Basic-Auth- - # Schicht davor. Der zweite Punkt ist kein Overkill, sondern noetig: Hermes' - # eigenes Auth-Gate "engagiert" laut seiner eigenen Fehlermeldung NUR bei - # einem 0.0.0.0-Bind. Auf 127.0.0.1 (das Caddy hier anspricht) ist unklar/ - # nicht garantiert, ob dashboard.basic_auth trotzdem noch durchgesetzt wird - # - deshalb sichert Caddy selbst zusaetzlich ab, statt sich darauf zu - # verlassen. Nutzt denselben HERMES_DASHBOARD_USER wie Hermes (aus .env), - # aber einen EIGENEN Hash: Caddys basic_auth will bcrypt, Hermes' Hash ist - # pbkdf2 — Formate sind nicht kompatibel, siehe .env.example fuer die - # Erzeugung von CADDY_BASIC_AUTH_HASH. + # Caddy uebernimmt TLS-Termination auf Port 443 UND die Basic-Auth davor. + # Caddy ist die EINZIGE Auth-/TLS-Schicht (aufgeraeumt 20.07.2026): Hermes' + # eigenes Dashboard-Auth-Gate (dashboard.basic_auth) wurde komplett entfernt, + # weil es sich laut eigener Fehlermeldung ohnehin nur bei einem 0.0.0.0-Bind + # engagiert — der Dashboard-Container bindet aber fest auf 127.0.0.1, das + # Gate haette hier nie gegriffen. Ein zweiter, nie greifender Auth-Layer + # haette nur Setup-Schritte gekostet, kein Sicherheitsgewinn. Nutzt + # HERMES_DASHBOARD_USER (aus .env) als Login-Name + einen eigenen bcrypt- + # Hash in CADDY_BASIC_AUTH_HASH (Caddy verlangt bcrypt, siehe .env.example + # fuer die Erzeugung). # # Zertifikat: bewusst KEIN Let's Encrypt (bräuchte oeffentliche Domain + # Port-80-Challenge). Stefans Wunsch: ein selbstsigniertes Zertifikat mit diff --git a/hermes-agent-config/config.yaml.example b/hermes-agent-config/config.yaml.example index a9d0450..bb0e917 100644 --- a/hermes-agent-config/config.yaml.example +++ b/hermes-agent-config/config.yaml.example @@ -32,27 +32,11 @@ model: # (den Hermes intern nutzt) setzt den Header automatisch aus diesem Feld. api_key: "${HERMES_GATEWAY_TOKEN}" -# ─── Dashboard-Auth (standardmaessig AUSKOMMENTIERT) ──────────────────── -# Hermes' eigenes Auth-Gate verweigert den Bind des Dashboards auf 0.0.0.0 -# komplett, 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") — greift aber laut derselben Meldung -# NUR bei einem 0.0.0.0-Bind. Unser docker-compose.yml bindet das Dashboard -# bewusst auf 127.0.0.1 und stellt es per Caddy-Reverse-Proxy (TLS + eigene -# Basic-Auth) nach aussen bereit — das Gate hier engagiert sich dabei gar -# nicht erst, dieser Block ist im Normalbetrieb also NICHT noetig. -# -# Nur falls Du den Dashboard-Bind mal direkt auf 0.0.0.0 stellst (z.B. ohne -# Caddy testen): Block unten einkommentieren + HERMES_DASHBOARD_PASSWORD_HASH -# in .env setzen. Hash EINMALIG erzeugen (NICHT das Klartext-Passwort -# committen oder in .env schreiben — nur den Hash), gegen den "hermes-agent"- -# Container, NICHT "hermes-agent-dashboard" (der braucht den Hash schon zum -# Hochfahren, Henne-Ei-Problem): -# docker exec -it hermes-agent python -c \ -# "from plugins.dashboard_auth.basic import hash_password; print(hash_password('DEIN-PASSWORT'))" -# Ergebnis nach .env als HERMES_DASHBOARD_PASSWORD_HASH eintragen. -# -# dashboard: -# basic_auth: -# username: "${HERMES_DASHBOARD_USER}" -# password_hash: "${HERMES_DASHBOARD_PASSWORD_HASH}" +# Kein dashboard.basic_auth-Block mehr hier: Hermes' eigenes Auth-Gate +# engagiert sich laut eigener Fehlermeldung nur bei einem 0.0.0.0-Bind. Das +# Dashboard bindet in docker-compose.yml fest auf 127.0.0.1 (command:), der +# Caddy-Reverse-Proxy davor (TLS + eigene Basic-Auth) uebernimmt die +# komplette Absicherung nach aussen. Falls der Dashboard-Bind je wieder auf +# 0.0.0.0 gestellt wird, blockt Hermes den Start hart ab ("no auth providers +# are registered") — das ist dann ein Signal, den Bind zurueckzustellen statt +# hier einen Auth-Provider nachzuruesten.