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.

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)

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.