juanfont/headscale · info

invalid SSH action

Error message

invalid SSH action

What it means

Declared at hscontrol/policy/v2/types.go:130 but currently has NO production return site: SSHAction.UnmarshalJSON actually reports invalid values with the separate sentinel ErrSSHActionInvalid ("is not a valid action", types.go:55) at types.go:1642. Valid SSH actions are "accept" and "check" (plus empty string, which per-rule Validate() rejects later). If you are matching on this sentinel with errors.Is you will never match; match ErrSSHActionInvalid instead.

Source

Thrown at hscontrol/policy/v2/types.go:130

	ErrInvalidTagFormat            = errors.New("tag must start with 'tag:'")
	ErrInvalidHostname             = errors.New("invalid hostname")
	ErrHostResolve                 = errors.New("error resolving host")
	ErrInvalidPrefix               = errors.New("invalid prefix")
	ErrInvalidAutogroup            = errors.New("invalid autogroup")
	ErrUnknownAutogroup            = errors.New("unknown autogroup")
	ErrHostportMissingColon        = errors.New("hostport must contain a colon")
	ErrTypeNotSupported            = errors.New("type not supported")
	ErrInvalidAlias                = errors.New("invalid alias format")
	ErrInvalidAutoApprover         = errors.New("invalid auto approver format")
	ErrInvalidOwner                = errors.New("invalid owner format")
	ErrGroupNotDefined             = errors.New("group not defined in policy")
	ErrInvalidGroupMember          = errors.New("invalid group member type")
	ErrGroupValueNotArray          = errors.New("group value must be an array of users")
	ErrInvalidHostIP               = errors.New("hostname contains invalid IP address")
	ErrTagNotDefined               = errors.New("tag not found")
	ErrAutoApproverNotAlias        = errors.New("auto approver is not an alias")
	ErrInvalidACLAction            = errors.New("invalid ACL action")
	ErrInvalidSSHAction            = errors.New("invalid SSH action")
	ErrInvalidProtocolNumber       = errors.New("invalid protocol number")
	ErrProtocolLeadingZero         = errors.New("leading 0 not permitted in protocol number")
	ErrProtocolOutOfRange          = errors.New("protocol number out of range (0-255)")
	ErrAutogroupNotSupported       = errors.New("autogroup not supported in headscale")
	ErrAutogroupInternetSrc        = errors.New("autogroup:internet can only be used in ACL destinations")
	ErrAutogroupSelfSrc            = errors.New("\"autogroup:self\" not valid on the src side of a rule")
	ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
	ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
	ErrAutogroupDangerAllDst       = errors.New("cannot use autogroup:danger-all as a dst")
	ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
	ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
	ErrHostNotDefined              = errors.New("host not defined in policy")
	ErrSSHSourceAliasNotSupported  = errors.New("alias not supported for SSH source")
	ErrSSHDestAliasNotSupported    = errors.New("alias not supported for SSH destination")
	ErrUnknownField                = errors.New("unknown field")
	ErrProtocolNoSpecificPorts     = errors.New("protocol does not support specific ports")
	ErrTestEmptyAssertions         = errors.New("test entry must have at least one of \"accept\" or \"deny\"")
	ErrTestProtocolNotAllowed      = errors.New("test protocol must be tcp, udp, sctp, or empty")

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Use "accept" or "check" as SSH action values
  2. In Go, match the runtime error with errors.Is(err, policy.ErrSSHActionInvalid), not ErrInvalidSSHAction
  3. If you maintain this package, consider removing or wiring up the unused sentinel to avoid confusion

Example fix

// before (Go error matching that never fires)
if errors.Is(err, policy.ErrInvalidSSHAction) { ... }

// after
if errors.Is(err, policy.ErrSSHActionInvalid) { ... }
Defensive patterns

Strategy: try-catch

Validate before calling

const validSSHActions = map[string]bool{"accept": true, "check": true, "": true}
for _, r := range rawSSH {
	if !validSSHActions[r.Action] {
		return fmt.Errorf("ssh action must be accept or check, got %q", r.Action)
	}
}

Type guard

// NOTE: runtime uses ErrSSHActionInvalid, not ErrInvalidSSHAction
func isInvalidSSHAction(err error) bool {
	return errors.Is(err, policy.ErrSSHActionInvalid) || errors.Is(err, policy.ErrInvalidSSHAction)
}

Try / catch

if err := json.Unmarshal(b, &p); err != nil {
	// the actually-returned sentinel is ErrSSHActionInvalid ("is not a valid action")
	if errors.Is(err, policy.ErrSSHActionInvalid) {
		return fmt.Errorf("ssh action must be accept or check: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: An SSH rule with "action": "acceptx" or "deny" produces `"deny" is not a valid action` wrapping ErrSSHActionInvalid — not this error. This sentinel would only appear if code explicitly returned it, which none currently does.

Common situations: Developers writing error-matching code against the sentinel list in types.go and wondering why errors.Is never fires; users seeing an invalid-SSH-action message and grepping for the wrong sentinel.

Related errors


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