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

  1. Replace the host alias with the node's tag (tag:name, declared in tagOwners) or the owning username / autogroup:self
  2. For same-user SSH, dst ["autogroup:self"] with the user or group as src
  3. 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

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


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