juanfont/headscale · error · ErrSSHUserDestRequiresSameUser
%w %q; use autogroup:self instead for same-user SSH access
Error message
%w %q; use autogroup:self instead for same-user SSH access
What it means
Thrown by validateSSHSrcDstCombination (hscontrol/policy/v2/types.go:2181) during Policy.validate(). A Username SSH destination is only valid when the source list contains exactly one username, it is that same user, and there are no groups (or tags — those hit the sibling error first). This mirrors Tailscale's rule that user destinations mean 'this exact user's own devices', expressible more idiomatically as dst=autogroup:self.
Source
Thrown at hscontrol/policy/v2/types.go:2180
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
}
}
}
return nil
}
// validateACLSrcDstCombination validates that [ACL] source/destination combinationsView on GitHub (pinned to 565fd254d0)
Solutions
- If the intent is same-user SSH (user reaches their own devices), use src ["username"] or a group and dst ["autogroup:self"]
- If the intent is cross-user access, it is not supported for Username destinations — target the user's tagged devices instead, or have the user share access via tags
- Ensure src contains exactly one username equal to the dst username with no groups mixed in
Example fix
// before
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["alice"], "users": ["alice"] }]
// after
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["autogroup:self"], "users": ["autogroup:nonroot", "alice"] }] Defensive patterns
Strategy: validation
Validate before calling
func userDstHasSameSingleUser(s *policyv2.SSH) bool {
var users []string; hasGroup := false
for _, src := range s.Sources {
switch v := src.(type) {
case *policyv2.Username: users = append(users, string(*v))
case *policyv2.Group: hasGroup = true
}
}
if hasGroup || len(users) != 1 { return false }
for _, dst := range s.Destinations {
if u, ok := dst.(*policyv2.Username); ok && string(*u) != users[0] { return false }
}
return true
} Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHUserDestRequiresSameUser) { /* replace username dst with autogroup:self */ } Prevention
- Default to autogroup:self for 'user reaches own devices'; username dst is almost always wrong
- Cross-user access must go through tags, not username destinations
When it happens
Trigger: An ssh rule with dst containing "alice" but src being ["bob"], ["group:eng"], or ["alice", "bob"] (multiple usernames), or any src mixing a group with usernames. srcHasGroups or len(srcUsernames) != 1 or the username not present triggers it.
Common situations: Trying to grant admin SSH to another user's devices (not allowed by the model); listing several users in src for convenience; translating 'user X can SSH to user X's machines' literally instead of using autogroup:self.
Related errors
- 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
- wildcard (*) is not supported as SSH destination
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/32400de3a514e685.
Report an issue: GitHub.