- Looked like
- SSH or network reachability failure
- Actually
- Tailscale SSH ACL mismatch
- Layer
- Tailnet access policy
Field note
An edge node became unreachable through Tailscale SSH.
The operator attempted to connect to the node:
tailscale ssh operator@100.64.x.x
The connection failed with a tailnet policy denial. At first glance, this looked like a normal SSH permission issue, a Linux user problem, or a network connectivity failure.
However, the node was still reachable over the tailnet. Basic network access
was working. The failure only happened when using tailscale ssh.
What Made It Confusing
The Headscale ACL already allowed broad network access for admin users:
{
"action": "accept",
"src": ["admin@"],
"dst": ["*:*"]
}
This rule allowed network-level access to the node.
However, Tailscale SSH does not rely only on normal network ACL rules. It has a
separate ssh policy block that must explicitly allow the SSH source,
destination, and Linux login user.
That means these two operations go through different permission paths:
ssh operator@100.64.x.x
tailscale ssh operator@100.64.x.x
The node could be reachable over the tailnet while still being denied by Tailscale SSH.
Root Cause
A recent ACL change tightened Tailscale SSH access to tagged edge nodes only. The new SSH ACL looked like this:
"ssh": [
{
"action": "accept",
"src": ["tag:sandbox"],
"dst": ["tag:edge-agent"],
"users": ["root"]
}
]
This rule only allowed Tailscale SSH when all of the following were true:
- source was
tag:sandbox - destination was
tag:edge-agent - login user was
root
Some existing edge nodes had been provisioned before this tag-based SSH model was introduced. Those legacy nodes had Tailscale SSH enabled:
tailscale up --ssh
But they did not advertise the required destination tag:
tag:edge-agent
The attempted login user was also a non-root operator account, while the ACL
only allowed root.
Failure Chain
- ACL change introduced tag-based Tailscale SSH access.
- The new SSH ACL required destination nodes to have
tag:edge-agent. - Some legacy edge nodes had been created before this tag existed.
- Those nodes had
tailscale up --sshenabled. - They did not advertise
tag:edge-agent. - The operator attempted to log in as a non-root user.
- The
tailscale sshrequest did not match any SSH allow rule. - Tailscale denied access with a generic policy error.
Resolution
The immediate fix was to either add a temporary compatibility rule for legacy nodes or migrate those nodes to the new tagging model.
Example compatibility rule:
{
"action": "accept",
"src": ["admin@"],
"dst": ["worker@*", "master@*"],
"users": ["operator", "root"]
}
Affected nodes could also be migrated to advertise the expected tag:
tailscale up --ssh --advertise-tags=tag:edge-agent
The long-term fix was to ensure that newly provisioned nodes always receive the expected tag during onboarding.
Engineering Lesson
This was not a basic SSH failure. It was a production-readiness gap caused by changing the access model without fully handling existing nodes.
The system had two separate permission layers:
- network ACL controls whether packets can reach the node
- SSH ACL controls whether
tailscale sshcan log into the node
A production-ready rollout should have included a compatibility rule for existing untagged nodes, a migration path to retag legacy nodes, a provisioning check for new nodes, and validation for nodes with Tailscale SSH enabled but no matching SSH ACL.
Takeaway
The incident was caused by a tag-based Tailscale SSH ACL rollout that did not account for legacy untagged nodes.
The node was not offline. The network was not broken. The normal ACL was not the problem. The real failure was a mismatch between the new SSH ACL model and the state of previously provisioned nodes.
- Symptom
- Tailscale SSH to an edge node failed with a policy denial even though the node was reachable over the tailnet.
- Misleading hypothesis
- The failure looked like a normal SSH permission issue, Linux user problem, or network connectivity failure.
- Boundary expansion
- The investigation moved from network reachability and Linux SSH into the separate Tailscale SSH policy layer.
- Root cause
- A tag-based Tailscale SSH ACL rollout required destination nodes to advertise tag:edge-agent and only allowed root, while legacy nodes were untagged and operators were logging in as a non-root user.
- Signals
-
- The node was still reachable over the tailnet
- The denial only happened when using tailscale ssh
- Broad network ACLs existed, but Tailscale SSH uses a separate ssh policy block
- Legacy nodes had Tailscale SSH enabled without the destination tag required by the new policy
- Checks
-
- Confirmed basic tailnet reachability before treating the node as offline
- Compared the requested source, destination, and login user against the ssh ACL block
- Checked whether legacy edge nodes advertised tag:edge-agent
- Validated whether the attempted Linux user was allowed by the SSH ACL
Evidence trail
Reusable lesson
Treat Tailscale SSH as a separate access path from normal tailnet reachability: validate source, destination tags, and allowed login users before debugging Linux SSH or network transport.
Dealing with a failure that looks like one layer but might be another?