juanfont/headscale · error

unknown field

Error message

unknown field

What it means

ErrUnknownField is returned by unmarshalPolicy (hscontrol/policy/v2/types.go:3140) when the HuJSON policy parses successfully but JSON unmarshalling into the Policy struct hits a key the struct does not define (encoding/json v2 ErrUnknownName wrapped in a SemanticError). It means the policy file contains a field that headscale's policy schema does not know, so the whole policy is rejected. The message includes the offending field name via JSON pointer's last token.

Source

Thrown at hscontrol/policy/v2/types.go:145

	ErrTagNotDefined               = errors.New("tag not found")
	ErrAutoApproverNotAlias        = errors.New("auto approver is not an alias")
	ErrInvalidACLAction            = errors.New("invalid ACL action")
	ErrInvalidSSHAction            = errors.New("invalid SSH action")
	ErrInvalidProtocolNumber       = errors.New("invalid protocol number")
	ErrProtocolLeadingZero         = errors.New("leading 0 not permitted in protocol number")
	ErrProtocolOutOfRange          = errors.New("protocol number out of range (0-255)")
	ErrAutogroupNotSupported       = errors.New("autogroup not supported in headscale")
	ErrAutogroupInternetSrc        = errors.New("autogroup:internet can only be used in ACL destinations")
	ErrAutogroupSelfSrc            = errors.New("\"autogroup:self\" not valid on the src side of a rule")
	ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
	ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
	ErrAutogroupDangerAllDst       = errors.New("cannot use autogroup:danger-all as a dst")
	ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
	ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
	ErrHostNotDefined              = errors.New("host not defined in policy")
	ErrSSHSourceAliasNotSupported  = errors.New("alias not supported for SSH source")
	ErrSSHDestAliasNotSupported    = errors.New("alias not supported for SSH destination")
	ErrUnknownField                = errors.New("unknown field")
	ErrProtocolNoSpecificPorts     = errors.New("protocol does not support specific ports")
	ErrTestEmptyAssertions         = errors.New("test entry must have at least one of \"accept\" or \"deny\"")
	ErrTestProtocolNotAllowed      = errors.New("test protocol must be tcp, udp, sctp, or empty")
	ErrTestDestinationMultiPort    = errors.New("test destination port must be a single port")
	ErrTestDestinationCIDR         = errors.New("test destination must be a single host, not a CIDR range")
	ErrAutogroupInternetTestDst    = errors.New("autogroup:internet not valid as a test destination")
	ErrSSHTestEmptySrc             = errors.New("SSH tests entry must have a non-empty src")
	ErrSSHTestEmptyDst             = errors.New("SSH tests entry must have at least one dst")
	ErrSSHTestDstUnknownTag        = errors.New("SSH tests dst contains unknown tag")
	ErrSSHTestDstDisallowedElement = errors.New("SSH tests dst contains disallowed element")
)

type resolved struct {
	ips netipx.IPSet
}

func newResolved(ipb *netipx.IPSetBuilder) (resolved, error) {
	ips, err := ipb.IPSet()

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Check the error's quoted field name and locate it in your policy file (search for the exact key)
  2. Rename the key to the current schema's name (see hscontrol/policy/v2/types.go Policy struct fields, e.g. grants, hosts, tagOwners, autoApprovers, tests)
  3. If the key is genuinely unneeded, delete it from the policy
  4. Validate the policy after editing: headscale policy check / reload, or run the policy through the v2 compiler before deploying

Example fix

// before (policy.hujson)
{
  "acls": [ ... ]
}
// after
{
  "grants": [ ... ]
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Before loading, verify every top-level key against the known set
var knownTopLevel = map[string]bool{"acls": true, "grants": true, "hosts": true, "tagOwners": true, "autoApprovers": true, "groups": true, "tests": true, "sshTests": true, "nodeAttrs": true}
func checkTopLevelKeys(t *testing.T, policyHuJSON []byte) {
    ast, _ := hujson.Parse(policyHujson)
    // walk top-level literal members and fail on unknown keys
}

Try / catch

// When compiling/loading a policy
pol, err := policyv2.CompileHuJSON(buf)
if err != nil {
    if errors.Is(err, policyv2.ErrUnknownField) {
        // err message quotes the bad field; surface it with file+key to the user
        return fmt.Errorf("policy contains unknown field: %w", err)
    }
    return err
}

Prevention

When it happens

Trigger: Loading any policy (config file, ACL HuJSON, or API policy update) that contains a misspelled or unsupported top-level or nested key, e.g. "acls" vs the supported "grants", "destPorts", or a typo like "tagOwner". Raised during unmarshalPolicy -> json.Unmarshal(ast.Pack(), &policy) when errors.AsType[*json.SemanticError] finds errors.Is(serr.Err, json.ErrUnknownName).

Common situations: Copying an ACL block from Tailscale or old headscale docs into a newer policy format that renamed fields; hand-editing policy HuJSON; upgrading headscale across a policy schema change where a field was renamed/removed; trailing garbage keys left after refactoring a policy.

Related errors


AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15). Data as JSON: /api/errors/19fcbef019ef1ca2. Report an issue: GitHub.