{"record":{"id":"6a9f918c69d0bf69","repo":"juanfont/headscale","slug":"checkperiod-is-only-valid-with-action-check","errorCode":null,"errorMessage":"checkPeriod is only valid with action \"check\"","messagePattern":"checkPeriod is only valid with action \"check\"","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":49,"sourceCode":"\tjson.RejectUnknownMembers(true),\n}\n\nconst Wildcard = Asterix(0)\n\nvar ErrAutogroupSelfRequiresPerNodeResolution = errors.New(\"autogroup:self requires per-node resolution and cannot be resolved in this context\")\n\nvar ErrUndefinedTagReference = errors.New(\"references undefined tag\")\n\n// SSH validation errors.\nvar (\n\tErrSSHTagSourceToUserDest             = errors.New(\"tags in SSH source cannot access user-owned devices\")\n\tErrSSHUserDestRequiresSameUser        = errors.New(\"user destination requires source to contain only that same user\")\n\tErrSSHAutogroupSelfRequiresUserSource = errors.New(\"autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged\")\n\tErrSSHTagSourceToAutogroupMember      = errors.New(\"tags in SSH source cannot access autogroup:member (user-owned devices)\")\n\tErrSSHWildcardDestination             = errors.New(\"wildcard (*) is not supported as SSH destination\")\n\tErrSSHCheckPeriodAboveMax             = errors.New(\"is above the max (168h)\")\n\tErrSSHCheckPeriodNegative             = errors.New(\"must be a positive duration\")\n\tErrSSHCheckPeriodOnNonCheck           = errors.New(\"checkPeriod is only valid with action \\\"check\\\"\")\n\tErrInvalidLocalpart                   = errors.New(\"invalid localpart format, must be localpart:*@<domain>\")\n\tErrSSHUsersMustBeSpecified            = errors.New(\"users must be specified\")\n\tErrSSHUserInvalid                     = errors.New(\"is not valid\")\n\tErrSSHAcceptEnvEmpty                  = errors.New(\"acceptEnv values cannot be empty\")\n\tErrSSHActionMustBeSpecified           = errors.New(\"action must be specified\")\n\tErrSSHActionInvalid                   = errors.New(\"is not a valid action\")\n\tErrSSHDestinationHostAlias            = errors.New(\"invalid dst\")\n\tErrTagNameMustStartWithLetter         = errors.New(\"tag names must start with a letter, after 'tag:'\")\n\tErrGroupMembersCannotBeRecursive      = errors.New(\"group members cannot be recursive\")\n)\n\n// SSH check period constants per Tailscale docs:\n// https://tailscale.com/docs/features/tailscale-ssh#checkperiod\n// SaaS imposes no minimum (0s is accepted) so headscale matches.\nconst (\n\tSSHCheckPeriodDefault = 12 * time.Hour\n\tSSHCheckPeriodMax     = 7 * 24 * time.Hour\n)","sourceCodeStart":31,"sourceCodeEnd":67,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L31-L67","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Remove the checkPeriod field from the accept rule","Or change \"action\" to \"check\" if you actually want periodic re-authentication","Re-apply the policy and verify it loads"],"exampleFix":"// before\n{\"action\": \"accept\", \"users\": [\"user1\"], \"dst\": [\"tag:srv\"], \"checkPeriod\": \"1h\"}\n// after\n{\"action\": \"accept\", \"users\": [\"user1\"], \"dst\": [\"tag:srv\"]}","handlingStrategy":"validation","validationCode":"// reject checkPeriod on non-check rules before submit\nfor _, r := range policy.SSH {\n    if r.Action != \"check\" && r.CheckPeriod != nil {\n        return fmt.Errorf(\"accept rule must not set checkPeriod\")\n    }\n}","typeGuard":null,"tryCatchPattern":"if errors.Is(err, hpolicy.ErrSSHCheckPeriodOnNonCheck) { /* strip checkPeriod and retry apply */ }","preventionTips":["Only add checkPeriod when action is literally \"check\"","Generate ssh rules from a template that couples action=check with checkPeriod"],"tags":["policy","ssh","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}