tailscale/tailscale · error

no certificate for hostname %q; this server only serves IP a

Error message

no certificate for hostname %q; this server only serves IP address certificates

What it means

ipCertManager.getCertificate received a TLS ClientHello whose SNI (hi.ServerName) is a DNS name, but this manager only issues certificates for the server's own IP addresses, and no downstream hostname provider is chained (m.nextTLS is nil or has no GetCertificate). The handshake is aborted, so clients see a TLS alert instead of a certificate.

Source

Thrown at cmd/derper/ipcert.go:183

	}
	ta, ok := hi.Conn.LocalAddr().(*net.TCPAddr)
	if !ok {
		return netip.Addr{}, false
	}
	ip := ta.AddrPort().Addr().Unmap()
	return ip, ip.IsValid()
}

func (m *ipCertManager) getCertificate(hi *tls.ClientHelloInfo) (*tls.Certificate, error) {
	connIP, connIPOK := connLocalIP(hi)
	if hi.ServerName != "" {
		sniIP, err := netip.ParseAddr(hi.ServerName)
		if err != nil {
			// The SNI is a DNS name; let the hostname provider handle it.
			if m.nextTLS != nil && m.nextTLS.GetCertificate != nil {
				return m.nextTLS.GetCertificate(hi)
			}
			return nil, fmt.Errorf("no certificate for hostname %q; this server only serves IP address certificates", hi.ServerName)
		}
		if !connIPOK || sniIP.Unmap() != connIP {
			return nil, fmt.Errorf("requested certificate for IP %v does not match the connection's IP address", sniIP)
		}
	}
	if !connIPOK {
		return nil, errors.New("unable to determine the connection's local IP address")
	}
	ctx := hi.Context()
	if ctx == nil {
		ctx = context.Background()
	}
	return m.certForIP(ctx, connIP)
}

// certForIP returns the current certificate for ip, obtaining one
// first if there is no unexpired certificate for it. Concurrent
// callers for the same IP share a single issuance.

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Run derper with a DNS-name cert mode as well (e.g. --hostname plus ACME), so ipCertManager chains to the hostname provider via next.
  2. Point clients at the IP address (DERPMap node with IP, and TLS SNI set to the IP string) instead of a DNS name.
  3. If you must keep the DNS name, move the derper to a host where it can get a hostname certificate and disable IP-only mode.

Example fix

# before: only IP certs; DNS SNI handshakes fail
derper --certdir=/var/lib/derper --stun ...  (IP cert mode only)

# after: chain a hostname cert provider behind the IP cert manager
derper --hostname=derp.example.com --certmode=letsencrypt ...  # DNS names now served by the next provider
Defensive patterns

Strategy: validation

Validate before calling

// Client side: only send an SNI you know the server can serve.
host := "203.0.113.10"
if net.ParseIP(host) == nil && ipOnlyServer {
    host = serverIP // use the IP literal instead of a DNS name
}
conn, err := tls.Dial("tcp", host+":443", &tls.Config{ServerName: host})

Type guard

func isIPLiteral(s string) bool { return netip.ParseAddr(s).IsValid() } // valid SNI for an IP-cert derper

Try / catch

cert, err := mgr.GetCertificate(hi)
if err != nil && strings.Contains(err.Error(), "only serves IP address certificates") {
    // route the connection to a hostname-capable provider or reject early
    return nil, err
}

Prevention

When it happens

Trigger: Running derper in IP-address cert mode as the top-level certProvider while clients reach it via a DNS hostname: the client puts that hostname in SNI, netip.ParseAddr fails (it is not an IP literal), and there is no next provider to delegate to. Typical when a DNS A record points at the derper IP but the config only enables IP certs.

Common situations: Adding a friendly DNS name to a derper deployed with only IP cert mode; health checks or monitoring probes that send Hostname/SNI; tailscale DERPMap entries configured with a DNS hostname while the server serves IP certs.

Understand the failure class

Related errors


AI-assisted analysis of tailscale/tailscale@cfe32b8be6 (2026-08-15). Data as JSON: /api/errors/1a1b47267a9305b0. Report an issue: GitHub.