Neutron, our AI engine, scored 97.1% on UC Berkeley's CyberGym benchmark. Learn more

Deep Dive

Deep Dive

Tailworm: Tailscale SSH Privilege Escalation to Root

Tailscale SSH's root daemon dials and replaces unix sockets for approved users, reaching root on permitted servers; upgrade to 1.104.4 or later; disable forwarding until updated.

TL;DR

  • Tailscale SSH's embedded server runs inside tailscaled, a daemon running as root, and performs unix-socket forwarding operations — dial, bind, and unlink — with root's credentials. An approved SSH session can use that to reach root-only sockets: the researchers demonstrated root command execution through docker.sock, a root shell through systemd's control socket, and capture of the keys that enroll new devices.
  • The audit tested released software: Tailscale client 1.104.2, headscale 0.29.5, and Tailscale's embedded gliderssh library. Tailscale shipped fixes in 1.104.4 on October 27 and published its findings and four security bulletins on October 28.
  • The researchers rate six of the eleven findings High, including the socket-forwarding class. Tailscale's bulletins TS-2026-014 through TS-2026-017 cover the confirmed SSH and Serve issues; they do not list CVE identifiers or CVSS scores as of October 28. Exploitation was demonstrated in a lab only: measured, policy-bounded automated spread in under a second per hop, with no passwords or stolen keys. No attacks in the wild are known.
  • Set TS_SSH_DISABLE_FORWARDING=1 in the tailscaled service environment and restart the service now, rewrite SSH rules as per-pair grants, and upgrade clients to 1.104.4 or later. The headscale server findings remain separate: updating Tailscale clients does not fix headscale's control-server issues.

The report numbers its findings as follows; severities are the researchers' assessments:

ID Severity / classification Component Impact
1 High SSH local forwarding Dials privileged sockets as root.
2 High SSH remote forwarding Replaces root-owned sockets.
3 High SSH path validation Races bypass the allowlist.
4 High Experimental conn25 Entitled users choose unauthorized egress destinations.
5 High gliderssh sessions Exhausts daemon memory.
6 Medium–High headscale / DERP client Registration denial of service; insider or hostile-relay memory exhaustion.
7 High Serve TCP forwarding Exposes the privileged local API.
8 Medium (contained) Serve Unix backends Symlink swaps redirect privileged connections.
9 Medium (threat-model finding) Funnel Local administrators bypass a CLI attribute check.
10 Configuration note Serve interface binding An opt-in setting exposes listeners beyond the tunnel.
11 Medium with header authorization; Low–Medium otherwise Serve HTTP/2 Capability headers cross mount boundaries.

Tailscale's assessment distinguishes bugs from intended behavior: findings 9 and 10 work as designed, and finding 4 concerns pre-production conn25, not stable app connectors.

Am I affected?

Tailscale SSH socket-forwarding escalation. On an unpatched client, you are affected if all of these hold:

  • You run the Tailscale client with Tailscale SSH enabled on a target node.
  • Your SSH policy permits the connecting user or device to reach the target with forwarding enabled, either through an accept rule or an approved check session. headscale 0.29.5 grants forwarding on accepted sessions without a per-rule disable switch.
  • The connecting device is compromised, or the connecting user is an attacker: the findings operate from fully unprivileged accounts and need no passwords, keys or sudo on the target.

headscale control-server denial of service (finding 6). An internet-reachable headscale 0.29.5 registration endpoint can be attacked without an account or SSH access. The report separately requires an authenticated insider for embedded-DERP memory exhaustion; a hostile relay is the prerequisite for the Tailscale-client response-body attack.

