{"record":{"id":"5f1db76fa1458a2f","repo":"juanfont/headscale","slug":"invalid-acl-action","errorCode":null,"errorMessage":"invalid ACL action","messagePattern":"invalid ACL action","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":129,"sourceCode":"\tErrInvalidGroupFormat          = errors.New(\"group must start with 'group:'\")\n\tErrInvalidTagFormat            = errors.New(\"tag must start with 'tag:'\")\n\tErrInvalidHostname             = errors.New(\"invalid hostname\")\n\tErrHostResolve                 = errors.New(\"error resolving host\")\n\tErrInvalidPrefix               = errors.New(\"invalid prefix\")\n\tErrInvalidAutogroup            = errors.New(\"invalid autogroup\")\n\tErrUnknownAutogroup            = errors.New(\"unknown autogroup\")\n\tErrHostportMissingColon        = errors.New(\"hostport must contain a colon\")\n\tErrTypeNotSupported            = errors.New(\"type not supported\")\n\tErrInvalidAlias                = errors.New(\"invalid alias format\")\n\tErrInvalidAutoApprover         = errors.New(\"invalid auto approver format\")\n\tErrInvalidOwner                = errors.New(\"invalid owner format\")\n\tErrGroupNotDefined             = errors.New(\"group not defined in policy\")\n\tErrInvalidGroupMember          = errors.New(\"invalid group member type\")\n\tErrGroupValueNotArray          = errors.New(\"group value must be an array of users\")\n\tErrInvalidHostIP               = errors.New(\"hostname contains invalid IP address\")\n\tErrTagNotDefined               = errors.New(\"tag not found\")\n\tErrAutoApproverNotAlias        = errors.New(\"auto approver is not an alias\")\n\tErrInvalidACLAction            = errors.New(\"invalid ACL action\")\n\tErrInvalidSSHAction            = errors.New(\"invalid SSH action\")\n\tErrInvalidProtocolNumber       = errors.New(\"invalid protocol number\")\n\tErrProtocolLeadingZero         = errors.New(\"leading 0 not permitted in protocol number\")\n\tErrProtocolOutOfRange          = errors.New(\"protocol number out of range (0-255)\")\n\tErrAutogroupNotSupported       = errors.New(\"autogroup not supported in headscale\")\n\tErrAutogroupInternetSrc        = errors.New(\"autogroup:internet can only be used in ACL destinations\")\n\tErrAutogroupSelfSrc            = errors.New(\"\\\"autogroup:self\\\" not valid on the src side of a rule\")\n\tErrAutogroupNotSupportedACLSrc = errors.New(\"autogroup not supported for ACL sources\")\n\tErrAutogroupNotSupportedACLDst = errors.New(\"autogroup not supported for ACL destinations\")\n\tErrAutogroupDangerAllDst       = errors.New(\"cannot use autogroup:danger-all as a dst\")\n\tErrAutogroupNotSupportedSSHSrc = errors.New(\"autogroup not supported for SSH sources\")\n\tErrAutogroupNotSupportedSSHDst = errors.New(\"autogroup not supported for SSH destinations\")\n\tErrHostNotDefined              = errors.New(\"host not defined in policy\")\n\tErrSSHSourceAliasNotSupported  = errors.New(\"alias not supported for SSH source\")\n\tErrSSHDestAliasNotSupported    = errors.New(\"alias not supported for SSH destination\")\n\tErrUnknownField                = errors.New(\"unknown field\")\n\tErrProtocolNoSpecificPorts     = errors.New(\"protocol does not support specific ports\")\n\tErrTestEmptyAssertions         = errors.New(\"test entry must have at least one of \\\"accept\\\" or \\\"deny\\\"\")","sourceCodeStart":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L111-L147","documentation":"Returned by Action.UnmarshalJSON (hscontrol/policy/v2/types.go:1613) when an ACL rule's \"action\" field is anything other than \"accept\". Headscale ACLs are allow-only (absence of a matching rule means deny), matching Tailscale's grants model; the sentinel is wrapped as `action=%q is not supported` so the offending value is shown.","triggerScenarios":"Policy with \"acls\": [{\"action\": \"deny\", \"src\": [...], \"dst\": [...]}], or a typo like \"Accept\"/\"allow\"/\"permit\". Fires during JSON unmarshalling, i.e. at policy load, before any other validation runs.","commonSituations":"Copy-pasting a firewall-style ruleset that mixes allow/deny rules; migrating from iptables-style thinking; users expecting explicit deny semantics. Any deny intent must be expressed by simply not granting access.","solutions":["Remove deny rules entirely — headscale policies are default-deny, only list what you allow","Check spelling and case: the only valid value is exactly \"accept\"","If you had a deny override, restructure by making the more specific accept rules not match that traffic"],"exampleFix":"// before\n\"acls\": [\n  {\"action\": \"accept\", \"src\": [\"group:admin\"], \"dst\": [\"*:*\"]},\n  {\"action\": \"deny\", \"src\": [\"*\"], \"dst\": [\"tag:server:22\"]}\n]\n\n// after\n\"acls\": [\n  {\"action\": \"accept\", \"src\": [\"group:admin\"], \"dst\": [\"*:*\"]}\n]","handlingStrategy":"validation","validationCode":"// Only 'accept' is valid for ACL actions\nfor _, acl := range rawAcls {\n\tif act := strings.TrimSpace(acl[\"action\"]); act != \"accept\" {\n\t\treturn fmt.Errorf(\"acls action must be 'accept', got %q\", act)\n\t}\n}","typeGuard":"func isInvalidACLAction(err error) bool {\n\treturn errors.Is(err, policy.ErrInvalidACLAction)\n}","tryCatchPattern":"if err := json.Unmarshal(b, &p); err != nil {\n\tif errors.Is(err, policy.ErrInvalidACLAction) {\n\t\t// wrapped message shows action=%q; drop deny rules, policies are allow-only\n\t\treturn fmt.Errorf(\"bad ACL action (policies are allow-only): %w\", err)\n\t}\n\treturn err\n}","preventionTips":["Remember headscale ACLs are default-deny: only list what you permit","Ban the string 'deny' in policy templates/lint rules","Validate generated policies with `headscale policy check` in CI"],"tags":["policy","acl","validation","json"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}