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
- Replace group:eng with autogroup:member (user-owned devices) or list the usernames explicitly
- Use tag:name (registered in tagOwners) to target tagged devices
- 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
- groups are never valid nodeAttrs targets — use autogroup:member or usernames
- The %T in the message reveals exactly which alias type was rejected; use it to find the offending token
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
- nodeAttrs target does not support this autogroup
- group must start with 'group:'
- invalid group member type
- group value must be an array of users
- %w: %q overlaps %q
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/f69d21fc6cd57582.
Report an issue: GitHub.