Hermes Agent v2026.7.1 Dashboard 认证变更加 nginx 适配方案

Hermes Agent v2026.7.1 Dashboard 认证变更加 nginx 适配方案

环境架构

用户 → nginx:443 → oauth2-proxy:4180 (Google OAuth) → hermes-agent:9119 (dashboard)

nginx 作唯一对外入口,oauth2-proxy 处理外部认证,Hermes dashboard 在 Docker 内部网络绑定 0.0.0.0:9119

问题:v2026.7.1 安全加固

v2026.7.1 移除了 HERMES_DASHBOARD_INSECURE=1 跳过认证门禁的能力(June 2026 hermes-0day MCP-persistence 安全事件后的加固)。现在只要 HOST=0.0.0.0(非 loopback),dashboard 必须有一个 auth provider 才能启动。

可选的 auth provider

方式环境变量说明
PasswordHERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD内置 BasicAuthProvider,纯密码登录
OAuthHERMES_DASHBOARD_OAUTH_CLIENT_IDNous Portal OAuth 或自定义 OIDC

故障根因:_auto_sso_response 逻辑缺陷

使用 BasicAuthProvider 时,dashboard 内部认证流程存在以下矛盾:

gated_auth_middleware
  → 用户无 session cookie
  → _auto_sso_response()          # middleware.py:140
    → 恰好 1 个 provider (basic)   # len(list_session_providers()) == 1
    → 硬编码假设:唯一 provider 支持 OAuth 重定向
    → 302 → /auth/login?provider=basic
  → auth_login route              # routes.py:182
    → p.start_login()             # BasicAuthProvider.start_login() 抛出 NotImplementedError
    → 500 Internal Server Error

根源: _auto_sso_response() 对所有 supports_session=True 的 provider 一视同仁,无视 BasicAuthProvider 只支持密码 POST(/auth/password-login),不支持 OAuth 重定向。

解决方案(纯 nginx 配置,不修改源码)

在 nginx conf 中加两个 location 块:

1. 拦截自动 SSO 重定向

_auto_sso_response 发出的 302 到 /auth/login?provider=basic 被 nginx 截获,重写到 /login(服务端渲染的密码登录页)。

location = /auth/login {
    return 302 /login;
}

2. 密码登录 POST 跳过 auth_request

如果不跳过多 oauth2-proxy 层,JS 的 fetch POST 到 /auth/password-login 会被 nginx 的 auth_request /oauth2/auth 拦截(302 → 未认证页面)。fetch 跟重定向拿到 HTML,resp.json() 解析失败,错误被 .catch 吞掉 → 用户看到"点击没反应"。

location /auth/password-login {
    proxy_pass http://hermes-agent:9119;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_set_header X-Forwarded-Proto $scheme;
    client_max_body_size 500M;
}

安全由 BasicAuthProvider 自身保障(需正确的 username+password)。

完整 nginx conf.d 配置

# 1. HTTP 自动跳转 HTTPS
server {
    listen 80;
    server_name hermes.atibm.com;
    return 301 https://$host$request_uri;
}

# 2. HTTPS 核心配置
server {
    listen 443 ssl;
    server_name hermes.atibm.com;

    ssl_certificate     /etc/letsencrypt/live/ghost.atibm.com/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/ghost.atibm.com/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;

    # ★ 拦截 dashboard 自动 SSO → 密码登录页
    location = /auth/login {
        return 302 /login;
    }

    # ★ 密码登录 POST → 跳过 auth_request,直通 dashboard
    location /auth/password-login {
        proxy_pass http://hermes-agent:9119;
        proxy_http_version 1.1;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        client_max_body_size 500M;
    }

    # 页面:走 auth_request 认证
    location / {
        auth_request /oauth2/auth;
        error_page 401 = @login;

        proxy_pass http://hermes-agent:9119;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_read_timeout 86400s;
        proxy_send_timeout 86400s;
        proxy_buffering off;
        client_max_body_size 500M;
    }

    # 登录跳转
    location @login {
        return 302 https://hermes.atibm.com/oauth2/sign_in;
    }

    # oauth2-proxy 认证 + 回调端点
    location /oauth2/ {
        proxy_pass http://hermes-oauth:4180;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}

docker-compose 环境变量

environment:
  - HERMES_DASHBOARD=1
  - HERMES_DASHBOARD_TUI=1
  - HERMES_DASHBOARD_INSECURE=1       # 保留(不再有效但无害)
  - HERMES_DASHBOARD_HOST=0.0.0.0
  - HERMES_DASHBOARD_BASIC_AUTH_USERNAME=hermes
  - HERMES_DASHBOARD_BASIC_AUTH_PASSWORD=y9Ak7YsBp7Hmb4NLAnu1

完整用户认证流程

  1. 用户访问 https://hermes.atibm.com/
  2. nginx → oauth2-proxy → Google 登录(已有 session 则跳过)
  3. Dashboard SPA 加载,无内部 session
  4. _auto_sso_response → 302 → /auth/login?provider=basic
  5. nginx 拦截 location = /auth/login → 302 → /login
  6. Dashboard 服务端渲染密码登录表单
  7. 用户输入 basic auth username+password,点击 Sign in
  8. JS fetch POST → nginx 直通 location /auth/password-login → Dashboard
  9. BasicAuthProvider 验证凭据 → 200 {"ok":true,"next":"/"} + session cookie
  10. JS window.location.assign('/') → Dashboard ✅

相关源码位置

  • /opt/hermes/hermes_cli/dashboard_auth/middleware.py:140-210_auto_sso_response
  • /opt/hermes/hermes_cli/dashboard_auth/middleware.py:250-403gated_auth_middleware
  • /opt/hermes/hermes_cli/web_server.py:384-403should_require_auth
  • /opt/hermes/hermes_cli/web_server.py:14193-14257 — auth_required 门禁初始化
  • /opt/hermes/plugins/dashboard_auth/basic/__init__.py:230start_login() NotImplementedError
  • /opt/hermes/hermes_cli/dashboard_auth/login_page.py — 服务端渲染密码登录页