The Mesh Converts One Foothold Into Fleet-Wide Root: Tailworm's 11 Unpatched Tailscale and Headscale Findings

On Sunday 25 October 2026, security researcher Erica Windisch of Cyberdione Labs published Tailworm, a security assessment of the Tailscale client agent v1.104.2 and the self-hosted control server headscale v0.29.5, tested in a lab between 22 and 24 October on unmodified released software. Eleven numbered findings in five classes, each with a working reproduction kit. The headline class: Tailscale SSH unix-socket forwarding executes as root, so one approved device plus one SSH policy rule reaches root on every server the policy permits — no passwords, no stolen keys, no user interaction, measured at under a second per hop. As of publication there is no CVE identifier, no vendor security bulletin and no fixed version. Tailscale's vice president of engineering, Mike Shaver, responded publicly on Reddit that the company had received the report, had already been examining the same areas with outside specialists, had found issues it is working to fix, and is still scoping which configurations are affected — while, in his words, not recommending that everyone disable Tailscale SSH.

We read the full Tailworm report first-hand. What follows is what it demonstrates, what it explicitly does not claim, and the one configuration decision that matters most before any vendor fix ships.

The privilege model that makes the findings work

Tailworm's classes 1–3 all rest on one documented design property, and the report states it plainly: Tailscale SSH performs no user-level authentication. The SSH handshake completes with authentication method none; the WireGuard layer has already identified the device, and that device identity — not a password, key pair or user credential — is what the single SSH policy rule evaluates. Every session therefore carries its forwarding capabilities for its whole lifetime, and compromise of any one entitled device speaks with that device's full authorization regardless of which user is behind it. This is also what makes the reported wormability mechanical rather than clever: propagation needs no credential material at all.

Two more properties complete the targeting picture, and both are measured, not asserted. Every node receives the full tailnet peer list regardless of what the access policy lets it reach, so an attacker on one device can enumerate every potential target from local state. And connection probing is three-valued and silent — policy-dropped (timeout), policy-rejected (fast refusal), permitted (the service answers) — so sweeping all peers maps the actually-permitted envelope in minutes with nothing for any sensor to alert on. The mesh hands the attacker the target list, a quiet sweep hands them the policy map, and findings 1–4 convert that map into access.

Findings 1–3: forwarding as root, and the race that defeats the allowlist

Finding 1 (T3): ssh -L unix-socket forwarding dials as root. The root-privileged tailscaled process connects, on the peer's behalf, to sockets restricted to root. The verified reproduction: forwarding /tmp/relay.sock to the Docker socket and running a container with the host root mounted, yielding uid=0(root) plus /etc/shadow contents in zero seconds of elapsed exploitation, with no sudo, no group membership and no victim-side daemon access.

Finding 2 (T2): ssh -R remote forwarding replaces root-owned sockets. The root daemon unlinks a sticky-directory-protected, root-owned socket and binds the attacker's forwarding listener at that path, impersonating the service — demonstrated as direct root code execution against a command-channel daemon, cleartext registry-credential capture against Docker, and traffic capture off a live control socket.

Finding 3 (T2-RACE / T3-RACE): a TOCTOU race defeats the {home, /tmp, /run/user} path allowlist on both the bind and dial sides. The validation checks the lexical path at check time; the privileged unlink/bind/dial re-resolves it later, and a rename loop running unprivileged inside the attacker's own SSH session (measured at roughly 170,000 path flips per second) wins the gap. End-to-end results include a raced keep-alive connection to the stock systemd-managed Docker daemon completing container creation with the host root mounted, a root shell via /run/systemd/private, and — the full impact chain — a byte-exact authkey captured from POST /localapi/v0/start and used to enroll a rogue node under the victim's identity on default (Tailnet-Lock-disabled) tailnets. On lock-enabled tailnets the disablement secret and signer-key material were captured in transit, but the final enrollment step was not executed. The report is explicit about that boundary, and we preserve it.

The report also ships a correction worth noting for its honesty: an earlier note attributing 0755 socket binds to stock OpenSSH was verified against the live sshd configuration and auth logs and retracted — OpenSSH is a structural analog only, and the effect is not demonstrated there. Conversely, the fix family it recommends has existence proofs in both directions: Dropbear's CVE-2025-14282 fix in 2025.89 (setuid to the session user before any channel request) is the privilege-drop precedent, and an October 2026 upstream OpenSSH commit now force-disables streamlocal forwarding whenever post-auth sshd would run as root — an explicit upstream statement that forwarding features assume user privilege, the assumption the gliderssh-derived stack violates by running them as root.

Classes 2–5: connectors, control plane, serve

