juanfont/headscale · error
must be a positive duration
Error message
must be a positive duration
What it means
ErrSSHCheckPeriodNegative is a sentinel in hscontrol/policy/v2/types.go:48 returned when a Tailscale SSH rule's checkPeriod parses to a negative duration. headscale mirrors Tailscale SaaS: 0s is accepted (no minimum), but a negative value is meaningless for a re-check interval and is rejected during policy validation at load time.
Source
Thrown at hscontrol/policy/v2/types.go:48
json.MatchCaseInsensitiveNames(true),
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.HourView on GitHub (pinned to 565fd254d0)
Solutions
- Fix the checkPeriod value in the ssh rule to a positive duration ("5m", "12h") or "0s" if you want the SaaS-accepted no-minimum value
- Remove checkPeriod entirely to fall back to the default (SSHCheckPeriodDefault = 12h) if you did not intend to set it
- Re-apply/reload the policy and confirm headscale starts or `headscale policy set` succeeds
Example fix
// before
{"action": "check", "users": ["autogroup:nonroot"], "dst": ["tag:srv:user1"], "checkPeriod": "-5m"}
// after
{"action": "check", "users": ["autogroup:nonroot"], "dst": ["tag:srv:user1"], "checkPeriod": "5m"} Defensive patterns
Strategy: validation
Validate before calling
// before applying: every ssh rule with action "check" must have a non-negative checkPeriod
for _, r := range policy.SSH {
if r.CheckPeriod != nil && r.CheckPeriod.Duration() < 0 {
return fmt.Errorf("rule %v: negative checkPeriod %s", r, r.CheckPeriod)
}
} Try / catch
if err := hpolicy.LoadPolicy(bytes); err != nil {
if errors.Is(err, hpolicy.ErrSSHCheckPeriodNegative) { /* point operator at the rule */ }
} Prevention
- Lint policy files for negative duration strings before deploy
- Keep policies in version control and validate with `headscale policy check`-style dry runs in CI
When it happens
Trigger: An ssh rule in the policy file sets checkPeriod to a negative Go duration string (e.g. "checkPeriod": "-5m") with action "check". Validation in types.go (~line 2859) wraps it as fmt.Errorf("checkPeriod %s %w", p.Duration, ErrSSHCheckPeriodNegative) and the whole policy load fails.
Common situations: Typo in the policy HuJSON/JSON (a stray '-' or an en-dash pasted from docs), migrating a policy between tools that sign durations differently, or a templating/system that computes checkPeriod arithmetically and can go negative.
Related errors
- checkPeriod is only valid with action "check"
- invalid localpart format, must be localpart:*@<domain>
- users must be specified
- is not valid
- action must be specified
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/e0f1d6039da7c268.
Report an issue: GitHub.