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
- Declare the tag under tagOwners with an owner list (group or user)
- Remove the stale dst tag from the ACL
- 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
- Run `headscale policy check` (or your validate call) in CI so tag refs are verified before apply
- The dst= prefix in the message names the exact failing tag — fix that one first, errors are aggregated
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.