Dashboard hinter Caddy: TLS (100 Jahre selbstsigniert) + eigene Basic-Auth auf Port 443
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.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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://<diese-maschine>: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://<diese-maschine>` (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?
|
||||
|
||||
|
||||
+45
-4
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user