{"record":{"id":"778f0afcb228997c","repo":"juanfont/headscale","slug":"username-must-contain","errorCode":null,"errorMessage":"username must contain @","messagePattern":"username must contain @","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":108,"sourceCode":"\tErrNodeAttrsAutogroupNotAllowed = errors.New(\"nodeAttrs target does not support this autogroup\")\n\tErrNodeAttrUnsupported          = errors.New(\"nodeAttrs uses a feature headscale does not yet support\")\n\tErrNodeAttrIPPoolUnsupported    = errors.New(\"nodeAttrs ipPool requires the IP allocator (https://github.com/juanfont/headscale/issues/2912)\")\n\tErrNodeAttrTargetUnsupported    = errors.New(\"nodeAttrs target alias type is not supported\")\n)\n\n// nodeAttrUnsupportedCaps lists caps that headscale parses but cannot act on\n// today. Each entry maps to the tracking issue an operator can follow. The\n// caps are accepted by Tailscale SaaS, but delivering them via headscale\n// without the matching server-side machinery would be misleading — nodes\n// would advertise a feature that does not work. Reject at policy load and\n// point operators at the issue.\nvar nodeAttrUnsupportedCaps = map[tailcfg.NodeCapability]string{\n\ttailcfg.NodeAttrFunnel: \"https://github.com/juanfont/headscale/issues/2527\",\n}\n\n// Policy validation errors.\nvar (\n\tErrInvalidUsername             = errors.New(\"username must contain @\")\n\tErrUserNotFound                = errors.New(\"user not found\")\n\tErrMultipleUsersFound          = errors.New(\"multiple users found\")\n\tErrInvalidGroupFormat          = errors.New(\"group must start with 'group:'\")\n\tErrInvalidTagFormat            = errors.New(\"tag must start with 'tag:'\")\n\tErrInvalidHostname             = errors.New(\"invalid hostname\")\n\tErrHostResolve                 = errors.New(\"error resolving host\")\n\tErrInvalidPrefix               = errors.New(\"invalid prefix\")\n\tErrInvalidAutogroup            = errors.New(\"invalid autogroup\")\n\tErrUnknownAutogroup            = errors.New(\"unknown autogroup\")\n\tErrHostportMissingColon        = errors.New(\"hostport must contain a colon\")\n\tErrTypeNotSupported            = errors.New(\"type not supported\")\n\tErrInvalidAlias                = errors.New(\"invalid alias format\")\n\tErrInvalidAutoApprover         = errors.New(\"invalid auto approver format\")\n\tErrInvalidOwner                = errors.New(\"invalid owner format\")\n\tErrGroupNotDefined             = errors.New(\"group not defined in policy\")\n\tErrInvalidGroupMember          = errors.New(\"invalid group member type\")\n\tErrGroupValueNotArray          = errors.New(\"group value must be an array of users\")\n\tErrInvalidHostIP               = errors.New(\"hostname contains invalid IP address\")","sourceCodeStart":90,"sourceCodeEnd":126,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L90-L126","documentation":"Username.Validate (hscontrol/policy/v2/types.go:341-347) requires every username token in a policy to contain an '@'. headscale uses '@' as the discriminator that a bare string is a user (vs group:/tag: prefixes). If the username is not naturally an email, a trailing '@' must be appended; resolveUser strips it again before matching Email/Name (types.go:395).","triggerScenarios":"Unmarshalling any policy alias position (group member, tagOwner, grant src/dst) with a string lacking '@', e.g. \"alice\" instead of \"alice@\" or \"alice@example.com\". Thrown at UnmarshalJSON time, so the whole policy parse fails.","commonSituations":"Using OIDC/username-based identity where names are not emails and forgetting the trailing '@'; writing 'group:admins' members as bare names; migrating from a v1 policy or Tailscale ACL that allowed bare usernames.","solutions":["Append '@' to bare usernames (\"alice\" -> \"alice@\")","Use the full email when the user was created via email","Check the actual user Name/Email with 'headscale users list' and use that exact value plus '@' if needed"],"exampleFix":"// before\n{\"groups\": {\"group:admins\": [\"alice\"]}}\n// after\n{\"groups\": {\"group:admins\": [\"alice@\"]}}","handlingStrategy":"validation","validationCode":"func validPolicyUsername(s string) bool { return strings.Contains(s, \"@\") }","typeGuard":"func isPolicyUsername(s string) bool { return strings.Contains(s, \"@\") }","tryCatchPattern":"if errors.Is(err, policy.ErrInvalidUsername) {\n    // the wrapped value shows the token; append '@' if it is a bare name\n}","preventionTips":["Always write bare usernames with a trailing '@'","Use full emails when available","CI-lint every alias for the '@' requirement"],"tags":["headscale","policy","username","acl","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}