{"record":{"id":"1fb638359bd0ad5d","repo":"juanfont/headscale","slug":"invalid-dst","errorCode":null,"errorMessage":"invalid dst","messagePattern":"invalid dst","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":56,"sourceCode":"var ErrUndefinedTagReference = errors.New(\"references undefined tag\")\n\n// SSH validation errors.\nvar (\n\tErrSSHTagSourceToUserDest             = errors.New(\"tags in SSH source cannot access user-owned devices\")\n\tErrSSHUserDestRequiresSameUser        = errors.New(\"user destination requires source to contain only that same user\")\n\tErrSSHAutogroupSelfRequiresUserSource = errors.New(\"autogroup:self destination requires source to contain only users or groups, not tags or autogroup:tagged\")\n\tErrSSHTagSourceToAutogroupMember      = errors.New(\"tags in SSH source cannot access autogroup:member (user-owned devices)\")\n\tErrSSHWildcardDestination             = errors.New(\"wildcard (*) is not supported as SSH destination\")\n\tErrSSHCheckPeriodAboveMax             = errors.New(\"is above the max (168h)\")\n\tErrSSHCheckPeriodNegative             = errors.New(\"must be a positive duration\")\n\tErrSSHCheckPeriodOnNonCheck           = errors.New(\"checkPeriod is only valid with action \\\"check\\\"\")\n\tErrInvalidLocalpart                   = errors.New(\"invalid localpart format, must be localpart:*@<domain>\")\n\tErrSSHUsersMustBeSpecified            = errors.New(\"users must be specified\")\n\tErrSSHUserInvalid                     = errors.New(\"is not valid\")\n\tErrSSHAcceptEnvEmpty                  = errors.New(\"acceptEnv values cannot be empty\")\n\tErrSSHActionMustBeSpecified           = errors.New(\"action must be specified\")\n\tErrSSHActionInvalid                   = errors.New(\"is not a valid action\")\n\tErrSSHDestinationHostAlias            = errors.New(\"invalid dst\")\n\tErrTagNameMustStartWithLetter         = errors.New(\"tag names must start with a letter, after 'tag:'\")\n\tErrGroupMembersCannotBeRecursive      = errors.New(\"group members cannot be recursive\")\n)\n\n// SSH check period constants per Tailscale docs:\n// https://tailscale.com/docs/features/tailscale-ssh#checkperiod\n// SaaS imposes no minimum (0s is accepted) so headscale matches.\nconst (\n\tSSHCheckPeriodDefault = 12 * time.Hour\n\tSSHCheckPeriodMax     = 7 * 24 * time.Hour\n)\n\n// ACL validation errors.\nvar (\n\tErrACLAutogroupSelfInvalidSource = errors.New(\"autogroup:self can only be used with users, groups, or supported autogroups\")\n)\n\n// Grant validation errors.","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L38-L74","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n{\"action\": \"accept\", \"users\": [\"user1\"], \"dst\": [\"tag:servers\"]}\n// after\n{\"action\": \"accept\", \"users\": [\"user1\"], \"dst\": [\"tag:servers:root\"]}","handlingStrategy":"validation","validationCode":"// ssh dst must be <alias>:<os-user>\nparts := strings.Split(dst, \":\")\nif len(parts) < 2 || parts[len(parts)-1] == \"\" { return fmt.Errorf(\"dst %q missing :user\", dst) }","typeGuard":"func isSSHDstForm(dst string) bool {\n    i := strings.LastIndex(dst, \":\")\n    return i > 0 && i < len(dst)-1\n}","tryCatchPattern":"if errors.Is(err, hpolicy.ErrSSHDestinationHostAlias) { /* error quotes the bad dst; append :<user> */ }","preventionTips":["Mentally expand dst as host:login — the OS user is mandatory","Never put IPs or CIDRs in ssh dst; use tags or hostnames"],"tags":["policy","ssh","dst","configuration","validation","go"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}