Tailworm 安全评估报告:Tailscale 与 headscale 的漏洞分析
Tailworm – Tailscale Vulnerabilities
安全研究员发布了针对 Tailscale v1.104.2 与 headscale v0.29.5 的安全评估报告,详细披露了 11 项漏洞。报告指出,SSH Unix 套接字转发、应用连接器(conn25)及服务端功能存在权限边界缺陷、TOCTOU 竞态条件和拒绝服务风险,允许攻击者在特定条件下获取目标主机的 root 权限或绕过网络策略。
原文披露了 Tailscale v1.104.2 和 headscale v0.29.5 的多个高危安全漏洞与设计缺陷,读者可以据此评估零信任网络架构中的特权边界和代理实现风险。
译文尚不完整,完整内容请切换到原文。
Tailworm研究人员指出安全评估报告 — Tailscale v1.104.2 和 headscale v0.29.5高层摘要面向业务读者的执行摘要发现与经验证的复现背景:发现 1–3 所依赖的身份验证模型背景:攻击可用的侦察和枚举背景:应用连接器与 conn25 功能发现 1 (T3) — ssh -L unix-socket 转发以 root 身份拨号(写入仅限 root 的套接字) — 严重性:高发现 2 (T2) — ssh -R unix-socket 转发替换 root 拥有的套接字 (BindUnlink) — 严重性:高发现 3 (T2-RACE / T3-RACE) — TOCTOU 绕过路径白名单(绑定和拨号端) — 严重性:高发现 4 (C1) — conn25:用户权限授予通过连接器的不受限制的出站选择(权限边界缺陷) — 严重性:高发现 5 (T1) — gliderssh 会话引脚:root-tailscaled OOM 循环 — 严重性:高发现 6 — headscale v0.29.5:C2/C4(内存耗尽) + C3 上游客户端炸弹 — 严重性:中高套接字目标变体发现 7 (SERVE-F-1) — serve --tcp 到 localapi 套接字:远程允许写入 localapi — 严重性:高发现 8 (SERVE-F-2) — isTailscaledSocket 防护仅在配置时生效 (TOCTOU) — 严重性:中(受控)发现 9 (SERVE-F-3) — funnel 权利强制仅限对端能力 + 本地配置(节点属性仅限 CLI) — 严重性:中(威胁模型发现)发现 10 (SERVE-F-4) — TS_SERVE_ALLOW_ALL_INTERFACES 将 serve 暴露在隧道绑定之外 — 配置说明发现 11 (SERVE-F-5) — h2 共享 serve-context 竞态:跨挂载能力泄漏 → 挂载级授权绕过 — 在标头授权部署中为中;否则为低到中Serve/funnel 确定的负面结果(证据 + poc-servefunnel/ 中的 FINDINGS.md)负面结果(已测量)类别上下文(对比审计)检查模式(基于时间的访问授权):范围和限制推荐的修复方案针对运维人员的临时缓解措施(未修补的部署)厂商端的根本原因修复(针对维护人员;运维人员无法操作)
TailwormResearchers noteSecurity Assessment Report — Tailscale v1.104.2 and headscale v0.29.5High-level summaryExecutive summary for business readersFindings and verified reproductionsBackground: the authentication model that findings 1–3 build onBackground: recon and enumeration available to an attackerBackground: app connectors and the conn25 featureFinding 1 (T3) — ssh -L unix-socket forwarding dials as root (writes to root-only sockets) — Severity: HighFinding 2 (T2) — ssh -R unix-socket forwarding replaces root-owned sockets (BindUnlink) — Severity: HighFinding 3 (T2-RACE / T3-RACE) — TOCTOU defeats the path allowlist (bind and dial sides) — Severity: HighFinding 4 (C1) — conn25: a user entitlement confers unrestricted egress selection through the connector (privilege-boundary flaw) — Severity: HighFinding 5 (T1) — gliderssh session pin: root-tailscaled OOM loop — Severity: HighFinding 6 — headscale v0.29.5: C2/C4 (memory exhaustion) + C3 upstream client bomb — Severity: Medium-HighSocket-target variationsFinding 7 (SERVE-F-1) — serve --tcp to the localapi socket: remote permit-write localapi — Severity: HighFinding 8 (SERVE-F-2) — isTailscaledSocket guard is config-time-only (TOCTOU) — Severity: Medium (contained)Finding 9 (SERVE-F-3) — funnel entitlement enforcement is peer-cap + local config only (the node attribute is CLI-only) — Severity: Medium (threat-model finding)Finding 10 (SERVE-F-4) — TS_SERVE_ALLOW_ALL_INTERFACES exposes serve outside the tunnel bind — Configuration noteFinding 11 (SERVE-F-5) — h2 shared serve-context race: cross-mount capability bleed → mount-level authorization bypass — Severity: Medium within header-authorization deployments; Low-Medium otherwiseServe/funnel clean negatives (evidence + FINDINGS.md in poc-servefunnel/)Negative results (measured)Class context (comparative audit)Check mode (time-based access grants): scope and limitsRecommended remediationInterim mitigations for operators (unpatched deployments)Vendor-side root-cause fixes (for the maintainers; not operator-actionable)
Tailworm
we have zero-trust in our infrastructure
Researchers note
The design choices of the open source tailscale and headscale software have created an environment ripe for risk, while marketing security as its design and purpose. Choices have been made to move away from security-in-depth, removing layers of authentication, and replacing daemons such as OpenSSH which employ privilege-separation with a highly-privileged process. Other findings include broader-than-advertised network security boundaries, which seems to be legitimate bugs, but the missing security and isolation is precisely what users are advertised by these tools.
Regarding disclosure timelines, it is of the researcher's opinion that infrastructure is vulnerable today. This report strictly audits the open source software provided to users under no condition of warranty per the BSD-3 license. As such, the researchers, and the users, are effectively developers of said software per its open source nature.
相关软件拥有企业赞助商。Tailscale Inc. 是一家营利性机构,雇用开发人员来构建安全软件。研究人员与 Tailscale 公司没有关联,未受雇于该公司,也没有被签约或付费来进行或报告这项研究。所有研究均在研究人员的系统和设备上针对开源软件开展。
研究人员认为,迅速认识到这些缺陷将加速受影响设备的修复。这些漏洞并非由研究人员引入,而是被发现的,且发现过程付出的努力较少。
-- Erica Windisch,Cyberdione Labs Corporation
安全评估报告 — Tailscale v1.104.2 和 headscale v0.29.5
测试窗口:2026-10-22 至 2026-10-24。所有发现均通过在实验室环境中对未修改的已发布软件(tailscale v1.104.2、headscale v0.29.5)进行复现来验证。实验室和组织标识已被红字或遮挡处理。发现标识:T1–T3、C1–C4 和 F-1–F-5 是内部审计标签,保留以便复现工具包可以进行交叉引用。
高层摘要
审计内容。Tailscale 客户端代理 v1.104.2(tailscaled、其内置的 Tailscale SSH 服务器、其 serve/funnel 功能以及其 DERP 中继客户端)、headscale v0.29.5 自托管控制服务器、两者构建时基于的 gliderssh 库,以及实验性的 conn25 应用连接器功能。发现分为五类;每个类别都指明了受影响的组件。
| # | 组件 / 功能 | 机制(出问题的地方) | 攻击者要求 | 主要结果 |
|---|---|---|---|---|
| 1 | Tailscale SSH(tailscaled 的内置 SSH 服务器,以 root 身份运行) | Unix 套接字转发(ssh -R/-L)以 root 身份执行路径检查和文件操作;竞态条件和缺少权限降级使得普通会话能够替换、拨号或删除特权资源 |
一台获批的设备 + 一条 SSH 策略规则(在 headscale 上自动授予转发权限;在 SaaS 上按规则授予) | 在目标服务器上以 root 身份执行;捕获注册密钥;任意 root 文件删除 |
| 2 | Tailscale 应用连接器(conn25,实验性) | 连接器将流量转换为攻击者选择的目标而无需验证;绝不会针对真实目标查询网络 ACL | 被授予连接器功能的设备 | 触达云元数据(凭据窃取)、内部局域网,以及 — 在 Kubernetes 中 — 每个集群服务 |
| 3 | headscale 控制服务器(自托管部署) | 未经身份验证的资源耗尽(内存增长、缓存驱逐) | 互联网可达性;无账户 | 控制服务器崩溃循环;全组织范围内阻止新设备加入 |
| 4 | Tailscale DERP 客户端(每个代理中的中继连接) | 对恶意中继响应的无界读取 | 将客户端定位/指向敌意中继的能力 | 客户端代理内存耗尽 |
| 5 | tailscale serve(本地服务暴露) | TCP 转发到 unix 套接字缺少 HTTP 路径上存在的防护;加上每个挂载授权中的竞态条件 | 运维人员配置转发(或对端在挂载检查中进行竞争) | 对节点的远程管理 API 访问;少数请求绕过应用授权 |
初始入侵可能如何发生。类别 1 假设攻击者已经在某台设备上获得了立足点——这通常通过常规途径发生:被攻击的网络应用(脆弱的 PHP 页面或 CMS 插件)、恶意构建依赖项(npm/PyPI 软件包、Docker 基础镜像、GitHub Action)、钓鱼植入程序、供应链妥协或被盗的身份凭证。Tailscale 并未创造该立足点;研究发现,该网格以机器速度将一个普通的立足点转化为整个舰队级别的特权访问,因为设备间路径是由假定受信任端点的策略授予的,而代理的特权操作可从无特权会话中访问。
默认部署中的实际风险。启用 Tailscale SSH 时(每个节点可选,通常是全舰队):一个被攻陷的账户可以访问其他服务器上的 root 权限(类别 1),并且在默认(未启用 Tailnet-Lock)配置中,以具有自动批准路由的合法标记服务器身份加入攻击者机器。配置了应用连接器时:对云凭证和 Kubernetes 服务的策略不可见访问(类别 2)。使用自托管且可从互联网访问的 headscale 时:未认证的崩溃循环和新设备引导锁定(类别 3)。任何指向敌对中继的客户端:代理耗尽(类别 4)。配置了 serve 转发的任何节点:远程 admin-API 暴露(类别 5)。
可蠕虫传播性。在实验室中测得:策略授予的 Tailscale SSH 加上类别 1 的发现允许自动传播到策略允许的每个设备——没有密码,没有被盗密钥,无需用户交互,每跳不到一秒钟。两个结构性限制依然有效:逐对访问授权限制了传播(阻止它的唯一控制措施),硬件绑定的设备凭证(TPM/Secure Enclave)防止了导致妥协持续存在的身份盗窃。所有其他控制措施改变的是可见性速度,而不是传播速度。
缓解措施(按效果排序)。(1) 逐对 SSH 授权——绝不使用身份范围或标记范围的接受规则。(2) 禁用 SSH 转发:在 SaaS 上,将每个规则的转发字段设置为 false;在 headscale v0.29.5 上无法禁用转发——它在每个被接受的会话上无条件授予,因此在修复之前,请将每个 headscale SSH 授权视为等同于 root。将 SSH 限制为非特权账户只是纵深防御——它并不能缓解这些发现,这些发现是从完全无特权的账户中运行的。(3) 服务器舰队上的节点密钥过期;TPM/Secure-Enclave 绑定的代理状态。(4) 将安全遥测保持在被攻陷服务器无法到达的基础设施上——经过自我测试,并带有覆盖率报告。(5) 限制连接器权限;连接器命名空间上的目标白名单或 Kubernetes 默认拒绝出口。(6) 建立在控制平面上的设备删除基础之上的事件响应:节点的密钥保存在具有 root 权限的攻击者可以复制的文件中,重新镜像主机并不会撤销已经失窃的副本——只有控制平面删除(或密钥过期)才可以。(7) 自托管控制平面:内存限制、重启监督、注册速率节流。厂商修复规模小且有先例(OpenSSH、Dropbear);上述内容在修复发布之前和之后都能保护你。
面向商业读者的执行摘要
- Tailscale SSH 套接字处理中的竞态条件(发现 1–3、7)。拥有企业内网网状网络普通批准访问权限的人——无需密码、管理员权限或专用软件——就可以接管该服务器上最高特权服务所使用的内部通信渠道,包括容器平台和 init 系统。演示表明,攻击者可以获得完整的管理员控制权,以及单独捕获用于注册新设备的凭据。只要启用了 Tailscale SSH,就会存在这种暴露风险;修复方法很小,并且已经在两个竞争产品中得到了验证。
- 转发功能以完整系统权限运行(发现 1、3–5)。获得批准的同一用户可以以安全策略永远无法察觉的方式重定向流量:访问仅通过监听机器内部接口来“保护”的管理工具、在机器的公共地址上打开监听端口,以及读取本应只有操作系统才能读取的文件。
- 设备注册凭据窃取形成闭环(完整影响链)。J通过接管套接字,攻击者可以捕获将机器添加到网络的密钥,并加入他们自己的机器——伪装成带有自动批准路由的合法标记服务器。在默认配置中,这会将一个被攻陷的账户转化为持久的、看起来官方的网络存在。
- 应用连接器(app-connector)功能绕过了网络政策(发现 4)。验证漏洞允许获批用户将流量路由到安全团队从未授权的目的地——包括发放服务器凭据的云元数据服务,以及 Kubernetes 中的每个内部服务。
- 拒绝服务与锁定(发现 5–6)。未经身份验证的互联网用户可以耗尽控制服务器的内存或驱逐合法的设备批准——阻止新设备加入。这是可用性攻击,而非数据窃取,但它们可能会使远程访问基础设施离线。
- 服务功能暴露了本地管理 API(发现 7)。一行配置就会将代理的管理接口暴露给远程对等节点——即服务器的管理控制台——并且相关的竞态条件允许一小部分请求绕过应用程序权限检查。
- 支出条款。准入要求很普通:一个获批账户,或者对于拒绝服务项目而言根本不需要账户。修复集中在三个地方——每对访问规则、绑定硬件的凭据以及超出被攻陷主机范围的遥测——这更多是流程上的变更而非采购,详情见下文的修复章节。
发现与验证复现
背景:发现 1–3 所建立的身份验证模型
Tailscale SSH 不执行用户级身份验证。SSH 协议的身份验证阶段通过方法 none 完成:WireGuard 层已经以密码学方式识别了该设备,并且该设备身份(而非密码、密钥对或用户凭据)才是 SSH 策略所评估的内容。因此,授权完全是一个策略规则(源设备到目标设备,规则中指定了目标账户),在此模型下建立的会话在其整个生命周期内都具有发现 1–3 的转发能力。对威胁建模的两个影响:攻击者除了在任何策略允许的设备上获得立足点外,不需要任何凭据材料;并且设备遭妥协与用户无关——获权设备上任何用户的恶意软件都拥有该设备的完全授权。这种模型也是下文所测量的蠕虫传播能力得以实现的原因:传播不需要密码,也不需要被盗密钥。
背景:攻击者可进行侦察与枚举
两个设计属性使攻击者能够以零检测风险获得完整的定位知识:
- 节点枚举是无条件的。 无论访问策略允许它访问什么,每个节点都会收到完整的 tailnet 节点列表(设备名称、地址、标签)。任何单台设备上的攻击者都可以从其自身的本地状态枚举组织中的每个潜在目标。
- 可达性探测是无声的且具三种状态的。 对任何节点的连接尝试都可以区分策略丢弃(超时 — ACL 拒绝)、策略拒绝(快速拒绝 — 代理响应但规则集拒绝)和允许(服务响应)。并行探测所有节点可以在几分钟内绘制出整个可达范围(策略实际允许的内容),且不会在任何地方产生警报(就任何传感器所见而言,每个探测都是一次普通的失败或成功连接)。
这些共同意味着攻击者的定位不受发现的限制:网状网络交给了他们目标列表,而一次安静的扫描交给了他们策略图。然后,发现 1–4 将该图转换为访问权限。
背景:应用连接器和 conn25 功能
Tailscale 的应用连接器(app-connector)功能允许组织通过指定的网关设备将流量路由到外部 SaaS 服务(或内部应用程序),以便对这些服务的访问看起来像是来自组织的地址,并且可以在一个地方进行管理。管理员指定连接器设备,定义应用(命名的服务定义,通常是域),并授予选定设备通过该连接器使用给定应用的权限。在预期操作中,客户端解析应用的域,被引导至连接器,然后连接器将流转发到应用的真实目的地。
conn25 是一种重新设计的连接器机制(在 v1.104.2 中存在但属于实验性功能),构建于按流映射之上:获得授权的客户端通过 tailnet 的对端 API 向连接器注册一个映射——“当我向中转地址 X 发送流量时,将其转换为真实的目地地址 Y”——连接器随后会安装数据包过滤规则并在其数据路径中执行转换。有两项实现属性对于发现 4 至关重要:客户端提供的目标地址 Y 仅进行了地址族验证(从未与应用程序配置的目标地址进行比对),并且连接器的入站规则在 tailnet 的访问控制评估运行之前就接受了中转池流量。在生产环境中,该机制主要通过 Kubernetes 运算符部署,它在集群的服务和 Pod 网络中运行具有完整容器特权的连接器 Pod。
对于威胁建模:管理员授予的授权是“此设备可以通过连接器使用应用程序 X”——但实现中的任何内容都没有将转换后的目标地址绑定到应用程序 X。读者在评估发现 4 时应牢记这一差距:该功能将路由决策委托给客户端,而授权决策则是针对应用程序做出的。
发现 1 (T3) — ssh -L unix 套接字转发以 root 身份拨号(写入仅限 root 的套接字) — 严重性:高
受影响的组件/版本:tailscaled v1.104.2 中的 Tailscale SSH 服务器(gliderssh streamlocal 本地转发)。先决条件:允许攻击者连接到目标的 SSH 策略规则,且允许本地转发。不需要 sudo、docker 组成员身份或受害者端守护进程访问权限。
具有允许本地转发的 SSH 策略规则的经过身份验证的对端,可以使具有 root 特权的 tailscaled 进程代表该对端连接到仅限 root 访问的 unix 套接字:
ssh -fNT -L /tmp/relay.sock:/tmp/docker.sock user@target # dockerd at an allowlisted-dir socket
docker -H unix:///tmp/relay.sock run --rm -v /:/host busybox sh -c 'id; head -c 40 /host/etc/shadow'
验证结果:uid=0(root) 加 /etc/shadow 内容,在 0 秒测量的经过时间内存,无需 sudo、docker 组或受害者守护进程参与。范围说明:静态路径白名单 {home, /tmp, /run/user} 被发现 3 绕过。复现:poc-docker/tailscale-privesc.sh — 发现 1 的单命令演示(阴性对照加上利用链),按操作员最初编写的字节原样交付,以保持与先前发布承诺的哈希连续性。sha256: c84d461603a2b48023606149b6808e3dfc0733abc6cc38d4b52e0618c3851a2d修复:参见“建议修复”(发现 1–3 具有相同的根本原因)。
发现 2 (T2) — ssh -R unix 套接字转发替换了 root 拥有的套接字 (BindUnlink) — 严重性:高
受影响组件/版本:tailscaled v1.104.2 中的 Tailscale SSH 服务器(gliderssh streamlocal 远程端口转发;BindUnlink 行为)。前提条件:同发现 1,且允许远程转发。影响:具有 root 权限的 tailscaled 取消链接受粘滞目录保护、root 拥有的套接字,并在该路径绑定攻击者的转发监听器,从而冒充该服务。对于命令通道守护进程,这是直接的 root 代码执行(已验证:一条精心构造的消息 → uid=0)。对于 Docker:明文捕获注册表凭据(观察到明文 POST)并控制对未固定标签(unpinned tags)的拉取;宿主机 root 权限需要进一步的攻击步骤(使用捕获的凭据进行标签投毒)。复现:poc-docker/(环境设置、两条命令、预期输出;已验证实验室演示)。修复:参见“建议修复”(发现 1–3 具有共同的根本原因)。
发现 3(T2-RACE / T3-RACE)— TOCTOU 绕过路径白名单(绑定和拨号端) — 严重程度:高
受影响组件/版本:tailscaled v1.104.2 中的 Tailscale SSH 服务器(gliderssh 中的 validateAndResolve{Listen,Socket}Path 路径验证)。前提条件:同发现 1–2(允许转发的 SSH 接受规则;竞态攻击者在攻击者的 SSH 会话内以无特权身份运行)。
validateAndResolve{Listen,Socket}Path 在检查时验证词法路径;unlink/bind 和拨号稍后在内核重新解析路径时发生。在 SSH 会话中运行的 rename(2) 竞争循环(~170k 路径状态翻转/秒)可以赢得检查到使用(check-to-use)的时间差:- 绑定端:root tailscaled 取消链接了 /run 下活跃的归 root 所有的控制套接字,并在真实路径上绑定了攻击者的转发(0600 监听器指纹);root CLI localapi 流量被转发到主机外;捕获到了包含 authkey 的按字节精确的 POST /localapi/v0/start(未执行注册)。复现:poc-t2race/(racer.py + attack.sh;预期:受害者套接字 inode 被替换,捕获的请求在 capture.log 中)。- 通过经竞争的默认 dockerd 进行端到端 root 执行 (GAP-A1):通过 -L 到 /run/docker.sock 在默认 systemd 管理的 Docker 守护进程上的一条经竞争的保持活动连接,完成了容器创建(Binds=/:/host)、启动和执行,并挂载了 uid=0 和主机 root 文件系统(mountinfo 证据)。范围说明:在强制实施 SELinux 的测试发行版上,主机文件读取被 MLS 策略阻止(挂载和执行成功);没有同等限制的发行版允许完全的主机读/写。- Kubelet 可达性 (GAP-A2):对 kubelet 套接字存根进行了 111 次经竞争的拨号,每个接受的对端都呈现了 uid=0 comm=tailscaled ——向 kubelet API 传递 root 凭据是机械化的;未执行 API 滥用。- 控制平面 CLI 捕获 (GAP-A3):预认证崩溃循环 → 保持套接字重新绑定 → headscale 管理员 CLI 流量通过被劫持的套接字被捕获为明文 h2c(解码的 gRPC 标头;headscale CLI 在其 unix 套接字上不使用 TLS)。- 完整影响链(在实验室虚拟机中进行了端到端演示):持久重新绑定在真实控制 socket 上保持 ≥4 分钟 → HTTP 感知的中间人 → 按字节精确捕获的活跃 authkey → 在临时节点上使用 → 一个恶意节点以受害者的身份加入了 tailnet(演示了锁定禁用/默认情况端到端;对于锁定启用情况,TKA disable-secret 和签名密钥材料在传输中被捕获,但未执行实际的禁用);验证了一个钓鱼级的响应者(向 tailscale status 提供攻击者控制的 JSON,退出码为 0)。- 附带发现 (确认的 gliderssh 子变体;已纠正归属):套接字在 root umask (0755) 下创建,仅在 listen 之后 chmod 到 0600;在 Listen 和 Chmod 之间翻转路径解析的竞争者 (a) 将攻击者的绑定保留在 0755(取证特征),以及 (b) 将 root chmod 应用于 活的 守护进程套接字 —— 将漏洞扩展到任意路径的 root chmod-0600(权限移除拒绝服务)。早期的注释将 0755 绑定归因于原版 OpenSSH;对实时 sshd 配置(streamlocalbindmask 0177、streamlocalbindunlink no)和身份验证日志(竞争窗口中没有登录)的验证表明这是错误归因,现已更正。原版 OpenSSH 仅保持结构上的类比;未在其中演示该效果,也不是任何披露声明的一部分。- 拨号端:/run/systemd/private 的 root 凭据拨号;systemd 授权竞争获胜的连接为 root(AUTH EXTERNAL 30 → OK);重放 busctl 字节 → StartTransientUnit → 一个 root 反向 shell(uid=0;演示为对已经赢得的连接的本地控制 —— 通过竞争利用进行交付是间歇性的,0–4%)。复现:poc-t3race/(racer.py + attack.sh + D-Bus 有效载荷二进制文件;README 中的操作说明,包括 AUTH-uid 掩蔽和关闭竞争窗口的 strace)。
影响:{home, /tmp, /run/user} 白名单并不是一个安全边界。
端到端利用(完整影响链):成功的绑定保持打开状态(经验证 ≥4 分钟),且具有 localapi 感知的中间人会替换监听器。演示的攻击链:从 POST /localapi/v0/start 获取字节级精确的 authkey → 捕获的密钥用于临时节点 → 恶意节点注册并上线(针对未启用锁定的 tailnet,以受害者的身份进行未授权的 tailnet 注册;注册已在实验室中执行并还原)。启用锁定的路径:tka/disable 将禁用机密作为原始请求正文发送,且 tka/wrap-preauth-key 生成可通过锁定的注册材料 —— 两者都在传输过程中被捕获(来自任何节点的提交都不需要签名)。响应者模式使真实的 tailscale status 打印攻击者控制的 JSON,退出码为 0(达到钓鱼级别)。新功能扩展 —— chmod 变体和 unlink 门控绕过:监听后的 chmod 在使用时遵循词法路径;竞态攻击者可以将其重定向到活动的守护进程套接字(观察到相同 inode 的 666→600 转换)⇒ 普通文件也存在任意路径的 root chmod-0600(演示了对 root 拥有的 /var 文件从 644 到 600 的修改;root 路径下 0755 模式的套接字是错过 chmod 的取证特征)。此外,unlink 类型门控可以被绕过:该门控对会话自身的 /tmp 路径执行 Lstat(是一个套接字,因此检查通过),但随后的 unlink() 通过竞态攻击者的符号链接重新解析 —— root 会取消链接任意常规文件(捕获到 6 个实时命中;已在测试文件上进行演示)。这修正了先前关于绑定路径破坏受套接字类型限制的范围界定声明:无特权的 SSH 会话可以删除任意 root 拥有的文件并将其 chmod 为 0600 —— 这是除内容修改之外的针对性可用性/破坏原语。复现:poc-t2race-unlock/ 和 gap-a/poc-a4-chmod/(均有证据;已对实时实验室凭据进行脱敏)。
相关漏洞历史:Dropbear CVE-2025-14282(2024.84–2025.88)是拨号端上的完全同一类漏洞 —— 通过 direct-streamlocal 的 root SO_PEERCRED。Dropbear 的修复(2025.89:在 USERAUTH_SUCCESS 内部、任何通道/全局请求之前,setuid 为会话用户)是此处推荐的权限降级修复系列;当前的 Dropbear master(17549ea6)在默认构建下对此类漏洞审计干净(其 streamlocal 仍然没有路径验证和无条件 unlink,但以会话用户的 uid 运行)。OpenSSH(master 26b1363c,经过源码审计):对此类漏洞呈干净的阴性 —— 认证后的子进程在服务器循环之前降权到会话用户,因此每个 streamlocal 绑定/取消链接/拨号都在用户的 uid 下运行(monitor 从不接触路径;由于不存在检查阶段,因此不存在检查/使用间隙)。上游 master b37c095a 现在在以 root 身份运行认证后的 sshd 时,会强制禁用 streamlocal(外加 GatewayPorts 以及小于 1024 的远程转发端口)—— 这是对这些功能假定用户特权的显式承认(gliderssh 违反了相同的假定)。残余的策略差距,而非特权越权:PermitListen 完全不约束 unix 路径(自 2018 年以来的公开 XXX);PermitOpen 对于 unix 拨号会故障关闭。OpenSSH 10.6p1 cee6aedb(来自 /tmp 的代理套接字、dirfd + O_NOFOLLOW 清理)是下方 dirfd 修复系列的直接先例。修复:请参阅“建议的修复方案”(发现 1 至 3 共享一个根本原因)。
发现 4 (C1) — conn25:用户授权通过连接器赋予了不受限制的出站选择(权限边界缺陷)—— 严重程度:高
受影响的组件 / 版本: tailscale v1.104.2、feature/conn25 连接器数据路径(测试中的未修改代码)。
断言。 按应用划分的 USER 权限(conn25 客户端能力,在为普通用户分配应用时授予)会暗中赋予网络管理能力:通过共享连接器选择任意出口目的地,且无需审批步骤、缺乏可见性,也无需对真实目的地进行策略评估。
预期边界与实际边界。 预期:应用权限将用户限制在应用认可的后端范围内(管理员配置的域、连接器端的 DoH 发现——根据已发布的协议)。实际:仅进行族匹配加上传输池成员资格。目的地在运行时由客户端通过注册 API 声明,且从未经过验证;传输目的地在过滤前被强制接受;DNAT 重写发生在过滤判定之后——真实目的地永远不是策略对象。核心危害是不受限制的出口选择:运维人员的模型是“连接器出站到应用批准的后端”(文档中记录的 IP 白名单存在是为了让远程系统能够接入连接器的公共 IP),但没有任何机制强制限制出口集合——拥有权限的对等节点可以选择任何目的地(任意子网、连接器自身的局域网和环回、IMDS、互联网),并且流量伪装成连接器进行出站(经由其 WAN 验证,再反向转换回来)。与记录在案的白名单的交互(分析得出,未经实验室演示):白名单验证的是连接器,而不是用户,因此当多个应用共享一个连接器出口 IP 时(一个连接器主机服务于多个应用——这是常见部署),该机制就会失效:有权访问应用 X 的对等节点转向访问应用 Y 的白名单端点时,流量将从受信任的连接器 IP 发出。在每个连接器 IP 对应一个应用的情况下,跨应用转向到白名单应用的流量仍然会被过滤;而未列入白名单的内部目标(IMDS、局域网服务)无论白名单如何设置都是可达的。
范围与局限性。 这并非一种新颖的拦截上限:带有标签的子网路由器或出口节点注册也能达到相同的最终状态。漏洞在于达到该状态的路径——这是一个用户级别的触发器,而不是基础设施角色;没有审批步骤(子网路由是经过审批且可见的;conn25 目的地则永远不是);没有治理痕迹;并且在子网路径上用于管理这些相同目的地的授权在传输路径上从未被参考。
严重性评估。 影响半径 = 被授予任何 conn25 应用的所有用户。前置条件 = 配置了该功能且攻击者拥有普通的应用程序权限——转向本身不需要任何漏洞利用。超越转向的提权需要具备已打标签的注册(前置条件阶梯):
| 路径 | 要求 | 产出 |
|---|---|---|
| 普通权限 | 管理员将应用授予了您的用户 | 本发现的转向(无需漏洞利用) |
| 泄露的可重用标签密钥 | 配置 hygiene 失败 | 恶意连接器(HA 路由选择、DNS 钩子) + autoApprover 路由授权 |
| 完整影响链环节(发现 3) | T2-RACE + 运维人员通过被劫持的套接字运行 up --auth-key(带标签) |
同上,通过 SSH 接受规则立足点使其变得可靠可利用 |
| 受compromised的带标签主机 | 主机妥协 | 同上,外加活动连接器的流量 |
机制(在未修改的 v1.104.2 上验证)。注册接受中转 IP→任意目标 IP(仅检查地址族和地址池);入站挂钩在 ACL 评估之前强制接受中转池目标;DNAT 在过滤器判定之后生效;源身份保留到连接器的出站端(互联网流量被伪装)。Tailnet 覆盖网络目标在接收端的 WireGuard 加密密钥路由处被丢弃(客户端源得以保留)——枢纽可达的集合正好是连接器自身的网络位置(其局域网、上行链路、IMDS、自身);由其他节点的批准路由服务的目标也会在加密密钥路由处失效。
证据。测试工具:conn25/poc-conn25-test.go(在官方代码上驱动 wgengine/filter + feature/conn25;所有步骤均通过,包括零 ACL 接受和阴性对照)。实时局域网枢纽(2026-10-24):poc-conn25-docker/(由零授权对端驱动的根等效 tcp 上的 docker API)和 poc-conn25-wp/(内部 PHP 应用;凭据拦截位置;互联网出站数据包捕获)。测试环境前提条件和有效性威胁:每个 PoC 目录中的 RIG.md(授权、地址族和地址池检查均实时运行;未授权的对端被拒绝)。
治理态势。conn25 凌驾于路由治理和授权评估之上:生产环境 conn25 不发布任何内容(魔法范围在客户端本地安装;中转 IP 随每个对端的 WireGuard allowed-IPs 传输;目标仅存在于连接器的运行时 DNAT 表中,约 1 小时过期,可轻松续期)——autoApprovers/routes 既看不到中转池也看不到任何目标;授权只控制能力生成。
可部署的遏制措施。mitigation-conn25/ —— 连接器上的内核层目标白名单(针对 tailnet 源流量的 FORWARD + INPUT 策略链):实现了代码所缺乏的目标验证;硬阻断 IMDS/内部/环回枢纽;注意事项:按连接器而非按对端执行,且 SaaS 目标出站会削弱默认拒绝尾部。修复:真正的修复是在注册时进行按应用的的目标验证。
生产路径(Kubernetes;在实验室虚拟机中通过官方 operator CR→STS→pod 流程验证):operator 部署的应用连接器在 pod 网络命名空间中以 privileged/uid-0 运行 —— 其位置 = 整个 Service CIDR + pod CIDR + 节点本地 TCP + 互联网出站。一个获得授权的对端注册了中转→kube-apiserver ClusterIP、跨命名空间 Service 以及节点 IP(全部被接受,无目标验证)并通过连接器到达它们:跨命名空间 Service 200 OK(未认证响应),kube-apiserver 和 kubelet 10250 可达(401 — 位置已送达,仍需凭据)。影响范围 = 任何获得授权的对端的每个集群 CIDR。操作风险:在非 Tailscale 控制平面下,连接器报告 ConnectorReady 但处于静默休眠状态。遏制措施:在连接器命名空间上设置默认拒绝的出站 NetworkPolicy(集群内部的目标白名单等效项)。
发现 5 (T1) — gliderssh 会话固定:root-tailscaled OOM 循环 — 严重性:高
受影响的组件/版本:tailscaled v1.104.2 中的 Tailscale SSH 服务器;此问题继承自上游的 gliderlabs/ssh v0.3.8。前提条件:经过身份验证的 SSH 会话(按策略)。影响:窗口变更阻塞发送会为每个会话通道挂起一个 goroutine;环境变量追加无限制;7 秒内分配了约 536 MB,断开连接后持续超过 100 秒——这是具有 root 权限的守护进程中的内存耗尽状况。复现:打开 SSH 会话,发送窗口变更/环境变量频繁变动,观察 RSS。修复:修复属于上游的 gliderlabs/ssh(非阻塞发送/有界缓冲区);在此次审计中未发现 tailscale 端的变通方法。
发现 6 — headscale v0.29.5:C2/C4(内存耗尽)+ C3 上游客户端炸弹 — 严重程度:中高
受影响的组件/版本:headscale v0.29.5(内置 DERP,noise 认证缓存);用于 C3 的 tailscale 客户端 derphttp/magicsock。影响:内置 DERP 的 sendHistory 增长(需要经过身份验证的内部人员;缺少打包引入(vendored)的修复);noise 认证缓存驱逐 = 节点接入预认证拒绝(1024 个微小注册驱逐了待处理的批准;故障时关闭(fails closed));derphttp ReadAll 炸弹(10 MB → 1.34 GB;错误字符串嵌入了主体,测得 586 MB;magicsock 重试保留使第一个峰值成为底线)。复现:每个 README / 原始工作树报告中的草图。修复:引入上游内置 DERP 修复;限制 noise 认证缓存;限制 derphttp 响应读取。
Socket 目标变体
已演示(文件中有证据):docker /tmp/docker.sock(此工具包)、命令通道守护进程(直接 root)、/run/systemd/private(root shell)、tailscaled localapi(authkey 捕获)、回环 TCP。该机制普遍适用;未构建有效载荷:/run/docker.sock、containerd/crio、系统 D-Bus、kubelet、本地数据库套接字——可通过竞态复现工具包访问;每个都需要其自身的 API 有效载荷。
发现 7 (SERVE-F-1) — serve --tcp 到 localapi socket:远程 permit-write localapi — 严重程度:高
受影响的组件/版本:tailscale v1.104.2 (e26db7a5)、ipn/ipnlocal serve TCP 转发。前提条件:serve 配置访问权限(本地操作员);用于使用该转发的经 ACL 授权的对端。
命令行界面(CLI)和守护进程接受 tailscale serve --bg --tcp <port> unix:/run/tailscale/tailscaled.sock;原始 TCP 转发将客户端字节原封不动地传入由 tailscaled 本身拨号的 localapi socket(对端凭据 = 守护进程 uid ⇒ PermitRead + PermitWrite)。实验室验证:GET /localapi/v0/status 和 PATCH /v0/prefs 均从远程对端返回 200 = 节点接管,无竞态(与发现 3 相比,这是到达 localapi 位置的第二条、更干净的路线;前提条件是操作员的 serve 配置访问权限,而不是 SSH 接受规则——完整影响链的所有步骤完全适用)。同一 socket 上的 unix-HTTP-proxy 路径在守护进程端被阻止(“cannot proxy to tailscaled socket”):提交 50f1c285b(unix TCP 转发,无防护)和 f4d34f38b(unix HTTP 代理,有防护)之间遗漏了防护一致性。修复:对 TCPForward unix 后端应用相同的防护(在配置时和每次拨号时)。funnel 变体(internet→localapi)另外受到上游控制铸造的 funnel 属性的限制;守护进程端不执行属性检查(发现 9)。复现:poc-servefunnel/(poc-sf1 脚本,receipts/sf1-*)。
发现 8 (SERVE-F-2) — isTailscaledSocket 保护仅在配置时生效 (TOCTOU) — 严重性:中(受控)
受影响的组件/版本: tailscale v1.104.2 (e26db7a5), ipn/ipnlocal serve unix-proxy guard。先决条件:能够在配置后用符号链接替换配置的 unix 后端路径(对路径目录拥有本地写权限)。
该保护在每次 serve-config 重新加载时运行一次(os.Stat 会跟随符号链接;配置时存在的符号链接确实会被捕获);拨号操作则是每请求进行一次。在配置之后将配置好的良性 unix 后端交换为符号链接会破坏该保护(实验:拨号落在了 tailscaled.sock 上;返回了 ipnserver HTML 横幅)。对于 localapi 本身是受控的(代理将 Host 重写为 "localhost" → 403 无效的 localapi request);剩余影响:通过 serve/funnel 进行的对任意仅限 root 的 unix 套接字(docker.sock 类)的 root 上下文拨号 — 以 serve 作为载体的 T 类特权拨号。对于非属主的 root,/tmp 交换会被 fs.protected_symlinks=1 阻止;非粘滞目录不受保护。修复建议:在每个连接中重新验证解析后的拨号目标(或者在配置时打开一次套接字并保留 fd)。复现:receipts/sf2-*。
发现 9 (SERVE-F-3) — funnel 权限强制仅限 peer-cap + 本地配置(节点属性仅限 CLI)— 严重性:中(威胁模型发现)
受影响的组件/版本: tailscale v1.104.2 (e26db7a5), funnel ingress gates;headscale v0.29.5 (无法生成 ingress 能力)。
守护进程对 funnel 数据路径的控制:(1) 在 peerapi /v0/ingress 处的每连接 peer 能力 https://tailscale.com/cap/ingress(由 netmap 驱动),(2) 本地 AllowFunnel 目标检查。funnel 节点属性和 funnel-ports 能力仅由 CLI 强制执行;原始的 POST /localapi/v0/serve-config 会在没有任何属性的情况下启用 funnel(实验:在 headscale 下显示 "Funnel on",它既不生成属性也不生成 CertDomains;该配置在重启后依然存在)。headscale 完全无法授予 ingress 能力(策略 v2 拒绝 "://" 能力名称;ingress/funnel 未被加入白名单;不支持 funnel)⇒ 在原生 headscale 上无 funnel 流量;一个通过 knob 注入的正向控制(TS_ALLOW_SELF_INGRESS=1,一个上游环境变量开关,明确标记为测试替代)驱动了数据平面:101 → 一个带有 Tailscale-Funnel-Request: ?1 且 X-Forwarded-For = 入站对端攻击者选择的 Tailscale-Ingress-Src 值的被 funnel 请求,原样转发(funnel XFF 的可信度取决于持有能力的转发器)。否定情况会安全关闭(403 no-cap / 错误目标 / 没有 knob 的自连接)。自备域(BYO-domain)证书遵循相同的形态:仅有本地配置门禁(Let's Encrypt 域验证是外部门禁)。修复建议:如果需要阻止在没有控制端生成的属性的情况下启用 funnel,则应在服务器端强制执行该属性。复现:poc-sf3-ingress.py, sf3-funnel-serveconf.json, receipts/sf3-*。
发现 10 (SERVE-F-4) — TS_SERVE_ALLOW_ALL_INTERFACES 在隧道绑定之外暴露 serve — 配置说明
受影响的组件/版本: tailscale v1.104.2 (e26db7a5);非默认 knob(上游已确认)。
该开关仅跳过针对 serve 监听器的 SO_BINDTODEVICE 隧道接口绑定(Web 客户端 + peerapi 保持绑定):任何能够路由到节点 tailscale IP(LAN 等)的网络都可以访问所有 serve 后端——没有 WireGuard、没有 tailnet ACL、没有身份标头(LAN 源的 WhoIs 会失败)。上游已确认(“将 serve 监听器重新暴露给本地网络”);默认关闭;仅由 k8s 操作员的实验性集群流量入口设置。适用于节点上的所有 serve 监听器,包括启用了 funnel 的监听器。经过源验证(测试环境中无实验室 LAN 路径)。
发现 11 (SERVE-F-5) — h2 共享 serve 上下文竞争:跨挂载点能力泄露 → 挂载点级授权绕过 — 严重性:在标头授权部署中为中危;其他情况下为低-中危
受影响的组件/版本: tailscale v1.104.2 (e26db7a5)、ipn/ipnlocal serveWebHandler。前置条件: serve TLS (h2)、≥2 个 AcceptAppCaps 不同的挂载点、持有该能力的对端、单个连接上的并发流。
serveWebHandler 按请求写入 c.AppCapabilities (serve.go:1226),并且 addAppCapabilitiesHeader 在 ReverseProxy 的 Rewrite 内部读取它 (serve.go:1152);TCP 连接的所有流共享一个 serveHTTPContext (BaseContext + OneConnListener),并且 serve TLS 宣称支持 h2。在 -race 下的固定版本树中复现:检测器以精确的生产调用栈确定性地触发;可观察到的泄露为 13/2000(一个不接受任何能力的挂载点转发了接受能力的挂载点的 Tailscale-App-Capabilities 标头)。针对文档化的标头授权中间件模式的访问决策翻转(同一个应用,/ops 接受 corp/cap/role,/admin 不接受任何内容,对端持有 role engineer):顺序基准测试 /admin 403 x100;在单个连接上的并发竞争请求下,8/4000 的 /admin 请求返回了 200 INTERNAL-APP-DATA——由泄露的标头授权的完全特权响应;静默(应用程序无法将其与合法流区分开)且可重试(0.2%/请求转化为耐心的对端可靠的攻击基元)。同对端绑定(值始终与请求对端自身的授权相交);funnel 流完全跳过标头。上游测试注入了每个测试的上下文,因此无法捕获它。修复方案: 为 AcceptAppCaps 使用每个请求的上下文,而不是共享的连接结构。复现: serve_sf5_race_test.go (bleed)、serve_sf5_bypass_test.go (flip)、receipts/sf5-。
Serve/funnel 干净的阴性结果(poc-servefunnel/ 中的证据 + FINDINGS.md)
localapi serve-config 授权(unix/path 的 PermitWrite + local-admin)、代理标头清理(服务端重置身份/XFF)、畸形路径处理、Web 客户端 CSRF(非 Tailscale 控制平面自动认证管理会话——记录在案的 headscale 待办事项——通过管理带标签节点的 WebUI 能力)、端口声明行为、空闲连接限制。
阴性结果(已测量)
- conn25 tailnet 目标网桥被反驳(实时测试): 带有 tailnet 目标的注册被接受,并且连接器会进行 DNAT,同时保留客户端的源(仅目标 NAT,经过源验证)——但接收 tailnet 节点的 WireGuard 加密密钥路由丢弃了内部数据包(对端 C 的密钥对源 A 的地址无效)。不发生 SSH 评估。conn25 枢轴仅适用于非 tailnet 目标(IMDS/LAN)。
- Go 中无 OOM→RCE 底座;netfilter 路径的混淆代理面很干净(对端面仅限 dial/bind/unlink/DNAT);默认路径下已阻止 crash-loop×socket-planting;约 1.17 亿次模糊测试执行,零崩溃;SSH 检查审批不受影响。
类上下文(比较审计)
- TOCTOU/以特权身份拨号类漏洞已被 CVE 确认:Dropbear 的 CVE-2025-14282(在 2025.89 中修复)即为拨号端实例 — 在竞态获胜的 unix 拨号上呈现 root SO_PEERCRED。
- 推荐的修复方案具有存在性证明:修复后的 Dropbear 在 USERAUTH_SUCCESS 内部、任何通道/全局请求之前将 setuid() 切换为会话用户 — 即在每个路径敏感的转发操作之前放弃特权(从结构上去除了我们发现所需的特权参与者)。除此之外,Dropbear 发布的路径策略比 gliderssh 更弱(零验证、无条件 unlink)— 其安全性归功于特权放弃,而非路径检查。
- OpenSSH:默认情况下该类漏洞为阴性结果 — 认证后子进程在服务器循环(sshd-session.c)之前降权为会话用户;monitor 从不 bind;没有检查阶段,因此不存在检查/使用间隙。上游提交 b37c095a(2026-10)强制禁用 streamlocal/GatewayPorts/privileged-port 反向转发,只要认证后的 sshd 以 root 运行 — 这是上游明确声明转发功能假定用户特权(可直接用于反驳 gliderssh,后者以 root 运行且无此类假设)。cee6aedb(10.6p1,通过 dirfd+O_NOFOLLOW 的 agent 套接字)是 dirfd 修复系列的先例。仅策略残留:PermitListen 忽略 unix 路径(自 2018 年起的大纲待办事项);PermitOpen 对 unix 拨号采取故障安全关闭(fails closed)。
- 已完成的漏洞类矩阵:gliderssh/Tailscale — 已证实该漏洞类,root 参与者;OpenSSH — 干净,用户 uid 参与者 + 上游强制禁用先例;Dropbear — 在 2025.89 中修复(CVE-2025-14282 = 漏洞类先例),早期 setuid 后的用户 uid。
检查模式(基于时间的访问授权):范围与限制
Tailscale SSH 提供 action: check 规则 — 在建立会话之前,连接的用户必须重新进行身份验证(通过身份提供商在浏览器中),从而授予一个时间窗口内的访问权限(checkPeriod;默认 12 小时,可配置为 1 分钟–168 小时或 always)。部署通常依赖它来保护高风险访问。经测量和记述的限制:
- 它不会缓解转发发现的问题(1–3)。检查模式仅控制会话的建立。端口转发(
ssh -R/-L)发生在已建立的、经人工批准的会话内部 — 在检查通过后,类 1 中的每个发现都适用于该会话的整个生命周期。启用检查模式并认为它能限制转发的操作员是错误的;只有每个操作的转发标志(SaaS)或供应商修复才能做到。 - 一次审批可在整个 SaaS 控制平面上分摊。根据供应商文档,成功的检查可在时间窗口内授权用户访问 tailnet 中的所有检查模式目标 — 一次认证,所有受控目标。经测量,自托管控制服务器(headscale)将时间窗口限定在目标对(destination pair)范围内,这在实质上更强;能够进行选择的部署应注意这一差异。
- 无需欺骗用户即可获得授权。 三种机制,强度依次递增:(a) 诱导 —— 攻击者的连接可以请求检查,导致受害者收到一个他们并未发起的、看起来合法的身份验证提示(可被钓鱼,但并非百分之百);(b) 顺风车 —— 在 tailnet 中占有一席之地的攻击者无需诱导任何东西:他们监控受害者下一次合法的授权,一旦发生,由此产生的窗口(在 SaaS 控制平面上是整个集群范围,见第 2 点)也会覆盖攻击者受检查门禁保护的目标 —— 用户自己常规的访问就武装了攻击者;(c) 强制事件 —— 本报告中的拒绝服务发现为攻击者提供了制造特定条件(节点连接失败、控制平面降级)的手段,这些条件会促使管理员在修复过程中重新进行身份验证 —— 修复过程本身完成了检查并打开了攻击者随后利用的窗口。运维规则:将未请求的 Tailscale 身份验证提示视为安全事件;将活跃事件期间打开的检查窗口视为与攻击者相关的事件,因为事件可能是为了挑起这些窗口而精心策划的。
- 保持窗口检测基于阈值且可调整节奏。 受检查门禁保护的连接在身份验证前最多可保持 30 分钟。实验室测量表明,攻击者保持在检测器阈值以下(针对 20 秒规则的 11 秒)即可在未被检测到的情况下搭便车;这里的检测需要随机化或人工审核的阈值,而不是固定的阈值。
配置指导: 对于特权规则,倾向于 checkPeriod 分钟或 always;批准时要求身份提供商进行双重验证(重新认证,而不是会话重用);在自托管控制平面上,依赖配对作用域窗口;切勿将检查模式视为对转发的控制 —— 那是转发标志的工作。
建议的修复方案
针对运营商的临时缓解措施(未修补的部署)
管理员现在就可以通过配置和流程采取这些操作 —— 无需厂商修复。按单位努力的风险降低程度排序。
即时、仅限配置:
- 盘点并限制 Tailscale SSH 规则。 列出策略中广播 SSH 的每个节点以及每个
ssh规则。将规则重写为逐个配对的授权(特定的源到特定的目的地);删除身份范围和标签范围的接受规则;绝不允许特权目标账户(root,或具有无密码 sudo 的账户)。在 headscale 上,假设每个 SSH 授权都包含所有转发 —— 将任何 SSH 授权视为等同于 root 并相应地限制规则集。在 SaaS 上,除非有记录在案的需求,否则将每个操作的转发标志设置为拒绝。 - 审计并限制服务配置。 在运行
tailscale serve的每个节点上,在配置中搜索针对 unix 套接字(serve --tcp <port> unix:<path>)的 TCP 转发,并删除任何针对代理自身套接字或其他特权路径的转发(发现 7)。 - 约束应用连接器 — 在防火墙处强制执行目标限制。在修复之前:从不需要该功能的客户端移除连接器功能;在 Kubernetes 中,对连接器命名空间应用默认拒绝出口的 NetworkPolicy。在连接器主机上,使用 netfilter 强制执行内核层目标白名单(本报告附带了一个可部署的脚本:
mitigation-conn25/)。强制执行点已得到验证:由连接器转换的流量在进行网络地址转换之前,会带着其 tailnet 源地址穿过 FORWARD/INPUT,因此那里的防火墙目标策略控制着本报告演示的每一个枢纽攻击 —— 云元数据(169.254.0.0/16,无条件丢弃)、内部 RFC1918 范围(默认拒绝,并为连接器的目标后端配置操作员白名单)、连接器本身的服务以及任意互联网出口。包含 IPv4 和 IPv6 规则。已知限制:该策略是针对每个连接器的(它无法区分是哪个授权对端在进行枢纽攻击),且 SaaS 目标连接器需要宽泛的出口,这会削弱默认拒绝的效力 —— 该控制措施对自托管/VPC 应用连接器的效果最强。 - 启用 SSH 节点上的进程作用域出口规则(发现 1–3 的网络可达性)。nftables 可以通过 cgroup 匹配流量,而 systemd 会将 tailscaled 放在其自己的 cgroup 中;形如“拒绝从 tailscaled 服务 cgroup 发往环回、链路本地(包含元数据)和 RFC1918 的出站流量,除非在白名单中”的规则,能够约束转发连接(由 root 代理执行的
-L拨号)可以到达的位置 —— 这正是 SSH 转发路径所缺少的内核级目标验证。明确允许代理的合法目标(控制平面、中继、tailnet 对端)。 - 防火墙在此处无法做什么。Unix 套接字操作(发现 1–3 中的竞态条件和套接字替换)绝不会遍历 netfilter —— 没有任何 iptables/nftables/ebtables 规则能看到它们;对于这些操作,下文的检测项(套接字生命周期审计、重命名风暴检测)是临时的控制手段。ebtables/网桥规则仅适用于桥接连接器主机上的局域网侧枢纽攻击。
- 加固自托管控制平面。headscale:设置能够承受记录在案的增长速率的 systemd 内存限制(
MemoryMax),启用重启监控,在注册端点前放置限流反向代理,并限制网络暴露(控制平面仅可从管理网络或 VPN 访问,不能暴露在公网)。 - 为身份赋予时钟。在服务器集群上启用节点密钥过期(取消标记设备的永不过期默认设置),并在硬件允许的情况下启用 TPM/安全隔离区绑定的代理状态,以便被盗的身份材料不会无限期存续。
未修补时的检测方法(部署以下传感器并逐一进行自测):
- Socket-lifecycle auditing. auditd rules on unix-socket
unlink/bind/chmodin the forwarding-allowed directories (/tmp, homes,/run/user) and on the agent's own socket paths — object-scoped (watch the path and operation), not syscall-name-scoped. - Cross-identity endpoint analytics. Baseline each device identity's network endpoint; alert when one endpoint begins serving a second node identity (site-independent — catches same-machine impersonation that location-change analytics cannot), and when a machine key appears from a new network/ASN (catches stolen state used elsewhere; note this detects cross-site use only — same-site reuse of stolen identity remains blind to it).
- Rename-storm detection. The race exploits sustained directory rename loops (~10⁵/s). inotify or audit watch with rate alerting on candidate directories; a socket with mode 0755 at a path that should be 0600 is a forensic signature of a race-won bind.
- Enrollment and key monitoring. Alert on authkey issuance and use outside change windows; alert on any node joining with a previously unseen key under a tagged identity; treat an unrequested Tailscale authentication prompt reported by a user as an incident.
- Control-plane health. headscale: monitor resident memory (it retains peak RSS after attack), registration-cache churn, and registration latency; alert on deviation.
Containment on suspicion or incident:
- Delete the device at the control plane — reimaging alone is not containment. A node's identity is key material stored in its state file; an attacker with root on the host can have copied that file off-box (demonstrated in this report: the copied state file re-creates the node's identity on other hardware, accepted by the control plane). Reimaging wipes only the compromised host's copy — it does not revoke stolen keys, which remain valid until the device is deleted or its keys expire. Assume exfiltration after any root compromise; delete the device (which invalidates both its machine and node keys), then rotate any authkeys and credentials reachable from the affected host; if a Tailnet Lock signing node was compromised, rotate the lock; review policy history via your GitOps trail for unauthorized diffs (routes, DERP map, approvers, key scopes).
Vendor-side root-cause fixes (for the maintainers; not operator-actionable)
Findings 1–3 share one root cause. Fix family: openat/dirfd-relative bind+dial; validate-at-use-time; BindUnlink restricted to session-owned sockets; privilege-drop forwarding à la OpenSSH (precedent: Dropbear 2025.89 / CVE-2025-14282 fix — setuid before any channel/global request); deny-by-default per-action forwarding flags; signer-gating of the TKA disablement endpoint (submission currently requires no signer). Serve family (findings 7–11): isTailscaleSocket parity for TCPForward unix backends with per-dial re-validation (7, 8); per-request context for AcceptAppCaps instead of the shared connection struct (11); attribute enforcement server-side if funnel arming without a control-minted attribute is to be prevented (9).
— Erica Windisch, Cyberdione Labs Corporation
Tailworm · 2026-10
来源:Hacker News-首页 · tailworm.com