juanfont/headscale · error
users must be specified
Error message
users must be specified
What it means
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.
Source
Thrown at hscontrol/policy/v2/types.go:51
const Wildcard = Asterix(0)
var ErrAutogroupSelfRequiresPerNodeResolution = errors.New("autogroup:self requires per-node resolution and cannot be resolved in this context")
var ErrUndefinedTagReference = errors.New("references undefined tag")
// SSH validation errors.
var (
ErrSSHTagSourceToUserDest = errors.New("tags in SSH source cannot access user-owned devices")
ErrSSHUserDestRequiresSameUser = errors.New("user destination requires source to contain only that same user")
ErrSSHAutogroupSelfRequiresUserSource = errors.New("autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged")
ErrSSHTagSourceToAutogroupMember = errors.New("tags in SSH source cannot access autogroup:member (user-owned devices)")
ErrSSHWildcardDestination = errors.New("wildcard (*) is not supported as SSH destination")
ErrSSHCheckPeriodAboveMax = errors.New("is above the max (168h)")
ErrSSHCheckPeriodNegative = errors.New("must be a positive duration")
ErrSSHCheckPeriodOnNonCheck = errors.New("checkPeriod is only valid with action \"check\"")
ErrInvalidLocalpart = errors.New("invalid localpart format, must be localpart:*@<domain>")
ErrSSHUsersMustBeSpecified = errors.New("users must be specified")
ErrSSHUserInvalid = errors.New("is not valid")
ErrSSHAcceptEnvEmpty = errors.New("acceptEnv values cannot be empty")
ErrSSHActionMustBeSpecified = errors.New("action must be specified")
ErrSSHActionInvalid = errors.New("is not a valid action")
ErrSSHDestinationHostAlias = errors.New("invalid dst")
ErrTagNameMustStartWithLetter = errors.New("tag names must start with a letter, after 'tag:'")
ErrGroupMembersCannotBeRecursive = errors.New("group members cannot be recursive")
)
// SSH check period constants per Tailscale docs:
// https://tailscale.com/docs/features/tailscale-ssh#checkperiod
// SaaS imposes no minimum (0s is accepted) so headscale matches.
const (
SSHCheckPeriodDefault = 12 * time.Hour
SSHCheckPeriodMax = 7 * 24 * time.Hour
)
// ACL validation errors.View on GitHub (pinned to 565fd254d0)
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
Example fix
// before
{"action": "accept", "dst": ["tag:srv:user1"]}
// after
{"action": "accept", "users": ["autogroup:nonroot"], "dst": ["tag:srv:user1"]} Defensive patterns
Strategy: validation
Validate before calling
for _, r := range policy.SSH {
if len(r.Users) == 0 {
return fmt.Errorf("ssh rule needs users")
}
} Try / catch
if errors.Is(err, hpolicy.ErrSSHUsersMustBeSpecified) { /* add users array and re-apply */ } Prevention
- Treat users/action/dst as required keys in any ssh-rule schema
- Validate generated policy JSON against a schema requiring minItems:1 on users
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- must be a positive duration
- checkPeriod is only valid with action "check"
- invalid localpart format, must be localpart:*@<domain>
- is not valid
- action must be specified
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/0e7d6d5dd612eb87.
Report an issue: GitHub.