juanfont/headscale · error · ErrSSHTagSourceToUserDest
%w (%s); use autogroup:tagged or specific tags as destinatio
Error message
%w (%s); use autogroup:tagged or specific tags as destinations instead
What it means
Thrown by validateSSHSrcDstCombination (hscontrol/policy/v2/types.go:2176) during Policy.validate(). The SSH rule has at least one tagged source (a tag: alias or autogroup:tagged) while another entry in the same rule's destinations is a Username. Tailscale's security model forbids tagged devices from SSHing to user-owned devices — tags confer machine identity, not user identity, so a check-user session cannot be attributed. The message suggests autogroup:tagged or specific tags as the destination instead.
Source
Thrown at hscontrol/policy/v2/types.go:2175
if v.Is(AutoGroupTagged) {
srcHasTaggedEntities = true
} else if v.Is(AutoGroupMember) {
srcHasGroups = true // autogroup:member is like a group of users
}
case *Group:
srcHasGroups = true
case *Username:
srcUsernames[string(*v)] = true
}
}
// Check destinations against source constraints
for _, dst := range destinations {
switch v := dst.(type) {
case *Username:
// Rule: Tags/autogroup:tagged CANNOT SSH to user destinations
if srcHasTaggedEntities {
return fmt.Errorf("%w (%s); use autogroup:tagged or specific tags as destinations instead",
ErrSSHTagSourceToUserDest, *v)
}
// Rule: Username destination requires source to be that same single user only
if srcHasGroups || len(srcUsernames) != 1 || !srcUsernames[string(*v)] {
return fmt.Errorf("%w %q; use autogroup:self instead for same-user SSH access",
ErrSSHUserDestRequiresSameUser, *v)
}
case *AutoGroup:
// Rule: autogroup:self requires source to NOT contain tags
if v.Is(AutoGroupSelf) && srcHasTaggedEntities {
return ErrSSHAutogroupSelfRequiresUserSource
}
// Rule: autogroup:member (user-owned devices) cannot be accessed by tagged entities
if v.Is(AutoGroupMember) && srcHasTaggedEntities {
return ErrSSHTagSourceToAutogroupMember
}
}
}View on GitHub (pinned to 565fd254d0)
Solutions
- Split the rule: one SSH rule with tag/autogroup:tagged sources and tag/autogroup:tagged destinations, and a separate rule with user/group sources and the username destination
- If the intent was same-user access from user-owned devices, use src=user/group + dst=autogroup:self
Example fix
// before
"ssh": [{ "action": "accept", "src": ["tag:prod", "group:eng"], "dst": ["alice"], "users": ["alice"] }]
// after
"ssh": [
{ "action": "accept", "src": ["group:eng"], "dst": ["alice"], "users": ["alice"] },
{ "action": "accept", "src": ["tag:prod"], "dst": ["tag:prod"], "users": ["root"] }
] Defensive patterns
Strategy: validation
Validate before calling
func sshRuleMixesTagsAndUserDst(s *policyv2.SSH) bool {
srcTagged, dstUser := false, false
for _, src := range s.Sources {
if _, ok := src.(*policyv2.Tag); ok { srcTagged = true }
if ag, ok := src.(*policyv2.AutoGroup); ok && ag.Is(policyv2.AutoGroupTagged) { srcTagged = true }
}
for _, dst := range s.Destinations { if _, ok := dst.(*policyv2.Username); ok { dstUser = true } }
return srcTagged && dstUser
} Type guard
func isTagAlias(s string) bool { return strings.HasPrefix(s, "tag:") } Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHTagSourceToUserDest) { /* split rule: tags→tag dst, users→user dst */ } Prevention
- Never mix tag sources and user destinations in one SSH rule — split it
- Design rule of thumb: machine sources target machine destinations, user sources target user destinations
When it happens
Trigger: An ssh rule like {"src": ["tag:prod", "group:eng"], "dst": ["alice"]} — any mix where srcHasTaggedEntities is true and dst contains a *Username. Also triggered when src includes autogroup:tagged and dst includes a username.
Common situations: Combining human and service sources in one SSH rule for brevity; migrating from older headscale/Tailscale versions before this combination check existed; writing a 'break-glass' rule with both tag and user sources.
Related errors
- SSH tests dst contains unknown tag
- tags in SSH source cannot access user-owned devices
- user destination requires source to contain only that same u
- autogroup:self destination requires source to contain only u
- tags in SSH source cannot access autogroup:member (user-owne
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/fe93ad3c46079e13.
Report an issue: GitHub.