juanfont/headscale · error · ErrSSHUserInvalid
user %q %w
Error message
user %q %w
What it means
Thrown in Policy.validate()'s SSH loop (hscontrol/policy/v2/types.go:2435) when a value in an ssh rule's users array is the empty string "" or "*". The sentinel ErrSSHUserInvalid reads "is not valid", producing e.g. `user "*" is not valid`. Empty means no login user; "*" as a check-user wildcard is not supported — any other string (including group:, autogroup:, or a malformed localpart) is treated as a literal login name and passes this check.
Source
Thrown at hscontrol/policy/v2/types.go:2435
}
for _, ssh := range p.SSHs {
// Empty action and users survive parse; surface them here.
if ssh.Action == "" {
errs = append(errs, ErrSSHActionMustBeSpecified)
}
if len(ssh.Users) == 0 {
errs = append(errs, ErrSSHUsersMustBeSpecified)
}
// "" and "*" are not valid login users; any other string
// (including autogroup, group, tag, malformed localpart) is
// treated as a literal user name.
for _, user := range ssh.Users {
switch user {
case "", "*":
errs = append(errs, fmt.Errorf("user %q %w", user, ErrSSHUserInvalid))
}
}
// acceptEnv entries cannot be empty; "*" and "**" are valid.
for _, env := range ssh.AcceptEnv {
if env == "" {
errs = append(errs, ErrSSHAcceptEnvEmpty)
}
}
for _, src := range ssh.Sources {
switch src := src.(type) {
case *AutoGroup:
ag := src
err := validateAutogroupSupported(ag)
if err != nil {
errs = append(errs, err)View on GitHub (pinned to 565fd254d0)
Solutions
- Replace "*" with the concrete login usernames to allow (e.g. "root", "alice", "autogroup:nonroot")
- Remove empty-string entries — usually a stray comma or unfilled template variable
- If the goal is non-root access for the connecting user, use "autogroup:nonroot" which is a valid literal here
Example fix
// before
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["autogroup:self"], "users": ["*"] }]
// after
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["autogroup:self"], "users": ["autogroup:nonroot", "alice"] }] Defensive patterns
Strategy: validation
Validate before calling
func sshUsersValid(p *policyv2.Policy) error {
for _, s := range p.SSH {
for _, u := range s.Users {
if u == "" || u == "*" { return fmt.Errorf("ssh user %q invalid", u) }
}
}
return nil
} Type guard
func isValidSSHUser(u string) bool { return u != "" && u != "*" } Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHUserInvalid) { /* list explicit login names or autogroup:nonroot */ } Prevention
- No wildcard for ssh users — enumerate names or use autogroup:nonroot
- Guard policy templates against empty users slots (trailing commas in HuJSON)
When it happens
Trigger: An ssh rule with "users": [""] (usually a trailing comma or an empty template slot in HuJSON) or "users": ["*"] intending 'any login user'.
Common situations: Generating policies with templates that leave an empty users entry; assuming "*" means all users as in some firewall CLIs; hand-editing HuJSON and leaving a dangling comma inside the users array.
Related errors
- is not valid
- tags in SSH source cannot access user-owned devices
- user destination requires source to contain only that same u
- autogroup:self destination requires source to contain only u
- tags in SSH source cannot access autogroup:member (user-owne
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/883b17bda3034ffd.
Report an issue: GitHub.