在 OpenClaw 网关上连一个客户端,WebSocket 一打开,服务端不等我们说话,先推过来一帧:
{"type":"event","event":"connect.challenge", "payload":{"nonce":"d57bef68-95bd-4c68-b55b-8989377ab652","ts":1793019487750}}客户端接着回一个 connect 请求。里面有一个 device 对象,带着公钥、64 字节的签名,还把刚才那个 nonce 原样抄了回去;而在它旁边,auth.token 字段里明明白白躺着网关令牌:
"auth": {"token":"tok_demo_123"},"device": {"id":"3faf77339ceab958…","publicKey":"53x2_wKpjVK1xL04…", "signature":"zrIaDJPZa4hjFxYMlznC…","signedAt":1793019487750, "nonce":"d57bef68-95bd-4c68-b55b-8989377ab652"}这就有点奇怪了。如果连接走的是 ws:// 而不是 wss://,令牌不就被路上的人看见了吗?既然令牌都明文发了,那个 nonce 和签名又是在防什么?是不是有了 challenge,明文 ws 也就够了?
我们就从这两帧出发,先弄清明文 WebSocket 上到底暴露了什么,再看各家软件是怎么在这条连接上证明“我是谁”的。
一、明文 WebSocket 上暴露了什么
现象:帧里的内容看起来像乱码
WebSocket 连接以一个普通的 HTTP 请求开始:客户端发 GET,带上 Upgrade: websocket,服务端回 101 Switching Protocols,之后同一条 TCP 连接就改成按“帧”收发数据。如果用 ws://,这个 HTTP 请求和之后的每一帧都直接跑在 TCP 上,没有任何加密。
但如果真的去看客户端发出的字节,会发现载荷并不是明文 JSON,而是一串看不出规律的十六进制。这是因为 RFC 6455 规定,客户端发往服务端的每一帧都必须做掩码1:随机挑 4 个字节,和载荷逐字节异或。那掩码算不算一种加密?
原因:掩码键就写在帧头里
不算。掩码键本身就放在帧头里,紧跟在长度字段后面,和载荷一起发出去。我们用一个小脚本构造了一帧2,图 1 是它在线路上的样子。
掩码真正防的是另一件事。早期研究发现,网页里的脚本如果能让浏览器往一条连接上写出任意字节,就可能骗过路上那些不懂 WebSocket 的透明代理,让它们把攻击者伪造的内容当成某个 HTTP 响应缓存起来。每一帧换一个随机掩码,脚本就控制不了线路上的实际字节,这类缓存投毒就做不成了1。它针对的是中间设备的误判,不是窃听者。
所以,在 ws:// 上,任何在路径上的人(同一个 Wi-Fi、路由器、运营商、公司代理)都能读到升级请求里的 URL、Cookie、头部,以及之后每一帧的内容。 那鉴权用的令牌,通常就放在这几个地方之一。
二、令牌放在哪里
浏览器的限制
照理说,HTTP 上最常见的做法是把令牌放进 Authorization 头。但浏览器里的 new WebSocket(url, protocols) 只接受一个 URL 和一组子协议名,不能自定义请求头。于是各家软件只能在图 1 左边那张卡片和右边那一帧之间挑一个位置:
- URL 参数:Jupyter 启动时打印的链接就是
?token=...3。缺点是 URL 会进访问日志和浏览器历史。 - Cookie:Jupyter 登录后、code-server 都靠 Cookie。浏览器会自动带上它,这带来跨站 WebSocket 劫持的问题:别的网站上的脚本也能向你的服务发起 WebSocket,浏览器照样附上 Cookie,而 WebSocket 不受同源策略限制4。所以用 Cookie 的服务都要检查
Origin头,Jupyter 的check_origin、code-server 的ensureOrigin、OpenClaw 的allowedOrigins都是这个用途。 - 子协议:Kubernetes 允许把令牌编进
Sec-WebSocket-Protocol,写成base64url.bearer.authorization.k8s.io.<token>5,借子协议这个唯一能设置的头。 - 第一帧消息:Home Assistant 连上后先发
auth_required,客户端回{"type":"auth","access_token":"..."}6;OpenClaw、VS Code Server 也是这样。浏览器不会自动附带这种凭据,天然没有跨站劫持的问题。
共同点:都是不记名令牌
位置不同,性质是一样的:这些令牌都是不记名令牌(bearer token),谁拿着它,服务端就当谁是合法用户。服务端只做一次比较,“你交上来的字符串等不等于我存的那个”。在明文 ws 上,路上的人看到它,就能拿去自己连一次。
Home Assistant 的访问令牌是 30 分钟过期的 JWT7,偷到之后能用的时间窗口有限;但它也支持以年计的长期令牌。code-server 更直接,Cookie 里存的就是口令的 argon2 哈希8,在改口令之前一直有效。
能不能干脆不把秘密发出去,只证明“我知道它”?
三、挑战-应答:不发送秘密本身
第一次尝试:发秘密的哈希
最直觉的想法是不发口令,发 sha256(口令)。可是服务端要验证,只能拿同样的哈希来比,于是这个哈希值本身就成了新的口令:窃听者不需要知道原口令,把听到的哈希原样再发一遍就能登录。这就是重放攻击。
问题出在每次发送的内容都一样。要让听到的东西下次不能用,就得让每次要回答的问题不一样。
解决:每个连接一道新题
这就是挑战-应答(challenge-response)。服务端在每个连接开始时生成一个随机数(nonce,number used once,只用一次的数)发给客户端,客户端用秘密和这个随机数一起算出应答。下一条连接换一个随机数,上一条连接里听到的应答就作废了。
obs-websocket(OBS 直播软件的远程控制接口)的 5.x 协议是一个很干净的例子9。服务启动时生成一个 salt;每来一个连接,再生成一个 challenge,两者一起放进第一帧 Hello 里发给客户端。客户端按下面两步算出应答:
服务端用自己保存的口令做同样的计算,比较结果10。图 2 是用口令 demo-pass 实际算出的两条连接。
它没有防住的事
挑战-应答解决了“听到就能重放”,但还留着两个口子。
一是服务端保存的东西。 obs-websocket 在配置里存的是口令本身,secret 也是服务端启动时现算的。共享秘密的方案都这样:要验证对方算得对不对,服务端自己必须能算,也就必须持有和口令等价的东西。服务端的配置一旦泄露,攻击者照样能登录。
二是弱口令。 窃听者虽然拿不到口令,却拿到了 salt、challenge 和对应的应答,等于拿到了一道“题和答案”。如果口令是能被猜出来的短词,他可以在自己机器上一个个试,不用再碰服务器。口令足够长、足够随机时,这条路就走不通。
顺带一提,VS Code Server 的握手里也有一步叫 sign:客户端先在 auth 消息里交上 connection token,服务端再发一段数据让客户端签名11。但这个签名由闭源模块 vsda 完成,网页版客户端直接把 connection token 当作“签名”交回去。从代码看,它是在确认连上来的是官方客户端,令牌本身仍然明文放在第一条消息里,防窃听靠的是 SSH 隧道或只在本机监听(这是根据源码的推断)。看起来像挑战,不一定是为了保护秘密。
那有没有一种办法,让服务端连“和口令等价的东西”都不用存?
四、用公钥签名代替共享秘密:OpenClaw 的设备身份
分析:把共享秘密换成一对密钥
办法是改用非对称签名。每个客户端设备第一次运行时生成一对 Ed25519 密钥,私钥留在本地(浏览器里也一样,不离开浏览器),只把公钥交给服务端。服务端出题,客户端用私钥签名,服务端用公钥验签。公钥是公开的,服务端存着它不怕泄露:拿到公钥的人签不出名。
OpenClaw 网关就是这么做的12。开头那两帧就是这套机制的全部:服务端在连接打开后立即发出 connect.challenge,里面的 nonce 是一个随机 UUID,ts 是服务端当前时间13;客户端把 nonce 和一串连接参数拼成签名载荷,用私钥签名后放进 connect.params.device。图 3 把客户端交上去的字段、被签名的载荷和服务端的四项检查放在一起。
每一项检查防的是什么
服务端的检查顺序写在 connect-auth.ts 里14,每一项都对应一种冒充方式:
deviceId 必须等于公钥的 SHA-256。 设备 ID 不是客户端自己起的名字,而是从公钥算出来的。想冒充某台已配对的设备,就得拿出它的公钥,进而得拿出能和公钥对上的签名。
nonce 必须等于这条连接的 nonce。 这是防重放的核心。服务端为每个连接生成一个新 UUID,并记在这条连接上;上一条连接里签过的名,nonce 对不上,直接拒绝。
签名必须覆盖 v3 载荷。 载荷不只有 nonce,还有角色、权限范围(scopes)、客户端类型和令牌15。这样窃听者也不能保留签名、只改一下请求的权限,因为改了任何一个字段,签名就对不上了。
signedAt 必须在两分钟以内。 有了 nonce,这一项看起来多余。它其实是上一代机制留下的:早期客户端不等 challenge,只签本地时间戳,靠时间窗口防重放,可在两分钟内重放仍然有效。现在的协议要求先等 connect.challenge,用里面的 ts 作为 signedAt,旧客户端会收到 DEVICE_AUTH_NONCE_REQUIRED 之类的迁移错误码12。
我们照着这四项检查写了一个玩具校验器2,用同一把私钥跑了五种情况:
连接 1,正常签名 : ok连接 2,原样再交一次 : device-nonce-mismatch连接 2,改 nonce 字段 : device-signature连接 1,改 scopes : device-signature连接 1,3 分钟后才交 : device-signature-stale把签名原样带到新连接,nonce 对不上;把 nonce 字段改成新连接的,签名又对不上,因为 nonce 在载荷里。这就是“签名绑定到连接”。
既然设备签名这么严,为什么 connect 帧里还要明文带一个令牌?
五、令牌为什么还要明文发
原因:两层鉴权各管一件事
OpenClaw 的鉴权分两层。第一层是网关鉴权,回答“你有没有资格跟这个网关说话”,凭的是共享令牌或口令,或者 Tailscale、可信代理之类的身份;第二层是设备身份和配对,回答“你是哪台设备、被批准过什么权限”,凭的是图 3 里的签名12。文档的说法是,网关鉴权先于设备配对执行。
共享令牌是运维人员配进网关、再复制到各个客户端的一个字符串,客户端手里只有它的原文,没法对它做非对称签名,只能直接交上去。它也被拼进了 v3 载荷,但签名只能证明“这个令牌是和这次签名一起交的”,并不能把令牌藏起来。
那令牌被偷了会怎样
假设攻击者在明文 ws 上看到了共享令牌,他拿着它自己连一次:
网关鉴权能过。 共享令牌本来就是不记名的。
设备这一关过不了。 他没有原设备的私钥,只能自己生成一对,这就成了一台新设备。新设备从非回环地址连上来,需要管理员在网关上审批配对,连接会以 1008 pairing required 断开16。只有直连 127.0.0.1 的本机连接才自动批准。
设备令牌更是偷了也没用。 配对之后网关会签发一个设备令牌,以后连接用它代替共享令牌。它同样放在 auth.token 里明文发送,但网关按 deviceId 去查它,而 deviceId 要配上那台设备私钥的签名才认。
OpenClaw 的文档在讨论纯 HTTP 访问时把这一点说得很直白:签名密钥从不离开浏览器,是明文传输唯一泄露不了的凭据;共享令牌则不然,任何在路径上观察 HTTP 连接的人都能读到17。也就是说,设计者清楚令牌会在明文 ws 上暴露,设备签名和配对审批是在这种情况下兜底的第二道门。
那么,只要有了设备签名,明文 ws 是不是就够了?
六、挑战-应答护住的是秘密,不是会话
现象:签名只发生在第一帧
回头看图 3,签名只覆盖 connect 这一帧。握手完成之后,聊天消息、工具调用、执行命令的请求都是普通的 JSON 帧,既没有签名也没有加密。obs-websocket、Home Assistant 也一样:鉴权只在开头做一次,之后的连接被当成“已经验证过的”。
原因:被动窃听和主动中间人是两种对手
挑战-应答能防的是被动窃听者:他只能看,看到的应答下次用不了。可路径上的人不一定只看。路由器、恶意 Wi-Fi、被控制的代理可以做主动中间人:他不需要知道任何秘密,只需要把服务端的 challenge 原样转给真正的客户端,再把客户端的签名原样转回服务端。双方的握手都是真的,签名也是真的,鉴权顺利通过;之后他就坐在这条已认证的连接中间,读每一条消息,甚至改写或插入消息。
在浏览器场景里还有更直接的办法。Control UI 本身是通过 HTTP 下发的网页,主动中间人可以直接改掉页面里的脚本,在私钥签名之前就动手。OpenClaw 文档也提到了这一点:纯 HTTP 下,主动攻击者可以修改页面、拿走页面里的任何东西17。
把前面几种方案放在一起,能看清每一步到底防住了什么:
| 鉴权方式 | 被动窃听后能重放 | 泄露的东西可离线猜口令 | 主动中间人改后续消息 |
|---|---|---|---|
| 不记名令牌(HA、Jupyter、VS Code Server) | 能 | 不适用,令牌即凭据 | 能 |
| 发口令的哈希 | 能 | 能 | 能 |
| 共享秘密挑战-应答(obs-websocket) | 不能 | 能,弱口令有风险 | 能 |
| 公钥签名 + nonce(OpenClaw 设备身份) | 不能 | 不能 | 能 |
| 换成 wss://,并校验证书 | |||
| 以上任何一种 | 看不到 | 看不到 | 不能 |
OpenClaw 的共享令牌本身仍属于第一行,靠配对审批兜底。
最后一列在 ws:// 上无论怎么设计应用层鉴权都是红的,因为问题不在“怎么证明身份”,而在“证明之后,这条连接上的字节没有任何保护”。要把它变绿,就得给整条连接的每一个字节都加上加密和完整性校验,并且确认对面真的是那台服务器。
七、wss 和 TLS 做了什么
TLS 补上的三件事
wss:// 就是把 WebSocket 跑在 TLS 上18。TLS 握手先于 HTTP 升级请求,所以它保护的范围是全部:URL、Cookie、头部、每一帧。它做了三件应用层鉴权做不到的事:
加密:路径上的人只能看到密文,令牌、应答、消息内容都看不到,图 4 的前两列直接无从谈起。
完整性:每条 TLS 记录都带认证标签,中间有人改一个字节,接收方就会发现并断开。
认证服务器:客户端校验服务器证书,确认对面是自己要连的那台机器。主动中间人拿不出能通过校验的证书,也就无法冒充服务器、转发 challenge。这一步是关键,跳过证书校验的 TLS 挡不住主动中间人。
TLS 只证明服务器是谁,客户端是谁仍然由应用层决定,所以令牌、挑战-应答、设备签名依然有用,只是不用再独自扛着窃听和篡改。
OpenClaw 怎么逼用户用上它
OpenClaw 的客户端在发起连接前就检查地址19:wss:// 一律放行;ws:// 只放行回环地址、私网 IP、.local 和 Tailscale 的 .ts.net。连别的地址会直接报错,不会把令牌发出去:
SECURITY ERROR: Cannot connect to "gw.example.com" over plaintext ws://.Both credentials and chat data would be exposed to network interception.Use wss:// for remote URLs. Safe defaults: keep gateway.bind=loopback andconnect via SSH tunnel (ssh -N -L 18789:127.0.0.1:18789 user@gateway-host),or use Tailscale Serve/Funnel.注释写明这个检查放在读取本地设备令牌之前,免得凭据先被加载出来。确实需要在内网用明文时,要在客户端进程上设置 OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1,而且只认进程环境变量,不能写进 openclaw.json。
它还提供了两样东西,让“确认对面是谁”更牢靠。一是证书指纹钉扎:gateway.remote.tlsFingerprint 或 --tls-fingerprint 填上网关证书的 SHA-256 指纹,客户端只认这一张证书,自签名证书也能安全使用,ws:// 地址配指纹会直接报错。二是默认只监听回环:gateway.bind 默认是 loopback,远程访问推荐走 SSH 隧道或 Tailscale Serve16,这两种方式都在 WebSocket 外面先套了一层加密和认证。设备令牌的自动重试也只在“可信端点”上做,也就是回环地址,或者配了指纹的 wss://。
回到开头的问题:OpenClaw 的 nonce 和签名防的是“令牌或签名被看到后被人拿去重放”“服务端泄露后被人冒充设备”;它不负责让明文 ws 变安全,这件事交给了 TLS、SSH 隧道和只在本机监听,并且在客户端直接拦住了不安全的组合。
八、同类软件的做法对照
把读过源码的几个软件放在一起,能看出两条路线:大多数用不记名令牌,把保密完全交给传输层;少数在应用层做挑战-应答,但也没有一个指望它替代 TLS。
| 软件 | 凭据放在哪 | 凭据形态 | 防重放 | 对明文的态度 |
|---|---|---|---|---|
| OpenClaw 网关 | 第一帧 connect | 共享 / 设备令牌 + Ed25519 设备签名 | 每连接 nonce,签名有效 2 分钟 | 非私网地址拒绝 ws://,支持证书钉扎 |
| obs-websocket 5 | 第一帧 Identify | SHA-256 挑战-应答 | 每连接 challenge | 只有 ws://,靠本机或内网 |
| Home Assistant | 第一帧 auth | JWT 访问令牌,30 分钟;可发长期令牌 | 无,靠过期 | 文档建议远程访问用 HTTPS |
| Jupyter Server | URL 参数、Authorization 头、Cookie | 随机令牌,登录后换 Cookie | 无 | 默认只听本机;检查 Origin |
| VS Code Server | 第一条控制消息 auth | connection token;sign 步骤校验官方客户端 | 无 | 靠 SSH 隧道或本机 |
| code-server | Cookie | 口令的 argon2 哈希 | 无,改口令前一直有效 | 建议 HTTPS;检查 Origin |
| Kubernetes API | Authorization 头或子协议 | 不记名令牌 | 无,靠令牌有效期 | 只走 HTTPS |
“防重放”一列指应用层机制。走 TLS 时,窃听者本来就拿不到可以重放的东西。
为什么大多数软件不在应用层做挑战-应答?从上面的分析可以推出原因:它只能防住被动窃听,主动中间人和后续消息还是得靠 TLS;而一旦用了 TLS,不记名令牌本身就已经安全了,应用层再做一遍收益不大。OpenClaw 做设备签名,主要不是为了替代 TLS,而是为了设备级的身份和授权:每台设备单独审批、单独吊销,服务端不存任何能冒充设备的秘密。obs-websocket 的场景则是局域网里的直播软件,用户基本不会为它配证书,挑战-应答至少保证口令不上线路。
小结
- 明文 ws 上什么是可见的? 升级请求的 URL、Cookie、头部和之后的每一帧都可见。客户端帧的掩码键就在帧头里,掩码是为了防代理缓存投毒,不是加密。
- 令牌通常放在哪? 浏览器不能给 WebSocket 加自定义头,所以放在 URL 参数、Cookie、子协议或第一帧消息里。用 Cookie 的要检查 Origin,防跨站 WebSocket 劫持。它们大多是不记名令牌,明文上看到就能拿去用。
- 发哈希为什么不够? 固定的哈希等于新的口令,照样能重放。要让每次的问题不一样,就是挑战-应答。
- 共享秘密的挑战-应答(obs-websocket)防住了什么? 口令不上线路,听到的应答下次无效;但服务端要存口令,弱口令可能被离线猜出。
- OpenClaw 的设备签名多做了什么? 用 Ed25519 公钥代替共享秘密,服务端只存公钥;nonce 和权限范围都签进载荷,签名只对这条连接、这组权限有效。
- 令牌为什么还要明文发? 共享令牌负责网关这一层,没法签名,只能直接交;被偷后,攻击者仍要过新设备的配对审批,设备令牌没有私钥也用不了。
- 挑战-应答护得住整条会话吗? 护不住。签名只在第一帧,主动中间人可以转发握手、然后读改后续消息;纯 HTTP 下还能改页面。
- wss 补上了什么? 加密、完整性和服务器认证,覆盖从升级请求开始的每一个字节。OpenClaw 在客户端直接拒绝向公网发 ws://,并提供证书钉扎、默认回环监听和 SSH 隧道等安全默认值。
所以开头那个问题的答案是:明文 ws 上令牌确实会被看到,challenge 能让“看到的东西拿去重放”这件事失效,但让整条连接不被偷看、不被篡改,只能靠 wss。
参考资料
Footnotes
-
RFC 6455: The WebSocket Protocolrfc-editor.org ↗第 5.3 节规定客户端帧必须带 4 字节掩码键,第 10.3 节说明掩码是为了防止针对中间代理的缓存投毒,不是为了保密。https://www.rfc-editor.org/rfc/rfc6455 ↩ ↩2
-
本文的调研笔记与实验脚本项目共享目录
websocket-auth-research/:experiment/mask.py构造并还原一帧客户端帧,verify.py是照 OpenClaw 校验逻辑写的玩具校验器,obs_values.py按 obs-websocket 的公式算图 2 里的值。 ↩ ↩2 -
Jupyter Server · Security in the Jupyter Serverjupyter-server.readthedocs.io ↗令牌可以放在 URL 参数
?token=、Authorization: token ...头里,登录后换成 cookie;WebSocket 的 Origin 检查见jupyter_server/base/websocket.py的check_origin。https://jupyter-server.readthedocs.io/en/latest/operators/security.html ↩ -
Cross-Site WebSocket Hijacking (CSWSH)christian-schneider.net ↗https://christian-schneider.net/CrossSiteWebSocketHijacking.html ↩
-
Kubernetes · Authenticating: Putting a bearer token in a requestkubernetes.io ↗浏览器发不了 Authorization 头时,可以把令牌编进子协议
base64url.bearer.authorization.k8s.io.<token>。https://kubernetes.io/docs/reference/access-authn-authz/authentication/ ↩ -
Home Assistant · WebSocket APIdevelopers.home-assistant.io ↗连上后服务端发
auth_required,客户端回{"type": "auth", "access_token": ...}。https://developers.home-assistant.io/docs/api/websocket/ ↩ -
home-assistant/core · websocket_api/auth.pygithub.com ↗访问令牌是 HS256 签名的 JWT,
ACCESS_TOKEN_EXPIRATION = timedelta(minutes=30)(homeassistant/auth/const.py)。https://github.com/home-assistant/core/blob/dev/homeassistant/components/websocket_api/auth.py ↩ -
coder/code-server · src/node/util.tsgithub.com ↗登录后 cookie 的值是口令的 argon2 哈希(
routes/login.ts),之后用safeCompare比较;WebSocket 路由前有ensureOrigin。https://github.com/coder/code-server/blob/main/src/node/util.ts ↩ -
obs-websocket 5.x protocol · Creating an authentication stringgithub.com ↗https://github.com/obsproject/obs-websocket/blob/master/docs/generated/protocol.md ↩
-
obs-websocket/src/utils/Crypto.cppgithub.com ↗
GenerateSecret和CheckAuthenticationString;salt 在WebSocketServer::Start()时生成一次,challenge 每个连接生成一次。https://github.com/obsproject/obs-websocket/blob/master/src/utils/Crypto.cpp ↩ -
microsoft/vscode · remoteExtensionHostAgentServer.tsgithub.com ↗握手先收
auth消息校验 connection token,再发sign请求;签名和校验由闭源模块vsda完成,web 客户端直接把 connection token 当作signedData。https://github.com/microsoft/vscode/blob/main/src/vs/server/node/remoteExtensionHostAgentServer.ts ↩ -
OpenClaw · Gateway protocol authgithub.com ↗共享令牌、设备令牌、设备身份、v2/v3 签名载荷,以及旧客户端迁移时的
DEVICE_AUTH_*错误码。https://github.com/openclaw/openclaw/blob/main/docs/gateway/protocol/auth.md ↩ ↩2 ↩3 -
OpenClaw · Gateway protocol handshakegithub.com ↗
connect.challenge事件和connect请求的完整字段。https://github.com/openclaw/openclaw/blob/main/docs/gateway/protocol/handshake.md ↩ -
openclaw/src/gateway/server/ws-connection/connect-auth.tsgithub.com ↗服务端校验设备签名的代码:
DEVICE_SIGNATURE_SKEW_MS = 2 * 60 * 1000,依次检查 deviceId、signedAt、nonce 和签名。本文读的是 2026-10-26 的0ef5ddd。https://github.com/openclaw/openclaw/blob/main/src/gateway/server/ws-connection/connect-auth.ts ↩ -
openclaw/packages/gateway-client/src/device-auth.tsgithub.com ↗
buildDeviceAuthPayloadV3:签名载荷用|拼接 v3、deviceId、clientId、clientMode、role、scopes、signedAtMs、token、nonce、platform、deviceFamily。https://github.com/openclaw/openclaw/blob/main/packages/gateway-client/src/device-auth.ts ↩ -
OpenClaw · Network exposuregithub.com ↗默认
gateway.bind: loopback,推荐 SSH 隧道或 Tailscale Serve;远程配对需要审批,直连回环自动批准。https://github.com/openclaw/openclaw/blob/main/docs/gateway/security/network-exposure.md ↩ ↩2 -
OpenClaw · Control UI: Connect and pair(Insecure HTTP 一节)github.com ↗原文:签名密钥不离开浏览器,是明文传输唯一泄露不了的凭据;共享令牌则任何路径上的观察者都能读到。主动攻击者还可以改页面。https://github.com/openclaw/openclaw/blob/main/docs/web/control-ui/connect-and-pair.md ↩ ↩2
-
RFC 8446: The Transport Layer Security (TLS) Protocol Version 1.3rfc-editor.org ↗https://www.rfc-editor.org/rfc/rfc8446 ↩
-
openclaw/packages/gateway-client/src/websocket-transport.tsgithub.com ↗
isSecureWebSocketUrl:ws:// 只放行回环、私网 IP、.local和.ts.net,其余报SECURITY ERROR;tlsFingerprint只能配 wss://。https://github.com/openclaw/openclaw/blob/main/packages/gateway-client/src/websocket-transport.ts ↩