Component Tested version Fix status
tailscaled, including its embedded Tailscale SSH server, serve/funnel and the DERP relay client 1.104.2 1.104.4 fixes findings 1–3, 5, 7, 8, 11 and the client portion of finding 6; conn25 and headscale require separate assessment
headscale control server 0.29.5 server-side fixes still pending in the report's October 28 status; registration-cache rework tracked in PR #3586
gliderssh library the v0.3.4 snapshot vendored by Tailscale 1.104.2 session-exhaustion fix shipped in Tailscale 1.104.4 (PRs #21945 and #21953)

Check what you run and what your policy allows:

tailscale version                     # client version on every node
tailscale status                      # the peers this device can see
headscale version                     # self-hosted control server, if you run one
sudo systemctl show tailscaled --property=Environment   # is TS_SSH_DISABLE_FORWARDING set?

The audit reports that nodes could enumerate tailnet peers in its test environment. Do not rely on the policy being secret: review the SSH rules in your policy file, on the Tailscale admin console for SaaS deployments or in your headscale policy file, and count how many are identity-wide or tag-wide accept rules rather than one source device to one destination device.

How do I fix it?

Upgrade Tailscale clients to 1.104.4 or later. While scheduling that update, reduce forwarding exposure and narrow SSH access; headscale control-server hardening remains a separate task.

  1. Disable SSH forwarding unless you have a documented need for it. In the tailscaled service environment, set:
sudo mkdir -p /etc/systemd/system/tailscaled.service.d &&
  printf '[Service]\nEnvironment=TS_SSH_DISABLE_FORWARDING=1\n' |
  sudo tee /etc/systemd/system/tailscaled.service.d/disable-forwarding.conf &&
  sudo systemctl daemon-reload &&
  sudo systemctl restart tailscaled

Tailscale's October 26 update says this "mitigates several of the more serious findings" without disabling Tailscale SSH entirely. On the SaaS control plane you can also set the per-rule forwarding fields to denied; headscale 0.29.5 has no equivalent per-rule switch. The service environment setting is applied on each Tailscale client.

  1. Rewrite SSH rules as per-pair grants: a specific source to a specific destination, never identity-wide or tag-wide accept rules, and never privileged target accounts; restricting SSH to non-privileged accounts is defense in depth only, since the findings operate from fully unprivileged accounts.
  2. Install Tailscale 1.104.4 or later on each client, then verify with tailscale version. Tailscale's October 28 findings and fixes links bulletins TS-2026-014 through TS-2026-017.
  3. For self-hosted headscale servers, apply the report's hardening until a fixed release exists: systemd MemoryMax, restart supervision, rate-limiting in front of the registration endpoints, and no exposure to the open internet.
  4. If you run tailscale serve, remove any TCP forward targeting a unix socket (serve --tcp <port> unix:<path>).
  5. If you are testing experimental conn25, remove the capability from clients that do not need it, apply default-deny egress NetworkPolicies to connector namespaces in Kubernetes, and enforce a kernel-layer destination allowlist on connector hosts, for which the report ships a script. Tailscale says stable app connectors are not affected.

What is the root cause?

Tailscale SSH's forwarding operations run with the root daemon's credentials, so every path check that tries to constrain them is a race the unprivileged session can win.

Two design decisions combine into the problem. First, Tailscale SSH performs no user-level authentication: authentication completes with method none, because the WireGuard layer has already identified the device, and that device identity, not a user credential, is what the policy evaluates, so a session carries the device's full authorization for its lifetime.

Second, the embedded SSH server lives inside tailscaled, which runs as root, and its gliderssh library performs the unix-socket forwarding operations, dial for ssh -L, bind and unlink for ssh -R, as root.

The library does try to constrain where forwarding may land. In tailscale/gliderssh, streamlocal.go, the functions validateAndResolveSocketPath and validateAndResolveListenPath check the path against a directory allowlist before the operation runs, resolving symlinks because the kernel, not the validator, performs the eventual connection:

// validateAndResolveSocketPath validates socketPath lexically, then resolves
// its symlinks and re-checks that the real destination is still within
// opts.AllowedDirectories and not excluded by opts.DeniedPrefixes. It returns
// the resolved path to dial.
//
// The lexical check alone is insufficient because the kernel connects to the
// symlink target: a symlink inside an allowed directory could otherwise
// redirect the connection to a socket outside it (e.g. a link in the user's
// home dir pointing at /var/run/docker.sock).

Tailscale 1.104.2 vendors gliderssh at version v0.3.4-0.20260716005906-1a0f895faf28, per its go.mod, and the allowlist covers the user's home directory, /tmp and /run/user/<uid>. But the validation happens at check time, while the unlink, bind and dial occur later, when the kernel re-resolves the path.

Finding 3 shows the gap: a rename(2) race loop inside the attacker's own SSH session, measured at roughly 170,000 path-state flips per second, presents a benign path to the check and flips the directory to the target before the root daemon operates on it. The report's one-line conclusion: the {home, /tmp, /run/user} allowlist "is not a security boundary".

The class has a CVE-recognized precedent and a known cure. Dropbear's CVE-2025-14282 was the dial-side instance, fixed in 2025.89 by setuid() to the session user before any forwarding operation, so no path check is needed. OpenSSH is the clean counterexample the report audited: its post-auth child drops to the session user before the server loop, so there is no check phase and no check-to-use gap.

How is it exploited?

The simplest demonstration, finding 1, needs one SSH policy rule that permits local forwarding; it shows the root dial without any race. The commands are from the report's public reproduction kit:

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'

The dial is performed by tailscaled, so the peer credentials the socket sees are root's. The verified result was uid=0(root) plus /etc/shadow contents, without sudo or Docker group membership. This demonstration uses the target's stock dockerd through tailscaled; it requires no planted daemon or separate dockerd vulnerability. The strongest chain, finding 3, races the check-to-use gap:

  1. The attacker holds a foothold on any policy-permitted device, from an exploited web application, a malicious dependency, a phishing implant, or stolen identity credentials, and connects over Tailscale SSH to a target under a rule that permits forwarding.
  2. Inside the session, the attacker runs a rename(2) race loop on a directory inside the allowlist, flipping path state roughly 170,000 times per second, so the lexical check sees a benign path and the kernel re-resolves to the target at operation time.
  3. The root daemon unlinks the live root-owned socket at the real path, for example /run/docker.sock, the tailscaled localapi socket or the headscale control socket, and binds the attacker's forwarding listener there.
  4. The attacker holds the rebind open for a verified 4 minutes or longer, and acts as a man-in-the-middle on the service. Against the localapi socket, the captured traffic included a byte-exact POST /localapi/v0/start containing an authkey, the credential that enrolls new devices.
  5. The attacker uses the captured authkey on a scratch node and joins it to the tailnet under the victim's identity, a rogue node that looks like a legitimate tagged server with routes auto-approved, demonstrated end to end in the lab.

A side-finding extends the primitives: the unlink gate and the post-listen chmod follow the lexical path at use time, so the racing session can make root unlink arbitrary regular files and chmod them to 0600, destruction short of content modification.

How does the attack flow?

One compromised device with an approved SSH rule becomes root on target servers, then a persistent node on the network, with no password or key theft anywhere in the chain.

flowchart LR A["Foothold on one
policy-permitted device"] --> B["Tailscale SSH session
auth method none"] B --> C["ssh -R/-L unix-socket
forwarding request"] C --> D["root tailscaled validates
path lexically at check time"] D --> E["rename race flips the path
between check and use"] E --> F["root-owned socket replaced
or dialed as root"] F --> G["Root execution and
enrollment-key capture"] G --> H["Rogue node joins
the tailnet"] style D fill:#f8d7da style E fill:#f8d7da

What can an attacker do?

The socket takeover reaches whichever privileged services live on unix sockets. Demonstrated in the report, with evidence on file:

  • Root command execution through a raced stock dockerd: a single raced keep-alive connection completed container creation with / bind-mounted into the container and command execution with uid 0.
  • A root shell through systemd's control socket: root-credential dials of /run/systemd/private were authorized by systemd, and replayed D-Bus bytes started a transient unit that produced a root reverse shell, with intermittent race delivery. Raced dials to a kubelet socket stub were likewise all accepted presenting uid 0.
  • Device-enrollment credential theft: the socket-takeover-to-enrollment chain converts a rebind into a rogue node with auto-approved routes, which is persistence that survives reimaging of the original host, plus arbitrary root-file deletion and forced chmod 0600 as availability destruction.
  • Fleet-wide spread: with a policy-granted SSH path, the report measured automated propagation to every device the policy permits, no passwords, no stolen keys, no user interaction, under a second per hop. Two structural limits held: per-pair access grants stopped the spread, and hardware-bound device credentials prevented identity theft from persisting.
  • Memory exhaustion of the root daemon, finding 5: window-change churn and unbounded environment appends allocated roughly 536 MB in 7 seconds, persisting past disconnect, a flaw inherited from upstream gliderlabs/ssh v0.3.8.
  • App-connector egress validation, finding 4: experimental conn25 lets an ordinary per-app user entitlement choose arbitrary egress destinations through the connector, including cloud metadata and Kubernetes services, because the client-declared destination is never validated against the app's configured backends. Tailscale says this does not affect stable app connectors.
  • headscale denial of service, finding 6: unauthenticated registration-cache eviction can block onboarding organization-wide; embedded-DERP memory exhaustion requires an authenticated insider. The Tailscale-client sub-finding requires a hostile relay.

For socket forwarding, the conditions are an approved or compromised device with policy-permitted SSH access and forwarding enabled on an unpatched client. headscale 0.29.5 grants forwarding without a per-rule disable switch. Check mode, Tailscale's browser re-authentication for SSH, does not gate forwarding: it approves session establishment, and the socket-forwarding findings then operate for the life of the approved session.

Why does the fix work?

The SSH forwarding fixes for findings 1–3 run socket operations as the session user, applying the privilege separation recommended by the report and previously used in Dropbear's CVE-2025-14282 fix. Serve and resource-exhaustion findings have separate fixes. From the report's upstream-response section and Tailscale's security bulletins:

  • Findings 1 to 3: SSH forwarding was re-architected onto a per-connection monitor with unprivileged, credentialed children. Dial, bind and unlink for forwarded sockets now run with the authenticated session user's credentials, and payload peer credentials identify that user rather than the root daemon. The lexical path re-validation that the race defeated was removed in favor of the kernel's discretionary access controls as the boundary.
  • Finding 5: window-change sends in gliderssh are non-blocking with keep-latest semantics and environment churn is bounded (PRs #21945 and #21953; the upstream commit message credits the audit's measurements).
  • Finding 7: serve TCP forwarding to the localapi socket is refused at configuration time and per dial (PR #21966).
  • Finding 8: Serve HTTP-proxy Unix backend targets are revalidated during every dial, replacing the configuration-reload-only symlink guard. Tailscale identifies this fix in TS-2026-016, shipped in 1.104.4.
  • Finding 11: capability handling moves to a per-request context (PR #21931).
  • Finding 6 (client): the relay bounds its response-body read (PR #21980).

For SSH forwarding, once dial, bind and unlink run as the session user, the kernel's own permission checks decide what the session may touch, and there is no check-to-use gap because no elevated check is needed. Tailscale 1.104.4 shipped these client fixes on October 27; the vendor confirmed availability on October 28. The report tested headscale 0.29.5 and still lists its server-side fixes as pending. Updating the Tailscale client does not remediate those headscale findings.

How do I check if I was compromised?

No real-world attacks are known. The report's detection guidance for investigating unpatched systems:

  • Socket-lifecycle auditing: auditd rules on unix-socket unlink, bind and chmod operations in the forwarding-allowed directories, /tmp, homes and /run/user, and on the agent's own socket paths, object-scoped by path and operation rather than syscall name. A socket with mode 0755 at a path that should be 0600 is a forensic signature of a race-won bind whose post-listen chmod was redirected.
  • Rename-storm detection: the race needs sustained directory rename loops on the order of 100,000 per second, so an inotify or audit watch with rate alerting on candidate directories catches it.
  • Enrollment and key monitoring: alert on authkey issuance or use outside change windows, on any node joining with a previously unseen key under a tagged identity, and treat a user report of an unrequested Tailscale authentication prompt as an incident, since an attacker's connection can solicit the check prompt. For headscale, monitor resident memory, registration-cache churn and registration latency.

If you find evidence of compromise, reimaging is not containment: a node's identity is key material stored in a state file, and an attacker with root can have copied it off-box, where it remains valid until the device is deleted at the control plane or its keys expire. Delete the device, rotate any authkeys and credentials reachable from the affected host, and rotate the Tailnet Lock if a signing node was compromised.

Timeline

  • 2026-10-22 to 2026-10-24: Cyberdione Labs tests released Tailscale 1.104.2, headscale 0.29.5 and the embedded gliderssh library; the connector findings concern experimental, pre-production conn25.
  • 2026-10-25: the Tailworm report is published with eleven findings and reproduction kits; The Hacker News covers it the same day.
  • 2026-10-26: Tailscale publishes "Tailscale SSH: Investigation and remediation update", confirming serious privilege escalation beyond the configured local user and targeting a patched client by the end of October 27, subject to testing. Fixes for findings 1 to 3 and 5 land in the tailscale and gliderssh main branches.
  • 2026-10-27: the fixes for findings 7, 11 and the client relay component merge (PRs #21966, #21931, #21980); a headscale registration-cache rework, PR #3586, is still pending. Tailscale tags the patched client as 1.104.4.
  • 2026-10-27: this post.
  • 2026-10-28: Tailscale publishes its findings and fixes with bulletins TS-2026-014 through TS-2026-017; this post is updated to reflect 1.104.4 availability and remaining scope.

FAQ

Is Tailscale telling everyone to turn Tailscale SSH off?

No. Tailscale's October 26 update says it is "not recommending that everyone disable Tailscale SSH", because exploitation depends on the permissions granted to the connecting user or device and the system's configuration. For unpatched clients, Tailscale's interim steps are to disable forwarding, review SSH access policies, and tighten access to sensitive systems. Its October 28 guidance is to upgrade to 1.104.4 or later.

Does check mode protect me?

No. Check mode gates session establishment only; port forwarding happens inside an established, approved session, and every socket-forwarding finding then applies for the life of that session. The report also measured that on the SaaS control plane one successful check amortizes across all check-mode destinations until the approval window expires, so a user's own routine authorization can arm destinations the attacker cares about.

Does Tailnet Lock stop the rogue-node enrollment?

It raises the bar but was not a full stop in the lab. With Tailnet Lock disabled, the default, the full chain from captured authkey to an online rogue node ran end to end; with the lock enabled, the disablement secret and signer-key material were captured in transit, though no real disablement was executed. Treat a compromised signer node as a lock rotation, not a cleanup. The report documents reproduction kits for each finding, and no working exploit against a real deployment is known to exist.

Can this be exploited from the internet?

Not the SSH findings directly: the attacker needs a foothold on a device the policy already permits, or an approved account. In the report's lab, the mesh converted one foothold into privileged access across policy-permitted destinations at machine speed. The headscale findings are different: an unauthenticated internet user can disrupt new-device onboarding through an internet-reachable registration endpoint, which is why you should not expose one.

Which client release fixes these issues?

Upgrade to Tailscale 1.104.4 or later and confirm with tailscale version on every node. Tailscale's October 28 findings-and-fixes post and bulletins TS-2026-014 through TS-2026-017 confirm the release. Until each client is updated, disable SSH forwarding and use per-pair SSH grants. headscale control-server fixes remain separate.

Scan your external attack surface with Ostorlab to surface internet-exposed headscale control servers and other self-hosted infrastructure that should not be reachable from the open internet.

References

  • Cyberdione Labs, "Tailworm: Security Assessment Report, Tailscale v1.104.2 and headscale v0.29.5", published October 25, 2026, with the eleven findings, verified reproductions, wormability measurements and detection guidance, and the mitigation matrix updated October 28: https://tailworm.com/ (checked October 28, 2026)
  • Tailscale, "Tailscale SSH: Investigation and remediation update", October 26, 2026, confirming the findings, the TS_SSH_DISABLE_FORWARDING=1 mitigation and the end-of-October-27 patched client target: https://tailscale.com/ssh-investigation-update (checked October 27, 2026)
  • tailscale/tailscale fix pull requests: #21941, #21942, #21943, #21944, #21945, #21953, #21959, #21963, #21966, #21931, #21980 (checked October 28, 2026)
  • tailscale/gliderssh streamlocal.go, functions validateAndResolveSocketPath and validateAndResolveListenPath, the lexical-then-resolve validation the race defeats: https://github.com/tailscale/gliderssh/blob/master/streamlocal.go (checked October 27, 2026)
  • Tailscale 1.104.2 go.mod, pinning gliderssh at v0.3.4-0.20260716005906-1a0f895faf28: https://github.com/tailscale/tailscale/blob/v1.104.2/go.mod (checked October 27, 2026)
  • Dropbear CVE-2025-14282, the class precedent fixed in 2025.89 by setting the session uid before any channel or global request: https://nvd.nist.gov/vuln/detail/CVE-2025-14282 (referenced via the Tailworm report, checked October 27, 2026)
  • headscale registration-cache rework, PR #3586, adopting admission control over eviction and citing the report: https://github.com/juanfont/headscale/pull/3586 (referenced via the Tailworm upstream response, checked October 27, 2026)
  • The Hacker News coverage of the Tailworm findings, October 25, 2026: https://thehackernews.com/2026/10/unpatched-tailscale-ssh-flaws-could-let.html (checked October 27, 2026)
  • Tailscale 1.104.4 release: https://github.com/tailscale/tailscale/releases/tag/v1.104.4; vendor findings and fixes, October 28, 2026: https://tailscale.com/blog/tailscale-ssh-findings-fixes; security bulletins TS-2026-014 through TS-2026-017: https://tailscale.com/security-bulletins (checked October 28, 2026)