juanfont/headscale · error · ErrNodeAttrTargetUnsupported

%w: %q (%T)

Error message

%w: %q (%T)

What it means

Thrown in Policy.validate()'s nodeAttrs loop (hscontrol/policy/v2/types.go:2707) when a nodeAttrs target is an alias type that has no meaning as a target. Supported: *Host, *AutoGroup, *Tag, *Username, *Prefix, and Asterix (wildcard) — the latter three are deferred to compile time. Any other type lands in the default branch and is reported as ErrNodeAttrTargetUnsupported with the value and its Go type (%T), e.g. a Group used as a nodeAttrs target.

Source

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

				}
			case *Group:
				err := p.Groups.Contains(t)
				if err != nil {
					errs = append(errs, err)
				}
			case *Tag:
				err := p.TagOwners.Contains(t)
				if err != nil {
					errs = append(errs, err)
				}
			case *Username, *Prefix, Asterix:
				// User / prefix / wildcard targets are accepted at
				// parse time and resolved at compile time (where a
				// typo'd username surfaces as a propagated Resolve
				// error from compileNodeAttrs). Mirrors the grant
				// source-side validation shape.
			default:
				errs = append(errs, fmt.Errorf("%w: %q (%T)", ErrNodeAttrTargetUnsupported, target, target))
			}
		}

		for _, attr := range na.Attrs {
			issue, ok := nodeAttrUnsupportedCaps[attr]
			if ok {
				errs = append(errs, fmt.Errorf("%w: %q tracked in %s", ErrNodeAttrUnsupported, attr, issue))
			}
		}

		if len(na.IPPool) > 0 {
			errs = append(errs, ErrNodeAttrIPPoolUnsupported)
		}

		for _, prefix := range na.IPPool {
			err := validateNodeAttrIPPool(prefix)
			if err != nil {
				errs = append(errs, err)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Replace group:eng with autogroup:member (user-owned devices) or list the usernames explicitly
  2. Use tag:name (registered in tagOwners) to target tagged devices
  3. If you need wildcard coverage, use "*" (Asterix), which is accepted at parse time

Example fix

// before
"nodeAttrs": [{ "target": ["group:eng"], "attr": ["funnel"] }]

// after
"nodeAttrs": [{ "target": ["autogroup:member"], "attr": ["funnel"] }]
Defensive patterns

Strategy: validation

Validate before calling

func nodeAttrTargetsSupported(p *policyv2.Policy) error {
    for _, na := range p.NodeAttrs {
        for _, t := range na.Targets {
            switch t.(type) {
            case *policyv2.Host, *policyv2.AutoGroup, *policyv2.Tag, *policyv2.Username, *policyv2.Prefix, policyv2.Asterix:
            default:
                return fmt.Errorf("nodeAttrs target type %T unsupported", t)
            }
        }
    }
    return nil
}

Type guard

func isNodeAttrTarget(s string) bool {
    if s == "*" { return true }
    if strings.HasPrefix(s, "tag:") { return true }
    if strings.HasPrefix(s, "autogroup:") { return s == "autogroup:member" || s == "autogroup:tagged" }
    if _, err := netip.ParsePrefix(s); err == nil { return true }
    return !strings.ContainsAny(s, ":/") // bare name: host or username
}

Try / catch

if err := pol.Validate(); errors.Is(err, policyv2.ErrNodeAttrTargetUnsupported) { /* replace group:/odd alias with autogroup:member, a username, or a tag */ }

Prevention

When it happens

Trigger: A nodeAttrs entry with "target": ["group:eng"] — groups are not a supported nodeAttrs target type (they are valid for ACL/SSH/grant sources, which is why the parser produces a *Group alias that then reaches this switch).

Common situations: Assuming group: works everywhere autogroup:member does; SaaS policies that headscale's parser types differently than expected; using future/extension alias forms not yet modeled.

Related errors


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