Tailscale SSH Flag Injection Broke Non-Root ACLs
A getent argument-handling bug let any allowed SSH user become root with the username -i.
If you used Tailscale SSH with autogroup:nonroot as the fence keeping developers out of root shells, that fence had a hole. In versions before 1.98.9, a user who already had SSH access to a Linux node could request the username -i and land in an interactive root session, ignoring the ACL policy that was supposed to forbid it.
This is not a distant theoretical issue. Tailscale SSH sells itself on moving authorization from scattered authorized_keys files into central policy. When that policy can be bypassed by how a local username string is handed to a Unix utility, the product’s main security story is the thing that failed.
How the username became a flag
On Linux, Tailscale SSH resolves the target local account by looking it up in the passwd database. The path it used ran through getent(1). Tailscale accepted usernames that began with a leading dash and passed them straight through as arguments.
That is the classic argv-injection pattern. Tools like getent treat arguments starting with - as options, not as names. Connecting as the username -i was interpreted as the long option --no-idn. With that flag, getent printed the full passwd file contents, beginning with the root entry. Tailscale then opened an interactive root session from that result.
The fix is blunt and correct: reject usernames with leading dashes. The change ships in Tailscale 1.98.9 and later, as documented in the security bulletins.
Impact is limited to people who already had Tailscale SSH access to the host. This is not unauthenticated remote root over the public internet. It is privilege escalation against ACL policy for an already-authorized peer. That still matters a great deal if your model was “engineers get non-root shells, SRE gets root.”
The same release fixed a sibling bypass
The same 1.98.9 cut also closed a second Tailscale SSH ACL hole from June 2026. Previously, clients could address users by numeric UID. Root restrictions in ACL enforcement only considered the username form. Connecting as 0@host granted a root session even when policy denied root and only allowed non-root local accounts.
Tailscale now disallows UIDs and numeric-only usernames over SSH to remove that ambiguity. Both bugs sit in the same class: identity strings that look like “users” to the ACL layer were not treated as opaque names all the way down to the session-start path. One path fed a flag to getent. The other treated 0 as a legitimate login name while root checks only watched for the string root.
If you only patch for the dash-username issue and ignore the release notes, you still leave the UID path open on older builds. Upgrade once, get both.
Why this hurts more than a normal SSH misconfig
Classic OpenSSH failures are usually local: a bad authorized_keys entry, PermitRootLogin yes, a shared key that never rotated. Tailscale SSH changes the trust model. tailscaled listens on the tailnet interface, authenticates the peer by tailnet identity, evaluates central ssh ACL rules, then starts a session as the requested local Unix user. The local account still owns files and sudo rights. The decision about whether you may become that account moved into policy.
autogroup:nonroot is the convenient knob for “any local user except root.” Teams use it so they do not have to enumerate every deploy or app account. The flag-injection bug turned that convenience into a false sense of isolation. Policy said non-root. The session starter said root. Anyone who treated the ACL as the only root gate was wrong until 1.98.9.
That does not make Tailscale SSH a bad idea. Central identity and policy still beat copying keys across a fleet. It does mean the username surface is part of the trusted computing base. Anything that passes a client-supplied string into a privileged helper (getent, login path, UID parsing) has to treat that string as hostile data, not as a clean identifier.
What to do on your hosts
Patch first. If any node runs Tailscale SSH on Linux, move to 1.98.9 or newer. That is the only complete fix for the argument-handling and UID issues.
tailscale version
# If below 1.98.9, upgrade via your package manager or the official install path,
# then confirm SSH is still enabled:
tailscale set --ssh
tailscale status
Audit who had non-root grants. Anyone covered by rules using users: ["autogroup:nonroot"] (or broad non-root lists) on Linux hosts was in the affected set for the -i path. Treat those grants as having been stronger on paper than in practice until the upgrade landed.
Keep defense in depth on the Unix side. ACL policy is not a substitute for:
PermitRootLogin noand no password auth if you still run OpenSSH on a non-tailnet interface- Local sudo rules that do not hand blanket root to every interactive user
- Separate local accounts for deploy versus human operators
- Host firewall or bind decisions that keep management paths off the public internet
Tailscale SSH can own port 22 on the tailnet interface while OpenSSH stays off the public address. That split is still sound. Just do not assume “Tailscale said non-root” was ever enough by itself.
Tighten SSH rules where root is intentional. Prefer explicit local usernames over autogroup:nonroot when the set of accounts is small and stable. Use action: "check" with a short checkPeriod for root or other high-privilege targets so step-up re-auth is required. Session recording (via tsrecorder tags) does not prevent the bypass, but it gives you forensic evidence if someone abused a pre-patch window.
Do not “fix” by trying -i in production. On unpatched nodes that would escalate. Validate versions and policy instead.
Broader takeaway for mesh SSH designs
Any system that maps a remote identity onto a local Unix session has two hard jobs: authorize the mapping, and start the session without reinterpreting the target name as flags, paths, or magic numbers. The first job is where most product marketing lives. The second is where this class of bug lives.
Tailscale’s response (reject leading dashes, ban numeric-only names) is the right shape of fix. The reporting credits go to Anthropic and Ada Logics for the argument-handling issue, and to Tim Hoffman for the earlier UID bypass. The pattern is familiar from decades of Unix tooling: never pass untrusted strings to CLI helpers without a -- separator or an explicit non-option API.
For working developers, the practical summary is short. Upgrade Tailscale to 1.98.9+ on every Linux host that runs Tailscale SSH. Re-read ACL rules that depend on autogroup:nonroot. Keep local root hard to reach even when policy is supposed to do the heavy lifting. The mesh and the central policy are still useful. They are not magic, and username parsing is not a detail you can ignore.
Sources & further reading
- TS-2026-009: Insecure argument handling in Tailscale SSH permitted root access — tailscale.com
- Tailscale ssh "operation not permitted" · Issue #6888 · tailscale/tailscale — github.com
- Who Logs You In When You Use Tailscale SSH? | Stonecharioteer on Tech — tech.stonecharioteer.com
- Tailscale SSH: Set Up Secure Remote Access Without Managing Keys — oneuptime.com
Ji-ho covers the increasingly tangled overlap between cloud architecture and security, drawing on a background as a penetration tester to keep his reporting grounded in real-world attack paths. He never lets a vendor claim go unquestioned and insists that every buzzword come with a proof of concept.
Discussion 0
No comments yet
Be the first to weigh in.