juanfont/headscale · error

dst=%q: %w

Error message

dst=%q: %w

What it means

Thrown in Policy.validate() (hscontrol/policy/v2/types.go:2402) when a tag: alias used as an ACL destination is not declared in tagOwners. The error from p.TagOwners.Contains(h) is wrapped as "dst=%q: %w" with the offending tag interpolated, so the message pinpoints which destination alias failed. Tag destinations must be ownable/declared even though ownership matters mostly for claiming nodes.

Source

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

				if err != nil {
					errs = append(errs, err)
					continue
				}

				err = validateAutogroupForDst(h)
				if err != nil {
					errs = append(errs, err)
					continue
				}
			case *Group:
				err := p.Groups.Contains(h)
				if err != nil {
					errs = append(errs, err)
				}
			case *Tag:
				err := p.TagOwners.Contains(h)
				if err != nil {
					errs = append(errs, fmt.Errorf("dst=%q: %w", *h, err))
				}
			}
		}

		// Validate protocol-port compatibility
		if err := validateProtocolPortCompatibility(acl.Protocol, acl.Destinations); err != nil { //nolint:noinlineerr
			errs = append(errs, err)
		}

		// Validate ACL source/destination combinations follow Tailscale's security model
		err := validateACLSrcDstCombination(acl.Sources, acl.Destinations)
		if err != nil {
			errs = append(errs, err)
		}
	}

	for _, ssh := range p.SSHs {
		// Empty action and users survive parse; surface them here.

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Declare the tag under tagOwners with an owner list (group or user)
  2. Remove the stale dst tag from the ACL
  3. Double-check exact tag spelling/case against the %q value in the message

Example fix

// before
"acls": [{ "action": "accept", "src": ["group:app"], "dst": ["tag:db:5432"] }]

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

Strategy: validation

Validate before calling

func aclDstTagsDeclared(p *policyv2.Policy) error {
    for _, acl := range p.ACLs {
        for _, d := range acl.Destinations {
            if t, ok := d.Alias.(*policyv2.Tag); ok {
                if err := p.TagOwners.Contains(t); err != nil { return fmt.Errorf("dst=%q: %w", *t, 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(), "dst=") { /* register the dst tag in tagOwners */ }

Prevention

When it happens

Trigger: An ACL entry with "dst": ["tag:db:5432"] where "db" is absent from tagOwners. The parallel src-side check produces the 'src=' wording instead.

Common situations: Adding a new service tag to dst but forgetting tagOwners; cleaning up tagOwners and missing one ACL dst reference; casing mismatch between tagOwners key and ACL dst.

Related errors


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