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

  1. Replace "*" with the concrete login usernames to allow (e.g. "root", "alice", "autogroup:nonroot")
  2. Remove empty-string entries — usually a stray comma or unfilled template variable
  3. 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

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


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/883b17bda3034ffd. Report an issue: GitHub.