Finding 4 (C1) concerns the experimental conn25 app-connector path: a per-app user entitlement silently confers unrestricted egress selection through a shared connector, because the client-supplied destination is validated only for address family and never against the app's configured destinations. Verified reach includes the cloud metadata service, internal LANs and — through the Kubernetes operator's privileged connector pods — every cluster service CIDR. The authorization decision is made about the app; the routing decision is delegated to the client.

Findings 5–6 are availability: an authenticated-session memory-exhaustion loop in the root daemon via the vendored gliderssh session handling (~536 MB in 7 seconds in the test), and unauthenticated control-plane exhaustion on headscale (memory growth plus auth-cache eviction blocking new-device onboarding org-wide), alongside a DERP-client unbounded read. Findings 7–11 cover tailscale serve: a TCP-forward to the agent's own localapi socket accepted at config time (lab-verified remote node takeover from a peer), a config-time-only socket guard defeatable by symlink swap after reload, funnel arming without a control-minted attribute, an experimental knob that unbinds serve listeners from the tunnel interface, and a shared-context race across HTTP/2 streams that bleeds one mount's authorization capability into another's (measured 8 flipped decisions in 4,000 requests — a 0.2%-per-request primitive for a patient peer).

What actually stops the spread

The report measured what contains the worm, not just what enables it, and the ranking is uncomfortable for common deployments. Per-pair (source-to-destination) access grants stopped the spread on their own — the single control that did. Hardware-bound device credentials (TPM/Secure Enclave) prevent the identity theft that makes compromise persist. Everything else — Tailnet Lock off the default path aside — changed visibility speed, not spread. Notably, Tailscale SSH check rules (time-boxed re-authentication) do not mitigate the forwarding findings: check gates session establishment, and forwarding happens inside an already-approved session for its whole lifetime. One approval on the SaaS control plane also amortizes fleet-wide across all check-gated destinations for the window.

There is one asymmetry operators of self-hosted infrastructure need to hear verbatim: on headscale v0.29.5, forwarding cannot be disabled at all — it is granted unconditionally on every accepted session. The report's guidance is to treat every headscale SSH grant as root-equivalent and rewrite the rule set accordingly until a fix ships. Containment after suspected compromise is equally blunt: reimaging the host is not containment, because node identity is key material in a file the attacker with root has already copied. Only control-plane device deletion (or key expiry) revokes it.

What to do

  • Rewrite SSH policy as per-pair grants today. List every node advertising SSH and every rule; replace identity-wide and tag-wide accept rules with specific source-to-destination pairs. This was the one control measured to stop lateral movement on its own. Never allow privileged target accounts (root, passwordless-sudo accounts) in SSH rules.
  • Turn off forwarding where the policy allows it — and know where it does not. On SaaS tailnets, set the per-rule forwarding flags to denied unless a documented need exists. On headscale v0.29.5 there is no such flag: treat every SSH grant as root-equivalent and narrow the rule set instead.
  • Grep every tailscale serve configuration for TCP forwards to unix sockets, especially the agent's own socket, and remove them. Audit funnel arming and the experimental all-interfaces knob on any node that carries them.
  • Constrain connectors at the firewall, not the feature flag. Remove connector entitlements from clients that do not need them; apply default-deny egress NetworkPolicy to connector namespaces in Kubernetes; the report ships a kernel-layer destination allowlist for connector hosts (unconditional drops for metadata and internal ranges).
  • Give identities a clock and a body. Enable node-key expiry on server fleets (opt out of the no-expiry default for tagged devices) and TPM/Secure-Enclave-bound agent state where hardware allows. Harden self-hosted control planes with memory limits, restart supervision and registration-rate throttling — and never expose them to the open internet.
  • Do not count check-mode re-authentication as a forwarding control. Prefer short checkPeriod values for privileged rules, but understand the forwarding primitives operate inside the approved session regardless.

Our write-up is analysis of the Tailworm report at tailworm.com (Erica Windisch, Cyberdione Labs Corporation; testing window 2026-10-22 to 2026-10-24; tailscaled v1.104.2 and headscale v0.29.5), read first-hand in full, including the five finding classes, the reproduction-kit inventory (poc-docker, poc-t2race, poc-t3race, conn25 harnesses, serve/funnel receipts), the negative results and the remediation appendix. Tailscale's position is carried as reported by The Hacker News on 25 October 2026 (VP of Engineering Mike Shaver's public Reddit response: report received, same areas already under examination with outside specialists, scope still being determined, no general recommendation to disable Tailscale SSH) — we did not independently retrieve the Reddit thread, and the absence of a CVE, bulletin or fixed version is as stated in both the report and that coverage as of 25 October. All verification was read-only: no Tailscale or headscale build was installed, no finding was reproduced, no tailnet was joined, and no vendor system was contacted. Related prior coverage: our briefing on the k0t campaign's 70,000 stolen edge credentials is the other half of this threat model — stolen remote-access credentials are the foothold Tailworm's classes assume as their entry condition.

Sources: