{"record":{"id":"6e21bf8d96dd058c","repo":"juanfont/headscale","slug":"errpreauthkeyacltaginvalid","errorCode":"ErrPreAuthKeyACLTagInvalid","errorMessage":"%w: '%s' did not begin with 'tag:'","messagePattern":"%w: '(.+?)' did not begin with 'tag:'","errorType":"validation","errorClass":"ErrPreAuthKeyACLTagInvalid","httpStatus":400,"severity":"error","filePath":"hscontrol/db/preauth_keys.go","lineNumber":37,"sourceCode":"\t// deleted key is treated as a missing record by callers, which the\n\t// registration handler maps to a 401 rather than a raw server error.\n\tErrPreAuthKeyNotFound          = fmt.Errorf(\"auth-key not found: %w\", gorm.ErrRecordNotFound)\n\tErrPreAuthKeyExpired           = errors.New(\"auth-key expired\")\n\tErrSingleUseAuthKeyHasBeenUsed = errors.New(\"auth-key has already been used\")\n\tErrUserMismatch                = errors.New(\"user mismatch\")\n\tErrPreAuthKeyACLTagInvalid     = errors.New(\"auth-key tag is invalid\")\n)\n\n// validateACLTags deduplicates, sorts, and checks that every tag carries the\n// \"tag:\" prefix. Shared by the pre-auth-key and OAuth credential paths so both\n// enforce the same tag shape.\nfunc validateACLTags(tags []string) ([]string, error) {\n\ttags = set.SetOf(tags).Slice()\n\tslices.Sort(tags)\n\n\tfor _, tag := range tags {\n\t\tif !strings.HasPrefix(tag, \"tag:\") {\n\t\t\treturn nil, fmt.Errorf(\n\t\t\t\t\"%w: '%s' did not begin with 'tag:'\",\n\t\t\t\tErrPreAuthKeyACLTagInvalid,\n\t\t\t\ttag,\n\t\t\t)\n\t\t}\n\t}\n\n\treturn tags, nil\n}\n\nfunc (hsdb *HSDatabase) CreatePreAuthKey(\n\tuid *types.UserID,\n\treusable bool,\n\tephemeral bool,\n\texpiration *time.Time,\n\taclTags []string,\n) (*types.PreAuthKeyNew, error) {\n\treturn Write(hsdb.DB, func(tx *gorm.DB) (*types.PreAuthKeyNew, error) {","sourceCodeStart":19,"sourceCodeEnd":55,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/db/preauth_keys.go#L19-L55","documentation":"validateACLTags rejects any tag lacking the literal 'tag:' prefix, wrapping ErrPreAuthKeyACLTagInvalid with the offending value in the message. It runs in both the pre-auth-key and OAuth client creation paths (the function is shared), enforcing Tailscale's ACL tag shape before anything is persisted. Deduplication and sorting happen first, so the error reports a normalized tag.","triggerScenarios":"Calling CreatePreAuthKey or OAuth client creation with tags like [\"server\"] or [\"web-server\"] instead of [\"tag:server\"]; automation feeding raw host roles as tags.","commonSituations":"Scripts translating cloud labels or Kubernetes labels directly into headscale tags; policy files that elsewhere use bare names, encouraging the same habit at key creation.","solutions":["Prefix each tag with 'tag:' at creation time","Validate tags in the UI/API layer before hitting the DB function","Cross-check tag names against the policy file's tagOwners so nodes can actually claim them"],"exampleFix":"// before\nkey, err := hsdb.CreatePreAuthKey(&uid, false, false, nil, []string{\"webserver\"})\n\n// after\nkey, err := hsdb.CreatePreAuthKey(&uid, false, false, nil, []string{\"tag:webserver\"})","handlingStrategy":"validation","validationCode":"for i, t := range tags {\n\tif !strings.HasPrefix(t, \"tag:\") {\n\t\ttags[i] = \"tag:\" + t // or reject explicitly\n\t}\n}\nif _, err := db.CreatePreAuthKey(&uid, false, false, nil, tags); err != nil {\n\treturn err\n}","typeGuard":"func areValidACLTags(tags []string) bool {\n\tfor _, t := range tags {\n\t\tif !strings.HasPrefix(t, \"tag:\") {\n\t\t\treturn false\n\t\t}\n\treturn true\n}","tryCatchPattern":"if _, err := db.CreatePreAuthKey(&uid, reuse, ephemeral, expiration, tags); err != nil {\n\tif errors.Is(err, db.ErrPreAuthKeyACLTagInvalid) {\n\t\treturn fmt.Errorf(\"tags must start with 'tag:': got %v\", tags)\n\t}\n\treturn err\n}","preventionTips":["Normalize tags at the input boundary (API/UI), not at the DB call","Keep tag lists in sync with the policy file's tagOwners section","Reject, don't auto-prefix, in admin surfaces so users learn the shape"],"tags":["go","preauth-key","acl","validation","tags"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}