juanfont/headscale · error

src=%w

Error message

src=%w

What it means

Thrown in Policy.validate() (hscontrol/policy/v2/types.go:2371) when a tag: alias used as an ACL source is not defined under tagOwners. The underlying error comes from p.TagOwners.Contains(tagOwner) and is wrapped with the prefix "src=" so the combined message reads like 'src=tag not found: "tag:foo"' (or the Contains wording), distinguishing source-position failures from destination ones. This enforces that only tags with declared owners can appear in ACLs.

Source

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

				err = validateAutogroupForSrc(ag)
				if err != nil {
					errs = append(errs, err)
					continue
				}
			case *Group:
				g := src

				err := p.Groups.Contains(g)
				if err != nil {
					errs = append(errs, err)
				}
			case *Tag:
				tagOwner := src

				err := p.TagOwners.Contains(tagOwner)
				if err != nil {
					errs = append(errs, fmt.Errorf("src=%w", err))
				}
			}
		}

		for _, dst := range acl.Destinations {
			switch h := dst.Alias.(type) {
			case *Host:
				if !p.Hosts.exist(*h) {
					errs = append(errs, fmt.Errorf("%w: %q", ErrHostNotDefined, *h))
				}
			case *AutoGroup:
				err := validateAutogroupSupported(h)
				if err != nil {
					errs = append(errs, err)
					continue
				}

				err = validateAutogroupForDst(h)

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Add the tag to tagOwners, e.g. "tagOwners": { "tag:prod": ["group:admin"] }
  2. Remove the tag from the ACL source if it is obsolete
  3. Verify exact casing and the tag: prefix — the %q in the message shows what was looked up

Example fix

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

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

Strategy: validation

Validate before calling

func aclSrcTagsDeclared(p *policyv2.Policy) error {
    for _, acl := range p.ACLs {
        for _, src := range acl.Sources {
            if t, ok := src.(*policyv2.Tag); ok {
                if err := p.TagOwners.Contains(t); err != nil { return err }
            }
        }
    }
    return nil
}

Type guard

func isTag(s string) bool { return strings.HasPrefix(s, "tag:") }

Try / catch

if err := pol.Validate(); strings.Contains(err.Error(), "src=") { /* check src tag spelling against tagOwners keys */ }

Prevention

When it happens

Trigger: An ACL entry with "src": ["tag:prod"] where "prod" is not a key in the policy's tagOwners map. Note "autogroup:tagged" is fine — this fires only for concrete tags.

Common situations: Deleting a tagOwners entry but leaving the tag in acls; introducing a new tag in ACLs before registering its owners; inconsistent naming (tag:Prod vs tag:prod — matching is case-sensitive).

Related errors


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