juanfont/headscale · error · ErrSSHDestinationHostAlias
%w %q
Error message
%w %q
What it means
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"`.
Source
Thrown at hscontrol/policy/v2/types.go:2505
continue
}
err = validateAutogroupForSSHDst(ag)
if err != nil {
errs = append(errs, err)
continue
}
case *Tag:
tagOwner := dst
err := p.TagOwners.Contains(tagOwner)
if err != nil {
errs = append(errs, err)
}
case *Host:
// Hosts-table aliases are valid on ACL dst but
// rejected here for SSH dst.
errs = append(errs, fmt.Errorf("%w %q", ErrSSHDestinationHostAlias, string(*dst)))
}
}
// Validate SSH source/destination combinations follow Tailscale's security model
err := validateSSHSrcDstCombination(ssh.Sources, ssh.Destinations)
if err != nil {
errs = append(errs, err)
}
// Validate checkPeriod
if ssh.CheckPeriod != nil {
switch {
case ssh.Action != SSHActionCheck:
errs = append(errs, ErrSSHCheckPeriodOnNonCheck)
default:
err := ssh.CheckPeriod.Validate()
if err != nil {
errs = append(errs, err)View on GitHub (pinned to 565fd254d0)
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
Example fix
// before
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["webserver"], "users": ["root"] }]
// after
"tagOwners": { "tag:web": ["group:admin"] },
"ssh": [{ "action": "accept", "src": ["group:eng"], "dst": ["tag:web"], "users": ["root"] }] Defensive patterns
Strategy: validation
Validate before calling
func sshDstHasNoHosts(p *policyv2.Policy) []string {
var bad []string
for _, s := range p.SSH {
for _, d := range s.Destinations {
if h, ok := d.(*policyv2.Host); ok { bad = append(bad, string(*h)) }
}
}
return bad
} Type guard
func isHostAlias(s string) bool { return !strings.ContainsAny(s, ":/") && net.ParseIP(s) == nil } Try / catch
if err := pol.Validate(); errors.Is(err, policyv2.ErrSSHDestinationHostAlias) { /* replace host alias with tag or autogroup:self */ } Prevention
- SSH dst vocabulary: usernames, tag:name, autogroup:member/tagged/self — never host aliases
- SSH targets node identity, not addresses; host aliases are ACL-only
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- tags in SSH source cannot access user-owned devices
- user destination requires source to contain only that same u
- autogroup:self destination requires source to contain only u
- tags in SSH source cannot access autogroup:member (user-owne
- wildcard (*) is not supported as SSH destination
AI-assisted analysis of juanfont/headscale@565fd254d0 (2026-08-15).
Data as JSON: /api/errors/e36361400bdaf77b.
Report an issue: GitHub.