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

  1. 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
  2. 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

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


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