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
| 方式 | 环境变量 | 说明 |
|---|---|---|
| Password | HERMES_DASHBOARD_BASIC_AUTH_USERNAME + _PASSWORD | 内置 BasicAuthProvider,纯密码登录 |
| OAuth | HERMES_DASHBOARD_OAUTH_CLIENT_ID | Nous 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
完整用户认证流程
- 用户访问
https://hermes.atibm.com/ - nginx → oauth2-proxy → Google 登录(已有 session 则跳过)
- Dashboard SPA 加载,无内部 session
_auto_sso_response→ 302 →/auth/login?provider=basic- nginx 拦截
location = /auth/login→ 302 →/login - Dashboard 服务端渲染密码登录表单
- 用户输入 basic auth username+password,点击 Sign in
- JS fetch POST → nginx 直通
location /auth/password-login→ Dashboard - BasicAuthProvider 验证凭据 → 200
{"ok":true,"next":"/"}+ session cookie - 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-403—gated_auth_middleware/opt/hermes/hermes_cli/web_server.py:384-403—should_require_auth/opt/hermes/hermes_cli/web_server.py:14193-14257— auth_required 门禁初始化/opt/hermes/plugins/dashboard_auth/basic/__init__.py:230—start_login()NotImplementedError/opt/hermes/hermes_cli/dashboard_auth/login_page.py— 服务端渲染密码登录页