Compare commits

..
Author SHA1 Message Date
william 143be8200e Drop the Traefik-level basic auth layer — keep only Hermes's own login
By request: one auth layer, not two. Note this isn't really removing a
layer I added on top of nothing — Hermes's own gate is mandatory and
can't be disabled while the dashboard is reachable through a separate
Traefik container (it fails closed at startup on any non-loopback bind
without a configured auth provider). The only thing actually optional
was the Traefik-level middleware, so that's what comes out; Traefik now
just does TLS termination + routing.
2026-08-23 17:02:59 +00:00
william 9739676409 Add Hermes web dashboard at hermes.apps.williamturner.eu
Enabled via HERMES_DASHBOARD=1 (supervised in-container alongside the
gateway, per docs/user-guide/docker.md), bound to 0.0.0.0:9119 so Traefik
can reach it. Two independent auth layers, not one:

1. Traefik basicauth middleware in front of the whole route.
2. Hermes's own basic-auth gate (mandatory once the bind is non-loopback).

Hermes's docs explicitly call basic-auth-alone "not suitable for direct
public-internet exposure" and cite a real June 2026 incident where
internet scanners reached exposed dashboards and drove agents into
planting SSH-key backdoors — hence the extra Traefik-level gate rather
than relying on Hermes's own login page alone.

Also fixes: the htpasswd hash for Traefik's basicauth needs its literal
'$' characters escaped as '2824147' in .env, or docker compose's own variable
interpolation corrupts it (mistook '' for further
 references). Also switched the hash from Python's default SHA-512
crypt ('$...') to apr1 ('$...', via openssl passwd -apr1) —
Traefik's basicauth middleware doesn't accept SHA-512-crypt.

