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 combinations

View on GitHub (pinned to 565fd254d0)

Solutions

  1. If the intent is same-user SSH (user reaches their own devices), use src ["username"] or a group and dst ["autogroup:self"]
  2. 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
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/32400de3a514e685. Report an issue: GitHub.