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

  1. Check tagOwners declares the exact tag from the error message (quoted in it)
  2. Fix typos or rename the sshTests dst to match the declared tag
  3. Remove any :port suffix from ssh dst entries — SSH tests take bare hosts/tags only
  4. 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

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


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