{"record":{"id":"0e7d6d5dd612eb87","repo":"juanfont/headscale","slug":"users-must-be-specified","errorCode":null,"errorMessage":"users must be specified","messagePattern":"users must be specified","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":51,"sourceCode":"\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:\n// https://tailscale.com/docs/features/tailscale-ssh#checkperiod\n// SaaS imposes no minimum (0s is accepted) so headscale matches.\nconst (\n\tSSHCheckPeriodDefault = 12 * time.Hour\n\tSSHCheckPeriodMax     = 7 * 24 * time.Hour\n)\n\n// ACL validation errors.","sourceCodeStart":33,"sourceCodeEnd":69,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L33-L69","documentation":"ErrSSHUsersMustBeSpecified in hscontrol/policy/v2/types.go:51 is returned when an SSH rule has an empty or missing users list. The users field defines which identities on the tailnet may initiate the SSH session, so an SSH rule without users has no meaning and headscale rejects it at policy validation time.","triggerScenarios":"A policy ssh block omits \"users\" or sets it to [] while defining action/dst. Validation fails with users must be specified before any other rule checks run.","commonSituations":"Hand-writing a first SSH rule and forgetting the users key; YAML/JSON indentation that accidentally detaches the users array; trimming a rule down for testing and removing all entries.","solutions":["Add a users array to the rule, e.g. [\"autogroup:nonroot\"], [\"user1\"], or [\"localpart:*@corp.com\"]","If you intended a broad rule, use an autogroup like autogroup:member or autogroup:nonroot rather than leaving users empty","Re-apply the policy"],"exampleFix":"// before\n{\"action\": \"accept\", \"dst\": [\"tag:srv:user1\"]}\n// after\n{\"action\": \"accept\", \"users\": [\"autogroup:nonroot\"], \"dst\": [\"tag:srv:user1\"]}","handlingStrategy":"validation","validationCode":"for _, r := range policy.SSH {\n    if len(r.Users) == 0 {\n        return fmt.Errorf(\"ssh rule needs users\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, hpolicy.ErrSSHUsersMustBeSpecified) { /* add users array and re-apply */ }","preventionTips":["Treat users/action/dst as required keys in any ssh-rule schema","Validate generated policy JSON against a schema requiring minItems:1 on users"],"tags":["policy","ssh","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}