juanfont/headscale · error

checkPeriod is only valid with action "check"

Error message

checkPeriod is only valid with action "check"

What it means

ErrSSHCheckPeriodOnNonCheck is a sentinel in hscontrol/policy/v2/types.go:49 returned when an SSH rule specifies checkPeriod but its action is not "check". checkPeriod only defines how often Tailscale re-validates an authenticated SSH session, so it is meaningless for action "accept" and headscale rejects the rule during policy validation.

Solutions

  1. Remove the checkPeriod field from the accept rule
  2. Or change "action" to "check" if you actually want periodic re-authentication
  3. Re-apply the policy and verify it loads

Example fix

// before
{"action": "accept", "users": ["user1"], "dst": ["tag:srv"], "checkPeriod": "1h"}
// after
{"action": "accept", "users": ["user1"], "dst": ["tag:srv"]}
Defensive patterns

Strategy: validation

Validate before calling

// reject checkPeriod on non-check rules before submit
for _, r := range policy.SSH {
    if r.Action != "check" && r.CheckPeriod != nil {
        return fmt.Errorf("accept rule must not set checkPeriod")
    }
}

Try / catch

if errors.Is(err, hpolicy.ErrSSHCheckPeriodOnNonCheck) { /* strip checkPeriod and retry apply */ }

Prevention

When it happens

Trigger: A policy ssh rule combines "action": "accept" (or omits/misspells action) with a "checkPeriod" field. Validation fails with checkPeriod is only valid with action "check" and the policy load aborts.

Common situations: Copy-pasting a "check" rule and changing action to "accept" without removing checkPeriod; converting Tailscale SaaS grants to ssh rules; upgrading headscale to a version that added strict SSH rule validation.

Related errors


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

Appendix: source

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

	json.RejectUnknownMembers(true),
}

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
)

View on GitHub (pinned to 565fd254d0)