juanfont/headscale · error
invalid action
Error message
invalid action
What it means
Exported sentinel declared in hscontrol/policy/v2/filter.go:20 and raised during policy parsing when a grant's action field is not one of the valid values. In the ACL/grants model, 'action' only admits SSH-rule actions ('accept' or 'check'); anything else fails unmarshaling with this error before any filter compilation happens.
Source
Thrown at hscontrol/policy/v2/filter.go:20
import (
"errors"
"fmt"
"net/netip"
"slices"
"strings"
"time"
"github.com/juanfont/headscale/hscontrol/types"
"github.com/juanfont/headscale/hscontrol/util"
"github.com/rs/zerolog/log"
"go4.org/netipx"
"tailscale.com/tailcfg"
"tailscale.com/types/views"
)
var (
ErrInvalidAction = errors.New("invalid action")
errSelfInSources = errors.New("autogroup:self cannot be used in sources")
)
// companionCap pairs a well-known Tailscale capability with its
// companion capability.
type companionCap struct {
original tailcfg.PeerCapability
companion tailcfg.PeerCapability
}
// companionCaps lists certain well-known Tailscale capabilities and
// their companion capability. When a grant includes one of these
// capabilities, Tailscale automatically generates an additional
// [tailcfg.FilterRule] with the companion capability and a nil CapMap value.
// The slice is ordered by the original capability name so that
// generated companion rules are emitted deterministically.
var companionCaps = []companionCap{
{tailcfg.PeerCapabilityTaildrive, tailcfg.PeerCapabilityTaildriveSharer},View on GitHub (pinned to 565fd254d0)
Solutions
- Change the action to "accept" — deny semantics come from omitting the grant, since the policy is default-deny
- For SSH rules that re-check periodically, use "check" with a checkPeriod
- Validate the policy with 'headscale policy check' before applying
Example fix
// before
{"src": ["group:admin"], "dst": ["tag:server:22"], "action": "deny"}
// after (omit the grant entirely for deny; use accept/check otherwise)
{"src": ["group:admin"], "dst": ["tag:server:22"], "action": "accept"} Defensive patterns
Strategy: validation
Validate before calling
func validSSHAction(a string) bool { return a == "accept" || a == "check" } Prevention
- Remember policies are default-deny: express 'deny' by omission
- Lint policies with 'headscale policy check' in CI
- Generate policies with templating/IaC that only emits accept/check
When it happens
Trigger: A policy containing a grants or acls entry with e.g. "action": "deny" or a typo like "accpet". The HuJSON/JSON unmarshal path for grant actions rejects the value (see types_test.go 'invalid action should fail' and TestACL_UnmarshalJSON_InvalidAction).
Common situations: Migrating from a firewall mindset and writing 'deny' actions (Tailscale ACLs are default-deny, only accept/check exist); typo in an IaC/terraform-managed policy; hand-editing HuJSON.
Related errors
- test(s) failed
- autogroup:self requires per-node resolution and cannot be re
- references undefined tag
- wildcard (*) is not supported as SSH destination
- is above the max (168h)
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/51d426c2058cf419.
Report an issue: GitHub.