先看一个真实的报错
把 DeepSeek Harness 放到公网,很多人的第一反应是 --host 0.0.0.0,然后被直接拒绝:
error: --host 0.0.0.0 is an unspecified (wildcard) address, which is not supported:
binding every interface would expose remote code execution to the network;
bind one concrete IPv4 or IPv6 address instead
这不是没做完,是故意的。DSH 的 Web 端默认只监听回环,且没有任何「按方法区分」的本机豁免——也就是说,它默认假设对面就是本机浏览器。想从外面访问,正确的做法不是加插件、也不是改代码,而是用 0.2.1 里已经有的三个原生参数,把它交给一个反向代理。
下面所有结论,我都用 0.2.1-rc.2 起了一个临时实例实测过,不是照抄文档。
三个参数,各管一件事
| 参数 | 它负责什么 | 它不负责什么 |
|---|---|---|
--host |
监听器绑哪个地址。只接受本机某个具体 IPv4/IPv6,通配地址被拒 | 不决定谁能访问,也不改变信任名单 |
--trusted-host |
往 Host/Origin 栅栏的信任名单里加 authority,可重复;host:port 精确匹配,不带端口则匹配任意端口 |
不做认证,不配置路由 |
--public-url |
公告浏览器该用哪个 HTTP(S) 根:打印与打开的启动 URL、DSH_WEB_URL、agent 运行上下文里的 GUI 地址 |
不授予任何信任,不配置监听、路由或 cookie 作用域 |
还有一对可选参数:--tls-cert 与 --tls-key,让监听器自己提供 HTTPS。两者必须同时给,只给一个会报用法错误;DSH 不签发、不续期证书,也没有 ACME,换了证书要重启。
实测:401 和 403 的分界线
我用 --host 127.0.0.1 --port 3099 --public-url https://dsh.example.com/ --trusted-host dsh.example.com 起了实例,然后换不同的 Host 头请求 /api:
| 请求的 Host | 结果 | 含义 |
|---|---|---|
dsh.example.com |
401 | 通过了信任栅栏,差的是登录 cookie |
127.0.0.1:3099 |
401 | 绑定 IP 本身天然被接受 |
evil.example.com |
403 | 不在名单里,直接拒绝 |
dsh.example.com + 错误 Origin |
403 | Origin 必须与 Host 同源 |
还有一个很容易配错的点,我单独验证了:只写 --public-url、不写 --trusted-host,访问照样是 403。公告和信任是两件事——前者告诉浏览器「去哪里找我」,后者才决定「我认不认这个域名」。
为什么代理必须保留 Host
这是整套配置里最容易翻车的地方。DSH 的会话 cookie 是 host-only 的,签名 payload 里绑定了它看到的 authority;同时每个请求还要过 Origin 校验。也就是说:
- 如果代理把 Host 改写成
127.0.0.1:3080,浏览器发来的Origin: https://dsh.example.com就对不上,直接 403(我实测过,确实如此); - 如果代理保留 Host 但没配
--trusted-host,同样 403; - 只有「保留 Host + trusted-host 写上公网域名」,401 才会出现——那是正常的「请先登录」。
换到 token 那一步,实测返回长这样(值已脱敏):
HTTP/1.1 303 See Other
location: ./
set-cookie: dsh-auth-xxxx=v1.eyJhdXRob3JpdHkiOiJkc2guZXhhbXBsZS5jb20i...;
Max-Age=2592000; Path=/; HttpOnly; SameSite=Strict
注意三点:cookie 有效期 30 天;作用域是 host-only;没有 Secure 标记——因为签发它的监听器是明文 HTTP。这一点后面单独说。
方案一:Caddy,根域名(推荐)
DSH 只绑回环,Caddy 在宿主机上终止 TLS:
dsh --profile web --no-open \
--host 127.0.0.1 --port 3080 \
--public-url https://dsh.example.com/ \
--trusted-host dsh.example.com
对应的 Caddyfile:
dsh.example.com {
encode zstd gzip
reverse_proxy 127.0.0.1:3080 {
header_up Host {host}
}
}
header_up Host {host} 是显式保留原始 Host。Caddy 默认通常也会透传,但写出来能避免以后换配置时被悄悄改掉。WebSocket 的 /api/remote.mux 由 Caddy 自动处理升级,不用额外配置。
方案二:Nginx
Nginx 的默认行为恰好相反——不写 proxy_set_header Host,上游收到的是 $proxy_host,于是必然 403:
map $http_upgrade $connection_upgrade {
default upgrade;
'' close;
}
server {
listen 443 ssl http2;
server_name dsh.example.com;
location / {
proxy_pass http://127.0.0.1:3080;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection $connection_upgrade;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_read_timeout 3600s;
proxy_buffering off;
}
}
顺带说一句:DSH 判断 Host 时不采信 X-Forwarded-*,所以你写不写转发头都不影响鉴权;写上的价值是日志和以后接别的服务时保持一致。
子路径部署:public-url 真正发光的地方
如果不想为它单独开一个域名,可以挂在子路径上。--public-url 要写成带前缀的根,代理负责把前缀剥掉:
dsh --profile web --no-open \
--host 127.0.0.1 --port 3080 \
--public-url https://example.com/dsh/ \
--trusted-host example.com
example.com {
handle_path /dsh/* {
reverse_proxy 127.0.0.1:3080 {
header_up Host {host}
}
}
}
这条路能走通,是因为前端的资源引用是相对路径(实测首页里的 ./assets/...、plugins/...),配上前缀剥离后浏览器解析出来的地址仍然会经过代理。不过根域名还是更省心:少一层前缀匹配,也少一类「某个绝对路径没被代理到」的疑难杂症。
容器里:别绑 0.0.0.0
容器里最常见的错误是「为了让宿主机访问到,绑 0.0.0.0」。DSH 不允许,官方给的做法是绑容器自己的地址:
services:
dsh:
image: your-registry/dsh:0.2.1-rc.2
restart: unless-stopped
command:
- sh
- -c
- >-
exec dsh --profile web --no-open
--host "$$(hostname -i | awk '{print $$1}')"
--port 3080
--public-url https://dsh.example.com/
--trusted-host dsh.example.com
compose 里 $$ 是转义后的 $,别写成单个。Caddy 放在同一个 compose 网络里,反代 dsh:3080 即可。如果嫌动态取地址麻烦,Linux 上也可以让 DSH 用 network_mode: host 绑 127.0.0.1,代价是失去端口隔离。
TLS 与 cookie 的那点遗憾
代理终止 TLS、上游走 HTTP,是标准做法,但它带来一个副作用:DSH 签发的 cookie 不带 Secure。原因是这个标记只由「接收请求的监听器是否提供 TLS」决定,public-url 和转发头都不参与判断。
后果不算致命——cookie 仍然是 HttpOnly + SameSite=Strict,且只能由代理转发进来;但如果你要求它必须带 Secure,只有一条路:让 DSH 自己开 TLS(--tls-cert 和 --tls-key),代理再反代到 https://127.0.0.1:3080。多一层证书管理,换一个标记,值不值得看你的合规要求。
上线前的检查清单
- 端口只对代理开放。
127.0.0.1:3080或 compose 内网,别让 3080 直接暴露在公网。 - trusted-host 写域名,不写 IP。写 IP 等于把 Host 校验让给了任何能用该 IP 访问的人。
- public-url 用 https。它在 agent 上下文里会被当作 GUI 地址使用,写 http 会误导后续操作。
- 启动 URL 当密码对待。那一行里带进程级 token,别贴进群里、别进工单。不想让它进日志,可以把配置里的
printUrl关掉,但那样你就得从别处拿登录入口。 - 别缓存 /api。反向代理对 API 和 WebSocket 一律关闭缓冲与缓存。
- 它是单用户模型。cookie 对应的就是 owner,多个人共用一个入口等于共用同一个 agent 和同一份文件权限。
我的判断
这三个参数的分工其实很清晰:监听、信任、公告。绝大多数配置错误,都是把其中两件事混成一件——以为 public-url 配了就能访问,或者以为 --host 换了就放行了。
DSH 在这件事上的取舍我挺认同:默认只信本机,往外走必须显式点名,token 换 cookie 而不是长期 bearer,cookie 绑 authority。它没有替你解决「怎么安全地暴露到公网」,但把每一层该由谁负责划得很清楚——剩下的,是反向代理和你的防火墙该干的活。










暂无评论内容