diff --git a/flows/flow-04-agent.sh b/flows/flow-04-agent.sh index ad4f6f81..aa72067b 100755 --- a/flows/flow-04-agent.sh +++ b/flows/flow-04-agent.sh @@ -218,25 +218,33 @@ else fail "Hermes gateway health check failed — ${oc_health:0:100}" fi -step "Hermes native dashboard UI via deeplink" +step "Hermes native dashboard UI reachable + auth-gated" HERMES_DASHBOARD_HOST="obol-agent.obol.stack" if [ "$ingress_port" = "80" ]; then HERMES_DASHBOARD_URL="http://${HERMES_DASHBOARD_HOST}" else HERMES_DASHBOARD_URL="http://${HERMES_DASHBOARD_HOST}:${ingress_port}" fi -dashboard_html="" +# hermes-agent v2026.7.x hardening: a non-loopback (0.0.0.0) dashboard bind now +# requires an auth provider, and the loopback-only inline __HERMES_SESSION_TOKEN__ +# is deliberately disabled on a gated bind (closes the hermes-0day hole). The +# stack configures basic-auth. Assert the dashboard container is UP (public +# /api/status → 200) and ENFORCES auth (protected /api/sessions → 401 for an +# unauthenticated caller; /api routes gate on the session token, not basic-auth). +dash_code() { curl --resolve "${HERMES_DASHBOARD_HOST}:${ingress_port}:127.0.0.1" \ + -s -o /dev/null -w "%{http_code}" --max-time 10 "$@" 2>/dev/null; } +dash_status="" dash_protected="" for i in $(seq 1 15); do - dashboard_html=$(curl --resolve "${HERMES_DASHBOARD_HOST}:${ingress_port}:127.0.0.1" \ - -sf --max-time 10 "$HERMES_DASHBOARD_URL/" 2>&1) || true - if echo "$dashboard_html" | grep -q "__HERMES_SESSION_TOKEN__"; then - pass "Hermes dashboard UI loaded: $HERMES_DASHBOARD_URL" + dash_status=$(dash_code "$HERMES_DASHBOARD_URL/api/status") + dash_protected=$(dash_code "$HERMES_DASHBOARD_URL/api/sessions") + if [ "$dash_status" = "200" ] && [ "$dash_protected" = "401" ]; then + pass "Hermes dashboard up + auth-gated (status=$dash_status protected=$dash_protected)" break fi sleep 2 done -if ! echo "$dashboard_html" | grep -q "__HERMES_SESSION_TOKEN__"; then - fail "Hermes dashboard UI deeplink failed — ${dashboard_html:0:100}" +if [ "$dash_status" != "200" ] || [ "$dash_protected" != "401" ]; then + fail "Hermes dashboard check failed — status=$dash_status protected=$dash_protected" fi # §4: Verify Hermes config still has the expected model/provider wiring. diff --git a/internal/hermes/hermes.go b/internal/hermes/hermes.go index 479ca5a5..b4e26a0e 100644 --- a/internal/hermes/hermes.go +++ b/internal/hermes/hermes.go @@ -35,7 +35,7 @@ const ( rawChartVersion = "2.0.2" // renovate: datasource=docker depName=nousresearch/hermes-agent - defaultImage = "nousresearch/hermes-agent:v2026.6.19" + defaultImage = "nousresearch/hermes-agent:v2026.7.1" // Use the upstream image venv instead of cloning Hermes into the PVC on // every cold start. The init container below validates the required extras // are present so image regressions fail before the gateway starts. @@ -977,6 +977,16 @@ func generateValues(namespace, hostname, dashboardHostname, agentBaseURL, token, # is safe here. Production deployments must override this via a values overlay. - name: GATEWAY_ALLOW_ALL_USERS value: "true" + # v2026.7.x hardening: a non-loopback (0.0.0.0) dashboard bind now + # requires an auth provider; the legacy --insecure flag no longer + # bypasses it (closes the hermes-0day unauthenticated-dashboard + # hole). Register the bundled basic-auth provider using the agent's + # existing API token as the password (already surfaced to the + # operator). Probe path /api/status stays auth-exempt. + - name: HERMES_DASHBOARD_BASIC_AUTH_USERNAME + value: obol + - name: HERMES_DASHBOARD_BASIC_AUTH_PASSWORD + value: %s readinessProbe: httpGet: path: /api/status @@ -1056,7 +1066,7 @@ func generateValues(namespace, hostname, dashboardHostname, agentBaseURL, token, - name: %s port: %d `, desc.DefaultPort, desc.DefaultPort, desc.DefaultPort, - quoteYAML(image()), quoteYAML(hermesBinary), dashboardPort, dashboardPort, desc.DefaultPort, dashboardPort, dashboardPort, dashboardPort, + quoteYAML(image()), quoteYAML(hermesBinary), dashboardPort, dashboardPort, desc.DefaultPort, quoteYAML(token), dashboardPort, dashboardPort, dashboardPort, desc.DataPVCName, desc.ServiceName, namespace, desc.ServiceName, desc.ServiceName, desc.DefaultPort, dashboardPort, desc.ServiceName, namespace, quoteYAML(hostname), desc.ServiceName, desc.DefaultPort, diff --git a/internal/serviceoffercontroller/agent_render.go b/internal/serviceoffercontroller/agent_render.go index 91a7e276..4e030929 100644 --- a/internal/serviceoffercontroller/agent_render.go +++ b/internal/serviceoffercontroller/agent_render.go @@ -27,7 +27,7 @@ const ( hermesDataPVC = "hermes-data" hermesAPIPath = "/health" // renovate: datasource=docker depName=nousresearch/hermes-agent - defaultHermesImage = "nousresearch/hermes-agent:v2026.6.19" + defaultHermesImage = "nousresearch/hermes-agent:v2026.7.1" ) // agentLabels returns the standard label set we attach to every primitive