juanfont/headscale · error

tag not found

Error message

tag not found

What it means

Returned by TagOwners.Contains (hscontrol/policy/v2/types.go:1488) and surfaced from Policy.validate when a tag: alias is used in a rule but has no corresponding entry in the policy's "tagOwners" map. Headscale (like Tailscale) requires every tag referenced in ACLs or SSH rules to be owned by at least one user or group, otherwise nodes cannot claim it.

Source

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

	ErrUserNotFound                = errors.New("user not found")
	ErrMultipleUsersFound          = errors.New("multiple users found")
	ErrInvalidGroupFormat          = errors.New("group must start with 'group:'")
	ErrInvalidTagFormat            = errors.New("tag must start with 'tag:'")
	ErrInvalidHostname             = errors.New("invalid hostname")
	ErrHostResolve                 = errors.New("error resolving host")
	ErrInvalidPrefix               = errors.New("invalid prefix")
	ErrInvalidAutogroup            = errors.New("invalid autogroup")
	ErrUnknownAutogroup            = errors.New("unknown autogroup")
	ErrHostportMissingColon        = errors.New("hostport must contain a colon")
	ErrTypeNotSupported            = errors.New("type not supported")
	ErrInvalidAlias                = errors.New("invalid alias format")
	ErrInvalidAutoApprover         = errors.New("invalid auto approver format")
	ErrInvalidOwner                = errors.New("invalid owner format")
	ErrGroupNotDefined             = errors.New("group not defined in policy")
	ErrInvalidGroupMember          = errors.New("invalid group member type")
	ErrGroupValueNotArray          = errors.New("group value must be an array of users")
	ErrInvalidHostIP               = errors.New("hostname contains invalid IP address")
	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")

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Add a tagOwners entry granting an existing user/group ownership: "tagOwners": {"tag:server": ["group:admin"]}
  2. Verify the tag name matches exactly (case-sensitive, must start with "tag:") in both tagOwners and the rule
  3. If the tag is obsolete, remove its references from acls/ssh sections instead

Example fix

// before
"acls": [{"action": "accept", "src": ["tag:server"], "dst": ["*:*"]}]

// after
"tagOwners": {"tag:server": ["group:admin"]},
"acls": [{"action": "accept", "src": ["tag:server"], "dst": ["*:*"]}]
Defensive patterns

Strategy: validation

Validate before calling

// Verify every tag referenced in rules has an owner before loading
func tagsReferenced(p map[string]any) map[string]bool {
	refs := map[string]bool{}
	for _, section := range []string{"acls", "ssh", "tests"} {
		// walk strings starting with "tag:" in src/dst arrays
	}
	return refs
}
// then: every ref must be a key of tagOwners

Type guard

func isTagNotDefined(err error) bool {
	return errors.Is(err, policy.ErrTagNotDefined)
}

Try / catch

if err := p.Validate(); err != nil {
	if errors.Is(err, policy.ErrTagNotDefined) {
		// wrapped message contains the missing tag name
		return fmt.Errorf("policy rejected, add tagOwners entry: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Policy with "acls": [{"action": "accept", "src": ["tag:server"], "dst": [...]}] but no "tagOwners": {"tag:server": [...]}. Also fires for tags used in SSH rules and (per the TODO at types.go:1997) will increasingly cover dst sides. Reported as "src=tag not found: ..." when raised from an ACL source check.

Common situations: Adding a new tag to ACLs and forgetting to grant ownership; renaming a tag in tagOwners but not in the rules; migrating from a policy style that pre-declared tags elsewhere. Nodes using the tag fail to register until fixed.

Related errors


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