juanfont/headscale · error · ErrSSHTestEmptyDst

SSH tests entry must have at least one dst

Error message

SSH tests entry must have at least one dst

What it means

ErrSSHTestEmptyDst is returned by validateSSHTests (hscontrol/policy/v2/types.go:3287) when an sshTests entry declares an empty or missing "dst" list. Each entry must name at least one SSH-reachable host for the assertion to mean anything; the destination list cannot be left blank or as [].

Source

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

	ErrAutogroupInternetSrc        = errors.New("autogroup:internet can only be used in ACL destinations")
	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) {

View on GitHub (pinned to 565fd254d0)

Solutions

  1. Add at least one dst to the flagged entry — a tag declared in tagOwners, a bare IP, or an allowed autogroup
  2. Delete the entry if it was scaffolding
  3. Re-validate the policy to catch follow-on dst shape errors

Example fix

// before
"sshTests": [{"src": "group:admin", "accept": ["root"]}]
// after
"sshTests": [{"src": "group:admin", "dst": ["tag:server"], "accept": ["root"]}]
Defensive patterns

Strategy: validation

Validate before calling

func sshTestHasDst(t SSHPolicyTest) bool { return len(t.Dst) > 0 }

Try / catch

if errors.Is(err, policyv2.ErrSSHTestEmptyDst) {
    // add at least one tag/IP/autogroup dst to the flagged entry
}

Prevention

When it happens

Trigger: An sshTests entry like {"src": "user:alice", "accept": ["root"]} with dst omitted, or "dst": []. Raised when len(t.Dst) == 0 during policy validation.

Common situations: Filling in the src and users first and forgetting the target; a templating step that produces empty dst arrays; refactoring a policy and dropping the dst line accidentally.

Related errors


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