juanfont/headscale · error · ErrSSHTestDstUnknownTag
SSH tests dst contains unknown tag
Error message
SSH tests dst contains unknown tag
What it means
ErrSSHTestDstUnknownTag is returned by validateSSHTestDestination (hscontrol/policy/v2/types.go:3341-3343) when an sshTests dst names a tag that is not declared in tagOwners. Tag entries must exist before they can be SSH test targets. Notably, a dst like "tag:server:22" (with a port suffix) also lands here: the parser only checks the tag: prefix, so the port suffix makes the lookup miss and it surfaces as an unknown tag (comment at types.go:3334-3336).
Source
Thrown at hscontrol/policy/v2/types.go:154
ErrAutogroupSelfSrc = errors.New("\"autogroup:self\" not valid on the src side of a rule")
ErrAutogroupNotSupportedACLSrc = errors.New("autogroup not supported for ACL sources")
ErrAutogroupNotSupportedACLDst = errors.New("autogroup not supported for ACL destinations")
ErrAutogroupDangerAllDst = errors.New("cannot use autogroup:danger-all as a dst")
ErrAutogroupNotSupportedSSHSrc = errors.New("autogroup not supported for SSH sources")
ErrAutogroupNotSupportedSSHDst = errors.New("autogroup not supported for SSH destinations")
ErrHostNotDefined = errors.New("host not defined in policy")
ErrSSHSourceAliasNotSupported = errors.New("alias not supported for SSH source")
ErrSSHDestAliasNotSupported = errors.New("alias not supported for SSH destination")
ErrUnknownField = errors.New("unknown field")
ErrProtocolNoSpecificPorts = errors.New("protocol does not support specific ports")
ErrTestEmptyAssertions = errors.New("test entry must have at least one of \"accept\" or \"deny\"")
ErrTestProtocolNotAllowed = errors.New("test protocol must be tcp, udp, sctp, or empty")
ErrTestDestinationMultiPort = errors.New("test destination port must be a single port")
ErrTestDestinationCIDR = errors.New("test destination must be a single host, not a CIDR range")
ErrAutogroupInternetTestDst = errors.New("autogroup:internet not valid as a test destination")
ErrSSHTestEmptySrc = errors.New("SSH tests entry must have a non-empty src")
ErrSSHTestEmptyDst = errors.New("SSH tests entry must have at least one dst")
ErrSSHTestDstUnknownTag = errors.New("SSH tests dst contains unknown tag")
ErrSSHTestDstDisallowedElement = errors.New("SSH tests dst contains disallowed element")
)
type resolved struct {
ips netipx.IPSet
}
func newResolved(ipb *netipx.IPSetBuilder) (resolved, error) {
ips, err := ipb.IPSet()
if err != nil {
return resolved{}, err
}
return resolved{ips: *ips}, nil
}
func newResolvedAddresses(ips *netipx.IPSet, err error) (ResolvedAddresses, error) {
if ips == nil {View on GitHub (pinned to 565fd254d0)
Solutions
- Check tagOwners declares the exact tag from the error message (quoted in it)
- Fix typos or rename the sshTests dst to match the declared tag
- Remove any :port suffix from ssh dst entries — SSH tests take bare hosts/tags only
- If the tag is intentional, add it to tagOwners with an owner
Example fix
// before
"tagOwners": {"tag:server": ["group:admin"]}
"sshTests": [{"src": "group:admin", "dst": ["tag:server:22"], "accept": ["root"]}]
// after
"tagOwners": {"tag:server": ["group:admin"]}
"sshTests": [{"src": "group:admin", "dst": ["tag:server"], "accept": ["root"]}] Defensive patterns
Strategy: type-guard
Validate before calling
// Ensure every tag used in sshTests dst exists in tagOwners before saving
func sshTestTagsDeclared(pol *Policy) bool {
for _, t := range pol.SSHTests {
for _, dst := range t.Dst {
if tag, ok := dst.(*Tag); ok {
if err := pol.TagOwners.Contains(tag); err != nil { return false }
}
}
}
return true
} Type guard
// Strip port suffixes: sshTests dst must be a bare tag or IP
func isBareSSHTestDst(dst string) bool {
// "tag:server:22" has two colons; valid forms are "tag:x" or an IP/host
return strings.Count(dst, ":") <= 1 || strings.Contains(dst, "]")
} Try / catch
if errors.Is(err, policyv2.ErrSSHTestDstUnknownTag) {
// message quotes the tag: add it to tagOwners or fix the name; strip any :port suffix
} Prevention
- SSH test destinations take no ports — never append :22
- Keep tag names consistent between tagOwners and sshTests (single source/renames in one commit)
- Run validation immediately after renaming tags
When it happens
Trigger: sshTests dst "tag:prod" when tagOwners has no "tag:prod" key; or dst "tag:server:22" where the :22 suffix turns the whole string into a non-declared tag. Raised when pol.TagOwners.Contains(a) fails (or pol is nil).
Common situations: Typos in tag names between sshTests and tagOwners; renaming a tag in tagOwners but not in sshTests; habitually adding :22 port to SSH test destinations (SSH tests take no ports); running validation before tagOwners is populated.
Related errors
- SSH tests entry must have a non-empty src
- SSH tests entry must have at least one dst
- %w (%s); use autogroup:tagged or specific tags as destinatio
- tags in SSH source cannot access user-owned devices
- user destination requires source to contain only that same u
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/65d10e2255815bea.
Report an issue: GitHub.