Also re-adds Hermes's OPENAI_BASE_URL/OPENAI_API_KEY routing through the
local litellm gateway (instead of OPENROUTER_API_KEY direct) — this was
part of the now-abandoned PR #12 and never actually landed on main.
2026-08-23 16:21:50 +00:00
2 changed files with 37 additions and 1 deletions
+10
View File
@@ -5,6 +5,7 @@
# --- domain / TLS --- # --- domain / TLS ---
MATRIX_SERVER_NAME=matrix.apps.williamturner.eu MATRIX_SERVER_NAME=matrix.apps.williamturner.eu
AGENT_HOSTNAME=agent.apps.williamturner.eu AGENT_HOSTNAME=agent.apps.williamturner.eu
HERMES_DASHBOARD_HOSTNAME=hermes.apps.williamturner.eu
# Set to true ONLY for the first-boot window while creating the bot account, # Set to true ONLY for the first-boot window while creating the bot account,
# then back to false (or unset) and redeploy. See README. # then back to false (or unset) and redeploy. See README.
MATRIX_ALLOW_REGISTRATION=false MATRIX_ALLOW_REGISTRATION=false
@@ -37,6 +38,15 @@ HERMES_MATRIX_ACCESS_TOKEN=
# (internal network only, not published anywhere). # (internal network only, not published anywhere).
HERMES_API_SERVER_KEY= HERMES_API_SERVER_KEY=
# --- hermes web dashboard (hermes.apps.williamturner.eu) ---
# Hermes's own login gate — mandatory once its dashboard is bound non-loopback (needed
# for Traefik, a separate container, to reach it at all), so this can't be turned off
# while the dashboard is reachable through Traefik.
HERMES_DASHBOARD_USERNAME=william
HERMES_DASHBOARD_PASSWORD=
# 32+ random bytes — `openssl rand -base64 32`
HERMES_DASHBOARD_SECRET=
# --- portainer (GitOps redeploy) --- # --- portainer (GitOps redeploy) ---
PORTAINER_STACK_WEBHOOK_URL= PORTAINER_STACK_WEBHOOK_URL=
+27 -1
View File
@@ -77,13 +77,28 @@ services:
# rooms (DMs to it would respond unprompted, per Hermes's own default behavior). # rooms (DMs to it would respond unprompted, per Hermes's own default behavior).
MATRIX_ALLOWED_USERS: ${MATRIX_HUMAN_USER_ID} MATRIX_ALLOWED_USERS: ${MATRIX_HUMAN_USER_ID}
MATRIX_REQUIRE_MENTION: "true" MATRIX_REQUIRE_MENTION: "true"
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} # Routed through the local litellm gateway, not OpenRouter directly — one place to
# hold the OpenRouter credential and swap models. Does NOT grant Hermes access to
# the Claude subscription (Anthropic-side restriction, proven earlier — the
# subscription only works through the real `claude` CLI binary, which Hermes isn't).
OPENAI_BASE_URL: http://litellm:4000/v1
OPENAI_API_KEY: ${LITELLM_MASTER_KEY}
# Left disabled: Hermes itself warns that a network-reachable API server combined # Left disabled: Hermes itself warns that a network-reachable API server combined
# with the default unsandboxed ('local') terminal backend gives any caller full # with the default unsandboxed ('local') terminal backend gives any caller full
# terminal/file access within the container. Matrix is the actual interface in use; # terminal/file access within the container. Matrix is the actual interface in use;
# re-enable (API_SERVER_HOST: 0.0.0.0) only alongside terminal.backend: docker if # re-enable (API_SERVER_HOST: 0.0.0.0) only alongside terminal.backend: docker if
# claude-agent ever needs to call Hermes programmatically. # claude-agent ever needs to call Hermes programmatically.
API_SERVER_ENABLED: "false" API_SERVER_ENABLED: "false"
# Web dashboard, supervised in-container alongside the gateway (same process group,
# same s6 tree) — see docs/user-guide/docker.md "Running the dashboard". Binds
# 0.0.0.0 so Traefik (a separate container) can reach it; that makes Hermes's own
# auth gate mandatory, which it enforces automatically once the bind isn't loopback.
HERMES_DASHBOARD: "1"
HERMES_DASHBOARD_HOST: 0.0.0.0
HERMES_DASHBOARD_PORT: "9119"
HERMES_DASHBOARD_BASIC_AUTH_USERNAME: ${HERMES_DASHBOARD_USERNAME}
HERMES_DASHBOARD_BASIC_AUTH_PASSWORD: ${HERMES_DASHBOARD_PASSWORD}
HERMES_DASHBOARD_BASIC_AUTH_SECRET: ${HERMES_DASHBOARD_SECRET}
volumes: volumes:
- /home/william/hermes-data:/opt/data - /home/william/hermes-data:/opt/data
networks: networks:
@@ -92,6 +107,17 @@ services:
# immediately exits ("Input is not a terminal") since a detached container has no # immediately exits ("Input is not a terminal") since a detached container has no
# stdin — the container then just sits there having done nothing, every restart. # stdin — the container then just sits there having done nothing, every restart.
command: ["gateway", "run"] command: ["gateway", "run"]
labels:
- "traefik.enable=true"
- "traefik.http.routers.hermes-dashboard.rule=Host(`${HERMES_DASHBOARD_HOSTNAME}`)"
- "traefik.http.routers.hermes-dashboard.entrypoints=websecure"
- "traefik.http.routers.hermes-dashboard.tls.certresolver=letsencrypt"
# Just TLS termination + routing — no Traefik-level auth middleware. Hermes's own
# login gate is not optional here anyway: it fails closed at startup once its bind
# isn't loopback-only (required for Traefik, a separate container, to reach it at
# all), so a second gate in front of it would only add friction, not remove Hermes's
# own one. One password, at Hermes's own login page.
- "traefik.http.services.hermes-dashboard.loadbalancer.server.port=9119"
claude-agent: claude-agent:
# Gitea PR-review only now — no Matrix presence (see hermes above; only one agent # Gitea PR-review only now — no Matrix presence (see hermes above; only one agent