juanfont/headscale · error
invalid dst
Error message
invalid dst
What it means
ErrSSHDestinationHostAlias in hscontrol/policy/v2/types.go:56 is a fragment sentinel ("invalid dst") wrapped as invalid dst %q at types.go:2505. It fires when an SSH rule destination entry is not a valid host alias — i.e. not of the form <host-alias>:<user> where host-alias is a tag, hostname, or similar, and <user> is the target OS user.
Source
Thrown at hscontrol/policy/v2/types.go:56
var ErrUndefinedTagReference = errors.New("references undefined tag")
// SSH validation errors.
var (
ErrSSHTagSourceToUserDest = errors.New("tags in SSH source cannot access user-owned devices")
ErrSSHUserDestRequiresSameUser = errors.New("user destination requires source to contain only that same user")
ErrSSHAutogroupSelfRequiresUserSource = errors.New("autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged")
ErrSSHTagSourceToAutogroupMember = errors.New("tags in SSH source cannot access autogroup:member (user-owned devices)")
ErrSSHWildcardDestination = errors.New("wildcard (*) is not supported as SSH destination")
ErrSSHCheckPeriodAboveMax = errors.New("is above the max (168h)")
ErrSSHCheckPeriodNegative = errors.New("must be a positive duration")
ErrSSHCheckPeriodOnNonCheck = errors.New("checkPeriod is only valid with action \"check\"")
ErrInvalidLocalpart = errors.New("invalid localpart format, must be localpart:*@<domain>")
ErrSSHUsersMustBeSpecified = errors.New("users must be specified")
ErrSSHUserInvalid = errors.New("is not valid")
ErrSSHAcceptEnvEmpty = errors.New("acceptEnv values cannot be empty")
ErrSSHActionMustBeSpecified = errors.New("action must be specified")
ErrSSHActionInvalid = errors.New("is not a valid action")
ErrSSHDestinationHostAlias = errors.New("invalid dst")
ErrTagNameMustStartWithLetter = errors.New("tag names must start with a letter, after 'tag:'")
ErrGroupMembersCannotBeRecursive = errors.New("group members cannot be recursive")
)
// SSH check period constants per Tailscale docs:
// https://tailscale.com/docs/features/tailscale-ssh#checkperiod
// SaaS imposes no minimum (0s is accepted) so headscale matches.
const (
SSHCheckPeriodDefault = 12 * time.Hour
SSHCheckPeriodMax = 7 * 24 * time.Hour
)
// ACL validation errors.
var (
ErrACLAutogroupSelfInvalidSource = errors.New("autogroup:self can only be used with users, groups, or supported autogroups")
)
// Grant validation errors.View on GitHub (pinned to 565fd254d0)
Solutions
- Rewrite the dst entry as <alias>:<os-user>, e.g. tag:servers:root or myhost:deploy
- Use a defined tag or exact hostname as the alias part, not IPs/CIDRs
- Check the quoted dst value in the error message and fix that specific entry
Example fix
// before
{"action": "accept", "users": ["user1"], "dst": ["tag:servers"]}
// after
{"action": "accept", "users": ["user1"], "dst": ["tag:servers:root"]} Defensive patterns
Strategy: validation
Validate before calling
// ssh dst must be <alias>:<os-user>
parts := strings.Split(dst, ":")
if len(parts) < 2 || parts[len(parts)-1] == "" { return fmt.Errorf("dst %q missing :user", dst) } Type guard
func isSSHDstForm(dst string) bool {
i := strings.LastIndex(dst, ":")
return i > 0 && i < len(dst)-1
} Try / catch
if errors.Is(err, hpolicy.ErrSSHDestinationHostAlias) { /* error quotes the bad dst; append :<user> */ } Prevention
- Mentally expand dst as host:login — the OS user is mandatory
- Never put IPs or CIDRs in ssh dst; use tags or hostnames
When it happens
Trigger: An ssh rule dst entry missing the :user suffix (e.g. "tag:servers" instead of "tag:servers:root"), containing a bare "*" (which ErrSSHWildcardDestination also covers), or otherwise failing host-alias parsing at types.go:2505. The raw dst string is included in the error.
Common situations: Writing only the host without the OS user — the most common first mistake with SSH ACLs; pasting IP addresses or CIDRs (valid in ACL dst but not ssh dst); forgetting the tag: prefix.
Related errors
- must be a positive duration
- checkPeriod is only valid with action "check"
- invalid localpart format, must be localpart:*@<domain>
- users must be specified
- is not valid
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/1fb638359bd0ad5d.
Report an issue: GitHub.