{"record":{"id":"20e984d5b8afdbbd","repo":"juanfont/headscale","slug":"user-destination-requires-source-to-contain-only-t","errorCode":null,"errorMessage":"user destination requires source to contain only that same user","messagePattern":"user destination requires source to contain only that same user","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":43,"sourceCode":")\n\n// Global JSON options for consistent parsing across all struct unmarshaling.\nvar policyJSONOpts = []json.Options{\n\tjson.DefaultOptionsV2(),\n\tjson.MatchCaseInsensitiveNames(true),\n\tjson.RejectUnknownMembers(true),\n}\n\nconst Wildcard = Asterix(0)\n\nvar ErrAutogroupSelfRequiresPerNodeResolution = errors.New(\"autogroup:self requires per-node resolution and cannot be resolved in this context\")\n\nvar ErrUndefinedTagReference = errors.New(\"references undefined tag\")\n\n// SSH validation errors.\nvar (\n\tErrSSHTagSourceToUserDest             = errors.New(\"tags in SSH source cannot access user-owned devices\")\n\tErrSSHUserDestRequiresSameUser        = errors.New(\"user destination requires source to contain only that same user\")\n\tErrSSHAutogroupSelfRequiresUserSource = errors.New(\"autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged\")\n\tErrSSHTagSourceToAutogroupMember      = errors.New(\"tags in SSH source cannot access autogroup:member (user-owned devices)\")\n\tErrSSHWildcardDestination             = errors.New(\"wildcard (*) is not supported as SSH destination\")\n\tErrSSHCheckPeriodAboveMax             = errors.New(\"is above the max (168h)\")\n\tErrSSHCheckPeriodNegative             = errors.New(\"must be a positive duration\")\n\tErrSSHCheckPeriodOnNonCheck           = errors.New(\"checkPeriod is only valid with action \\\"check\\\"\")\n\tErrInvalidLocalpart                   = errors.New(\"invalid localpart format, must be localpart:*@<domain>\")\n\tErrSSHUsersMustBeSpecified            = errors.New(\"users must be specified\")\n\tErrSSHUserInvalid                     = errors.New(\"is not valid\")\n\tErrSSHAcceptEnvEmpty                  = errors.New(\"acceptEnv values cannot be empty\")\n\tErrSSHActionMustBeSpecified           = errors.New(\"action must be specified\")\n\tErrSSHActionInvalid                   = errors.New(\"is not a valid action\")\n\tErrSSHDestinationHostAlias            = errors.New(\"invalid dst\")\n\tErrTagNameMustStartWithLetter         = errors.New(\"tag names must start with a letter, after 'tag:'\")\n\tErrGroupMembersCannotBeRecursive      = errors.New(\"group members cannot be recursive\")\n)\n\n// SSH check period constants per Tailscale docs:","sourceCodeStart":25,"sourceCodeEnd":61,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L25-L61","documentation":"SSH validation error (hscontrol/policy/v2/types.go:2178-2181): an SSH rule's dst is a specific user, but its src contains anything other than exactly that same user. Tailscale requires user-owned SSH destinations to be reachable in SSH rules only from the owning user (plus groups containing them); any other alias in src fails validation.","triggerScenarios":"{\"src\": [\"user:alice\", \"user:bob\"], \"dst\": [\"user:alice\"], ...} or {\"src\": [\"group:devs\"], \"dst\": [\"user:alice\"]} where the group contains more than alice — the error names the offending source value.","commonSituations":"Trying to grant admins SSH into all user devices (not expressible per-user; use a different model); listing multiple users in src for convenience; misunderstanding that dst-user implies src-user equality.","solutions":["Restrict src to exactly the dst user: {\"src\": [\"user:alice\"], \"dst\": [\"user:alice\"]}","Or move dst to autogroup:self so each user reaches their own devices under one rule","For admin access to user devices, have the users' nodes tagged or rely on Tailscale's check-action model instead"],"exampleFix":"// before\n{\"src\": [\"group:devs\"], \"dst\": [\"user:alice\"], \"users\": [\"alice\"], \"action\": \"accept\"}\n\n// after\n{\"src\": [\"group:devs\"], \"dst\": [\"autogroup:self\"], \"users\": [\"autogroup:nonroot\"], \"action\": \"check\", \"checkPeriod\": \"8h\"}","handlingStrategy":"validation","validationCode":"func sshUserDstRuleValid(src []string, dst string) bool { userDsts := filterNonTags(dst); if len(userDsts) == 0 { return true }; for _, d := range userDsts { if len(src) != 1 || src[0] != d { return false } }; return true }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Prefer dst autogroup:self over per-user rules","Never list extra sources beside a user dst","Use check actions for admin-style access instead"],"tags":["policy","ssh","validation","headscale"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}