Hermes Agent 跑在一台内网 Linux 虚拟机上,人在 MacBook Air 前面。想用桌面客户端 Hermes One 连过去聊天,客户端启动就问你要 SSH 地址或者 HTTP 地址加 token。
服务端必须先配,不配连不上。这篇把两种连法的完整步骤写出来,照着做就能通。
💡 两种方式选哪个
- SSH 隧道:客户端在本地开个隧道钻进去,Hermes 端口只监听
127.0.0.1,外面扫不到。推荐。- HTTP 直连:直接填
http://<内网IP>:8642加 token。少一层,但端口对整个内网敞开,靠 token 挡。两种可以同时开着,互不影响。本文先把 SSH 走通(第 1~5 步),HTTP 是在此基础上改一个值(第 6 步)。
一、要准备什么
| 东西 | 说明 | 怎么查 |
|---|---|---|
| 服务端内网 IP | 跑 Hermes 那台机器的地址 | 在服务端运行 ip -4 addr show | grep inet |
| 服务端登录用户名 | Hermes 是用哪个账号跑的 | 在服务端运行 whoami |
| Hermes 端口 | API server 端口,默认 8642 | 本文一律用 8642 |
| 客户端公钥 | Mac 上的 SSH 公钥 | 在 Mac 运行 cat ~/.ssh/id_rsa.pub |
本文示例用 203.0.113.19 代表服务端 IP,用户名 hermesuser。照做时换成你自己的值。
⚠️ 客户端和服务端必须能互通。内网地址意味着 Mac 要在同一网段,或者已经连上 VPN。人在外面用 4G 是连不上的,那需要另配 Tailscale 或反向代理,不在本文范围。
二、服务端配置(在跑 Hermes 的机器上操作)
第 1 步:开启 API server
Hermes 默认不开 API server,得在 ~/.hermes/.env 里加四行。
先看看有没有配过:
grep -n "API_SERVER" ~/.hermes/.env
没有输出就是没配。用编辑器打开 ~/.hermes/.env,在末尾追加:
API_SERVER_ENABLED=true
API_SERVER_KEY=先随便填个占位符
API_SERVER_PORT=8642
API_SERVER_HOST=127.0.0.1四个值的含义:
| 变量 | 作用 | 本步填什么 |
|---|---|---|
API_SERVER_ENABLED | 总开关,不写就不启动 | true |
API_SERVER_KEY | 客户端认证用的 Bearer token | 占位符即可,见下面说明 |
API_SERVER_PORT | 监听端口 | 8642 |
API_SERVER_HOST | 监听地址 | SSH 模式填 127.0.0.1 |
💡
API_SERVER_KEY为什么可以先填占位符客户端第一次通过 SSH 连上来时,会检查这个值:长度不足 16 字符或者长得像占位符,它就自己生成一个 48 位十六进制字符串写回服务端的
.env。这不是 bug,是客户端源码
src/main/ssh-remote.ts里isUsableApiServerKey()的设计(MIN_API_SERVER_KEY_LENGTH = 16)。所以你自己精心生成的 key 可能会被换掉——不用跟它较劲,让它自己生成最省事。后面第 6 步要用 HTTP 模式时,记得重新读一遍
.env拿最新的值。
第 2 步:确认 SSH 服务端允许端口转发
SSH 隧道靠的是端口转发。检查一下:
sudo sshd -T | grep -i allowtcpforwarding
要看到 allowtcpforwarding yes。如果是 no,编辑 /etc/ssh/sshd_config 把 AllowTcpForwarding 改成 yes,然后 sudo systemctl reload sshd。
⚠️ 路径是
/etc/ssh/sshd_config(ssh目录下),不是/etc/sshd_config。另外注意别改成ssh_config——那个是客户端配置,改了没用。
💡 这项默认就是
yes,绝大多数机器不用动。sudo是必须的,普通用户跑sshd -T会报Permission denied。没有 sudo 权限的话用这个替代,直接查配置文件里有没有显式关掉:
grep -i allowtcpforwarding /etc/ssh/sshd_config没有任何输出,或者输出前面带
#(注释掉了),都说明用的是默认值yes,可以放心往下走。只有明确看到不带#的AllowTcpForwarding no才需要改。
第 3 步:把客户端公钥加进来
在 Mac 上先看公钥:
cat ~/.ssh/id_rsa.pub
输出是一整行,ssh-rsa AAAAB3... 开头,用户名@主机名 结尾。整行复制。
⚠️ 一定是
.pub结尾的公钥。公钥公开传没问题;不带.pub的那个是私钥,绝对不要拷来拷去、不要发到聊天软件里。
如果 No such file or directory,说明 Mac 还没生成密钥,跑一次:
ssh-keygen -t ed25519 -C "macbook-air"
一路回车(passphrase 可以留空),然后 cat ~/.ssh/id_ed25519.pub。
回到服务端,把这行追加进去:
echo "把刚复制的整行粘在这里" >> ~/.ssh/authorized_keys
第 4 步:修权限(最容易踩的坑)
这步是本文最重要的一步。权限不对,认证一定失败,而且报错信息完全看不出原因。
chmod 700 ~/.ssh && chmod 600 ~/.ssh/authorized_keys
验证:
ls -ld ~/.ssh ~/.ssh/authorized_keys
必须看到 drwx------(.ssh 目录)和 -rw-------(authorized_keys 文件):
drwx------ 2 hermesuser hermesuser 4096 Aug 1 19:20 /home/hermesuser/.ssh
-rw------- 1 hermesuser hermesuser 740 Aug 1 19:20 /home/hermesuser/.ssh/authorized_keys⚠️ 为什么这步能卡住人
sshd 默认
StrictModes yes,只要~/.ssh目录组可写或其他人可写(比如drwxrwxr-x,也就是 775),它就直接拒绝公钥认证——不给任何提示,日志里也不写具体原因。客户端这边只会看到一句笼统的
Could not connect via SSH or reach Hermes on the remote,让人以为是 key 错了或者服务没起来,方向全错。很多人只
chmod了authorized_keys文件,忘了父目录,就栽在这里。
第 5 步:重启 Hermes gateway
改完 .env 必须重启才生效。用 systemd 管理的话:
systemctl --user restart hermes-gateway.service
⚠️ Hermes gateway 关闭时要等在线会话排空,重启可能要 2~3 分钟。这期间
systemctl --user is-active会一直显示deactivating,是正常的,耐心等。
等它变成 active 后,确认端口起来了:
ss -tlnp | grep 8642
应该看到:
LISTEN 0 128 127.0.0.1:8642 0.0.0.0:* users:(("hermes",pid=892356,fd=21))127.0.0.1:8642 说明只监听本地回环——这正是 SSH 模式要的效果,外网和内网都扫不到这个端口。
再验证服务真的能应答:
curl -s http://127.0.0.1:8642/health
真实输出长这样,能看到版本号就说明 API server 正常工作:
{"status": "ok", "platform": "hermes-agent", "version": "0.19.0"}三、客户端连接(在 Mac 上操作)
先自测免密登录
别急着打开客户端。 客户端界面下方那句提示 “Make sure you can already run ssh user@host without a password prompt” 是硬要求,先在 Mac 终端验证:
ssh hermesuser@203.0.113.19 'echo OK'
- 输出
OK且没问密码 → 通过,往下走。 - 弹出密码提示 → 公钥认证没成功,回头查第 3、4 步。
Connection refused/timeout→ 网络不通或 sshd 没跑,跟 Hermes 无关。
如果要看具体卡在哪,加 -vvv:
ssh -vvv hermesuser@203.0.113.19 'echo OK' 2>&1 | tail -30
关键是找 Offering public key: 那几行——它列出客户端实际尝试了哪些密钥。
⚠️ 另一个高频坑:客户端根本没用你以为的那把钥匙
如果
-vvv输出里全是这样:debug1: Will attempt key: /Users/mana/.ssh/id_rsa RSA SHA256:wBww... debug1: Trying private key: /Users/mana/.ssh/id_ecdsa debug3: no such identity: /Users/mana/.ssh/id_ecdsa: No such file or directory debug1: Trying private key: /Users/mana/.ssh/id_ed25519 debug3: no such identity: /Users/mana/.ssh/id_ed25519: No such file or directory debug1: Next authentication method: password hermesuser@203.0.113.19's password:说明 Mac 上只有
id_rsa一把钥匙,而你加到服务端的是另一把的公钥。它试完手头所有钥匙都不匹配,就回落到密码认证。最省事的解法:别折腾专用密钥,直接用 Mac 现成的
~/.ssh/id_rsa.pub(第 3 步就是这么写的)。客户端的 Private Key Path 留空,默认就走~/.ssh/id_rsa。
填写客户端
自测过了,打开 Hermes One,选 Connect via SSH:
| 字段 | 填什么 |
|---|---|
| SSH Host | 203.0.113.19 |
| SSH Port | 22 |
| Username | hermesuser |
| Private Key Path | 留空(默认 ~/.ssh/id_rsa) |
| Remote Hermes Port | 8642 |
点 Connect via SSH →。
连上后可能弹一条警告
界面顶部出现琥珀色横幅:
API Server Key not set — chat will fail. SET NOW Show details ✕这是客户端的误报,服务端没问题。
原因在客户端源码:getApiServerKeyStatus() → getApiServerKey() → readEnv(profile) → profilePaths(profile).envFile。这条链读的是 Mac 本地的 ~/.hermes/.env,整个过程没有任何 SSH 感知。Mac 上没装 Hermes、没这个文件,就判定 hasKey: false,横幅就弹了——它查错了机器。
这条文案属于 “Configuration health”(本地配置体检)模块,不是连接状态检测。客户端自己的 i18n 文案里都写了「if you keep secrets in a vault… this warning can be ignored」。
怎么处理:
- 先直接发一条消息试试。 能正常回复就是纯误报,点
✕关掉。 - 想彻底消掉:点
SET NOW,再点里面的 Auto(自动生成)。它只写 Mac 本地的~/.hermes/.env,服务端完全不受影响。
💡 之后 Mac 本地和服务端会有两个不同的
API_SERVER_KEY,这不冲突:SSH 模式下客户端从服务端读真 token,本地那个只是让体检闭嘴用的。
四、可选:加开 HTTP 直连模式
不想每次走 SSH 隧道,可以让客户端直接 HTTP 连。
第 6 步:把监听地址改成 0.0.0.0
编辑服务端 ~/.hermes/.env,把第 1 步那行改掉:
API_SERVER_HOST=0.0.0.0⚠️ 必须是
0.0.0.0,不要填具体内网 IP。填成
203.0.113.19这种具体地址后,127.0.0.1就不再监听了——SSH 隧道转发的目标正是127.0.0.1:8642,SSH 模式会直接失效。0.0.0.0表示所有网卡都听,两种模式才能共存。
重启:
systemctl --user restart hermes-gateway.service
确认监听地址变了:
ss -tlnp | grep 8642
LISTEN 0 128 0.0.0.0:8642 0.0.0.0:* users:(("hermes",pid=900967,fd=21))第 7 步:取出真正的 token
这步不能跳。 前面说过,客户端第一次 SSH 连接时可能已经把 API_SERVER_KEY 换掉了。重新读一遍:
grep "^API_SERVER_KEY" ~/.hermes/.env
输出形如:
API_SERVER_KEY=3f9a1c74-2b58-4e0d-9a61-7c4e8d05b2af用这个值,不是你第 1 步填的占位符。
⚠️ 上面这串是示例,你的值一定不一样,照抄必然认证失败。UUID 形式(客户端
randomUUID()生成)和 48 位十六进制(客户端randomBytes(24)生成)都是正常的。
第 8 步:服务端自测
在服务端用内网 IP(不是 127.0.0.1)验证三件事:
带 token 应该返回模型列表:
curl -s -H "Authorization: Bearer 你的token" http://203.0.113.19:8642/v1/models
不带 token 应该返回 401:
curl -s -o /dev/null -w "%{http_code}\n" http://203.0.113.19:8642/v1/models
真发一条消息,确认 agent 能回:
curl -s -m 120 -X POST http://203.0.113.19:8642/v1/chat/completions \
-H "Authorization: Bearer 你的token" \
-H "Content-Type: application/json" \
-d '{"model":"hermes-agent","messages":[{"role":"user","content":"只回复两个字:正常"}],"stream":false}'三项都对了再去填客户端。
客户端填写
Settings → Connection → 选 Remote:
| 字段 | 填什么 |
|---|---|
| Remote URL | http://203.0.113.19:8642 |
| API Key | 第 7 步取到的值 |
⚠️ 安全提醒:绑
0.0.0.0后,8642 会暴露在这台机器的所有网络接口上,包括 docker 网桥(172.17.x那些)。唯一的防线就是 Bearer token。内网可信的话风险不大;不放心就改回127.0.0.1只用 SSH 模式。
五、排查清单
连不上时按顺序查,每一步都有明确的判定标准。
| # | 检查 | 命令 | 期望 |
|---|---|---|---|
| 1 | 网络通不通 | Mac: nc -vz 203.0.113.19 22 | succeeded |
| 2 | 免密登录 | Mac: ssh user@ip 'echo OK' | 输出 OK,不问密码 |
| 3 | .ssh 权限 | 服务端: ls -ld ~/.ssh | drwx------ |
| 4 | 公钥在不在 | 服务端: ssh-keygen -l -f ~/.ssh/authorized_keys | 指纹与 Mac 的 ssh-keygen -l -f ~/.ssh/id_rsa.pub 一致 |
| 5 | 端口转发 | 服务端: grep -i allowtcpforwarding /etc/ssh/sshd_config | 无输出或带 # = 默认 yes |
| 6 | gateway 活着 | 服务端: systemctl --user is-active hermes-gateway.service | active |
| 7 | 端口在听 | 服务端: ss -tlnp | grep 8642 | 有 LISTEN 行 |
| 8 | 服务能应答 | 服务端: curl -s http://127.0.0.1:8642/health | 有响应 |
💡 第 4 步的指纹对比是最直接的判据。两边跑同样的命令,输出的
SHA256:...必须一样。不一样就是加错了公钥。
自己搭个隧道验证
想绕开客户端确认隧道链路本身没问题,在 Mac 上手动开一个:
ssh -N -L 18642:127.0.0.1:8642 hermesuser@203.0.113.19
这条命令不会返回,让它挂着。另开一个终端:
curl -s http://127.0.0.1:18642/health
有响应就说明隧道链路完全正常,问题在客户端配置。测完回第一个终端按 Ctrl+C。
💡
-L 18642:127.0.0.1:8642的意思是:Mac 本地的 18642 端口,转发到服务端的127.0.0.1:8642。本地用 18642 是为了避免和其它服务撞车,随便换个空闲端口都行。
六、实战验证
本文所有命令都在真实环境跑过。
环境:
| 项 | 值 |
|---|---|
| 服务端 | Linux 6.14.0,Hermes Agent,systemd user service 托管 |
| 客户端 | MacBook Air,Hermes One (fathah/hermes-desktop) |
| 网络 | 同一内网 |
验证结果:
| 步骤 | 结果 |
|---|---|
| 开启 API server,重启 gateway | ✅ ss 显示 127.0.0.1:8642 LISTEN |
/health 探活 | ✅ 有响应 |
手动 SSH 隧道 + 隧道内 /health | ✅ 通 |
带 token 请求 /v1/models | ✅ 200,返回模型列表 |
| 不带 token 请求 | ✅ 401 |
| 真实 chat completion | ✅ 返回预期内容 |
改 0.0.0.0 后内网 IP 直连 | ✅ 三项检查全通过 |
| 客户端 SSH 模式连接 | ✅ 连通,可正常对话 |
踩到的两个坑,都是真实报错:
~/.ssh权限 775 → sshd StrictModes 静默拒绝公钥认证。客户端只报笼统的Could not connect via SSH,服务端日志也不写原因。修成 700 后立刻通。私钥没在客户端机器上 →
ssh -vvv显示只尝试了id_rsa,其余 4 个候选全是No such identity,最后回落到密码认证。改用客户端现成的id_rsa.pub解决,顺带避免了传私钥。
一个误报:连接成功后弹 API Server Key not set — chat will fail.,实测聊天功能完全正常。查客户端源码确认它读的是本地 ~/.hermes/.env,与服务端无关。点 SET NOW → Auto 后横幅消失,且核对服务端 .env 的 mtime 和 key 值均未改变——确认只写了本地文件。
七、小结
- SSH 模式的服务端配置就四行
.env加一次重启,API_SERVER_HOST填127.0.0.1。 chmod 700 ~/.ssh是最容易漏又最致命的一步,权限不对时所有报错都会指向错误的方向。- 打开客户端之前,先在终端跑通
ssh user@host 'echo OK'。这一步能过,客户端基本就能连。 - 客户端会覆写服务端的
API_SERVER_KEY,要 HTTP 模式的话记得重新读.env拿最新值。 - “API Server Key not set” 横幅查的是客户端本地文件,属于误报,别去服务端找问题。
- HTTP 模式改
0.0.0.0即可与 SSH 模式共存,但不要填具体 IP,那样会让 SSH 隧道失效。