diff --git a/.env.example b/.env.example index 506df0c..d6dfa38 100644 --- a/.env.example +++ b/.env.example @@ -47,6 +47,25 @@ HERMES_GID=10000 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): +# docker run --rm caddy:2-alpine caddy hash-password --plaintext 'DEIN-PASSWORT' +# Output hier eintragen. +# +# ACHTUNG, GLEICHE DOLLARZEICHEN-FALLE WIE OBEN: ein bcrypt-Hash sieht aus wie +# $2a$14$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS +# Docker Compose interpretiert "$xyz" in .env als Variablen-Referenz und +# ersetzt still durch Leerstring ("WARN ... variable is not set"). FIX: jedes +# einzelne "$" durch "$$" ersetzen, z.B. +# $$2a$$14$$abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRS +# Am schnellsten mit demselben sed-Trick wie beim Dashboard-Hash: +# sed -i '/^CADDY_BASIC_AUTH_HASH=/ s/\$/$$/g' .env +CADDY_BASIC_AUTH_HASH= + # Optional: NUR setzen wenn Du Hermes' EIGENEN OpenAI-kompatiblen API-Server # aktivierst (siehe API_SERVER_HOST/API_SERVER_KEY-Kommentar im # hermes-agent-Service in docker-compose.yml) — z.B. um einen mobilen Client diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 0000000..af9adcd --- /dev/null +++ b/Caddyfile @@ -0,0 +1,26 @@ +# Caddy-Config fuer den Hermes-Dashboard-Reverse-Proxy. +# Keine Secrets hier drin — Username/Passwort-Hash kommen als {$ENV_VAR} +# (Caddyfile-Shorthand fuer {env.ENV_VAR}) aus dem Container-Environment, +# das wiederum aus .env kommt (siehe docker-compose.yml, Service "caddy"). +# +# TLS: bewusst kein "tls internal" (Caddys eigene interne CA, Zertifikate nur +# ~12h gueltig, auto-rotiert) und kein Let's Encrypt (braucht oeffentliche +# Domain + Port 80). Stattdessen ein manuell erzeugtes selbstsigniertes +# Zertifikat mit 100 Jahren Laufzeit, siehe README fuer den openssl-Befehl. +# Einmalig im Browser als Sicherheits-Ausnahme akzeptieren, danach nie wieder +# Renewal-Aerger. +:443 { + tls /certs/dashboard.crt /certs/dashboard.key + + basic_auth { + {$HERMES_DASHBOARD_USER} {$CADDY_BASIC_AUTH_HASH} + } + + reverse_proxy 127.0.0.1:9119 +} + +# Optional-freundlich: wer versehentlich mit http:// statt https:// draufklickt, +# landet trotzdem verschluesselt statt an "connection refused". +:80 { + redir https://{host}{uri} permanent +} diff --git a/README.md b/README.md index 529d933..a2fdc05 100644 --- a/README.md +++ b/README.md @@ -153,26 +153,57 @@ aussen — kein anderer Rechner kommt mehr direkt ran. ``` Danach neu hochziehen: `docker compose up -d --build hermes-agent-dashboard` -5. **Stack starten:** +5. **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: + ```bash + 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. `/CN` ist 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): + ```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: + ```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). + +6. **Stack starten:** ```bash docker compose up -d ``` Baut beim ersten Mal das Hermes-Image aus `hermes-agent-src` (dauert ein paar Minuten), startet dann `hermes-proxy`, `hermes-gateway`, - `hermes-agent` (Gateway/Messaging-Prozess) und `hermes-agent-dashboard`. - Falls `HERMES_DASHBOARD_PASSWORD_HASH` noch leer ist (Schritt 4 noch nicht - gemacht): `hermes-agent-dashboard` bricht beim Start ab, die anderen drei - Services laufen trotzdem — Hash nachtragen, dann - `docker compose up -d hermes-agent-dashboard`. + `hermes-agent` (Gateway/Messaging-Prozess), `hermes-agent-dashboard` und + `caddy`. Falls `HERMES_DASHBOARD_PASSWORD_HASH` oder `CADDY_BASIC_AUTH_HASH` + noch leer sind: der jeweilige Service bricht beim Start ab, die anderen + laufen trotzdem — Werte nachtragen, dann + `docker compose up -d hermes-agent-dashboard caddy`. -6. **Einmalig Claude-CLI-Login** (oeffnet Browser-OAuth mit deiner Claude-Max-Subscription): +7. **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. -7. **Testen, dass der Proxy laeuft:** +8. **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" \ @@ -181,15 +212,17 @@ aussen — kein anderer Rechner kommt mehr direkt ran. ``` Ohne oder mit falschem Bearer-Token gibt's `401 unauthorized`. -8. **Mit Hermes chatten:** +9. **Mit Hermes chatten:** ```bash docker exec -it hermes-agent hermes chat ``` - oder das Dashboard direkt im Browser oeffnen: `http://:9119` - — bindet auf `0.0.0.0`, kein SSH-Tunnel mehr noetig, dafuer fragt es jetzt - den Basic-Auth-Login aus Schritt 4 ab. Traffic ist weiterhin unverschluesseltes - HTTP (siehe Sicherheitshinweis unten) — fuer mehr als "kurz testen" gehoert - ein TLS-Reverse-Proxy davor. + oder das Dashboard im Browser oeffnen: `https://` (Port + 443, kein `:9119` mehr — 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)? @@ -212,20 +245,34 @@ 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 19.07.2026 bewusst auf `0.0.0.0` (Stefans -Entscheidung — ein SSH-Tunnel machte mit dem SSL-Setup Probleme) und ist per -Basic-Auth gesichert (siehe Setup-Schritt 4, Hermes verweigert den 0.0.0.0-Bind -sonst ohnehin hart). Das ist Testzustand, kein Dauerbetrieb-Haerten: das -Passwort geht als HTTP Basic-Auth unverschluesselt raus (kein TLS auf Port -9119), Keys/Config/Sessions liegen dahinter offen. Fuer Dauerbetrieb gehoert -ein TLS-Reverse-Proxy (Caddy/nginx) davor, der Basic-Auth + Zertifikat -uebernimmt — Dashboard-Port dann wieder `127.0.0.1`, nur der Proxy von aussen -erreichbar. +`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. + +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 doch mal direkt nach aussen exposest, ist der Traffic -unverschluesseltes HTTP, kein TLS. Nicht direkt ins offene Internet haengen — -SSH-Tunnel, WireGuard/Tailscale oder einen TLS-Reverse-Proxy davorschalten. +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) @@ -234,7 +281,12 @@ SSH-Tunnel, WireGuard/Tailscale oder einen TLS-Reverse-Proxy davorschalten. - `hermes-data/agent-home/` — Hermes' `~/.hermes`-Aequivalent: Config (`config.yaml`, aus `hermes-agent-config/config.yaml.example` geseedet), Sessions, Memory, Skills, `.env` -- `.env` — Secrets (Token, ggf. API_SERVER_KEY) +- `hermes-data/caddy-certs/` — selbstsigniertes 100-Jahre-Zertifikat + (`dashboard.crt`/`dashboard.key`, Setup-Schritt 5a) — Private Key, niemals + committen +- `hermes-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? diff --git a/docker-compose.yml b/docker-compose.yml index cddcd78..55c9e8d 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -195,10 +195,51 @@ services: # Hash-Erzeugen). - HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin} - HERMES_DASHBOARD_PASSWORD_HASH=${HERMES_DASHBOARD_PASSWORD_HASH:?HERMES_DASHBOARD_PASSWORD_HASH muss in .env gesetzt sein - siehe .env.example. Ohne Auth-Provider verweigert Hermes den 0.0.0.0-Bind komplett.} - # Stefans Wunsch vom 19.07.2026: weiter auf 0.0.0.0, weil SSH-Tunnel/SSL - # Probleme machte — jetzt aber MIT registriertem Basic-Auth-Provider (s.o.), - # sonst kommt der Container laut Hermes' eigenem Auth-Gate gar nicht hoch. - command: ["dashboard", "--host", "0.0.0.0", "--no-open"] + # 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 + # gar keinen registrierten Provider mehr, siehe Fehlermeldung "the auth + # gate engages on non-loopback binds") und der Caddy-Reverse-Proxy unten + # macht TLS + Basic-Auth auf Port 443 nach aussen. HERMES_DASHBOARD_USER/ + # _PASSWORD_HASH bleiben trotzdem gesetzt (schadet nicht, falls der Bind + # doch mal wieder auf 0.0.0.0 wandert). + command: ["dashboard", "--host", "127.0.0.1", "--no-open"] + + # ─── 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. + # + # Zertifikat: bewusst KEIN Let's Encrypt (bräuchte oeffentliche Domain + + # Port-80-Challenge). Stefans Wunsch: ein selbstsigniertes Zertifikat mit + # 100 Jahren Laufzeit, einmalig im Browser als Ausnahme akzeptieren, nie + # wieder um Erneuerung kuemmern. Erzeugung siehe README Setup-Schritt. + # network_mode: host aus demselben Grund wie bei hermes-agent/-dashboard: + # muss 127.0.0.1:9119 (Dashboard) erreichen UND selbst auf dem Host-Port + # 443 lauschen. + caddy: + image: caddy:2-alpine + container_name: hermes-caddy + restart: unless-stopped + network_mode: host + depends_on: + - hermes-agent-dashboard + volumes: + - ./Caddyfile:/etc/caddy/Caddyfile:ro + - ./hermes-data/caddy-certs:/certs:ro + - ./hermes-data/caddy-data:/data + - ./hermes-data/caddy-config:/config + environment: + - HERMES_DASHBOARD_USER=${HERMES_DASHBOARD_USER:-admin} + - CADDY_BASIC_AUTH_HASH=${CADDY_BASIC_AUTH_HASH:?CADDY_BASIC_AUTH_HASH muss in .env gesetzt sein - siehe .env.example} networks: hermes-net: