{"record":{"id":"e36361400bdaf77b","repo":"juanfont/headscale","slug":"w-q-e36361","errorCode":null,"errorMessage":"%w %q","messagePattern":"%w %q","errorType":"validation","errorClass":"ErrSSHDestinationHostAlias","httpStatus":null,"severity":"error","filePath":"hscontrol/policy/v2/types.go","lineNumber":2505,"sourceCode":"\t\t\t\t\tcontinue\n\t\t\t\t}\n\n\t\t\t\terr = validateAutogroupForSSHDst(ag)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t\tcontinue\n\t\t\t\t}\n\t\t\tcase *Tag:\n\t\t\t\ttagOwner := dst\n\n\t\t\t\terr := p.TagOwners.Contains(tagOwner)\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)\n\t\t\t\t}\n\t\t\tcase *Host:\n\t\t\t\t// Hosts-table aliases are valid on ACL dst but\n\t\t\t\t// rejected here for SSH dst.\n\t\t\t\terrs = append(errs, fmt.Errorf(\"%w %q\", ErrSSHDestinationHostAlias, string(*dst)))\n\t\t\t}\n\t\t}\n\n\t\t// Validate SSH source/destination combinations follow Tailscale's security model\n\t\terr := validateSSHSrcDstCombination(ssh.Sources, ssh.Destinations)\n\t\tif err != nil {\n\t\t\terrs = append(errs, err)\n\t\t}\n\n\t\t// Validate checkPeriod\n\t\tif ssh.CheckPeriod != nil {\n\t\t\tswitch {\n\t\t\tcase ssh.Action != SSHActionCheck:\n\t\t\t\terrs = append(errs, ErrSSHCheckPeriodOnNonCheck)\n\t\t\tdefault:\n\t\t\t\terr := ssh.CheckPeriod.Validate()\n\t\t\t\tif err != nil {\n\t\t\t\t\terrs = append(errs, err)","sourceCodeStart":2487,"sourceCodeEnd":2523,"githubUrl":"https://github.com/juanfont/headscale/blob/565fd254d06c4c7f9a8cad1714a43445c79ba420/hscontrol/policy/v2/types.go#L2487-L2523","documentation":"Thrown in Policy.validate()'s SSH destination loop (hscontrol/policy/v2/types.go:2505) when an ssh rule destination is a hosts-table alias (*Host). Hosts are valid aliases for ACL destinations but not for SSH destinations — SSH targets nodes by identity (user, tag, autogroup), not by IP alias. The sentinel ErrSSHDestinationHostAlias reads \"invalid dst\", so the message is `invalid dst \"webserver\"`.","triggerScenarios":"An ssh rule with \"dst\": [\"webserver\"] where webserver is a hosts-table entry; typically copy-pasted from an ACL block that legitimately used the same host alias.","commonSituations":"Reusing ACL host aliases in ssh blocks; attempting to target a node by its stable IP alias instead of its tag or owning user; policy porting from configs where hosts in ssh dst were silently ignored.","solutions":["Replace the host alias with the node's tag (tag:name, declared in tagOwners) or the owning username / autogroup:self","For same-user SSH, dst [\"autogroup:self\"] with the user or group as src","Keep host aliases for ACL dst entries only"],"exampleFix":"// before\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"webserver\"], \"users\": [\"root\"] }]\n\n// after\n\"tagOwners\": { \"tag:web\": [\"group:admin\"] },\n\"ssh\": [{ \"action\": \"accept\", \"src\": [\"group:eng\"], \"dst\": [\"tag:web\"], \"users\": [\"root\"] }]","handlingStrategy":"validation","validationCode":"func sshDstHasNoHosts(p *policyv2.Policy) []string {\n    var bad []string\n    for _, s := range p.SSH {\n        for _, d := range s.Destinations {\n            if h, ok := d.(*policyv2.Host); ok { bad = append(bad, string(*h)) }\n        }\n    }\n    return bad\n}","typeGuard":"func isHostAlias(s string) bool { return !strings.ContainsAny(s, \":/\") && net.ParseIP(s) == nil }","tryCatchPattern":"if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHDestinationHostAlias) { /* replace host alias with tag or autogroup:self */ }","preventionTips":["SSH dst vocabulary: usernames, tag:name, autogroup:member/tagged/self — never host aliases","SSH targets node identity, not addresses; host aliases are ACL-only"],"tags":["policy","ssh","hosts","validation"],"backgroundTag":null,"analyzedSha":"565fd254d06c4c7f9a8cad1714a43445c79ba420","analyzedAt":"2026-08-15T13:12:30.133Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}