tailscale/tailscale · error

requested certificate for IP %v does not match the connectio

Error message

requested certificate for IP %v does not match the connection's IP address

What it means

A defensive validation in ipCertManager.getCertificate: the SNI parsed as an IP literal, but after Unmap it does not equal the connection's local IP address (or the local IP could not be determined). The server refuses to serve a certificate for an IP it is not actually terminating the connection on, preventing issuance loops and mis-issuance attempts via spoofed SNI.

Source

Thrown at cmd/derper/ipcert.go:186

		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.
func (m *ipCertManager) certForIP(ctx context.Context, ip netip.Addr) (*tls.Certificate, error) {
	m.mu.Lock()
	e := m.entryLocked(ip)

View on GitHub (pinned to cfe32b8be6)

Solutions

  1. Run derper directly on the host that owns the public IP (host network mode in Docker: --network=host) so LocalAddr matches the certified IP.
  2. If NAT is unavoidable, switch to DNS-name certificate mode for that derper and use hostname SNI instead of IP SNI.
  3. Verify what the server sees: log hi.Conn.LocalAddr() and compare with the SNI value to identify the rewriting layer.

Example fix

# before: container NAT breaks SNI-vs-local-IP equality
docker run -p 443:443 -p 80:80 derper ...   # LocalAddr is the container IP

# after: host networking so the cert IP equals the connection IP
docker run --network=host derper ...
Defensive patterns

Strategy: validation

Validate before calling

// Before requesting an IP cert, assert SNI equals the connection's local IP.
func sniMatchesConn(hi *tls.ClientHelloInfo) bool {
    sni, err := netip.ParseAddr(hi.ServerName)
    if err != nil { return false }
    local, ok := connLocalIP(hi)
    return ok && sni.Unmap() == local
}

Try / catch

if _, err := mgr.GetCertificate(hi); err != nil {
    if strings.Contains(err.Error(), "does not match the connection's IP") {
        // deployment problem: NAT/container rewriting dst; fix network, don't retry
    }
}

Prevention

When it happens

Trigger: Client dials IP-B but sends SNI for IP-A (different address); more commonly, network address translation rewrites the destination: derper behind Docker port-publish, k8s NodePort/LoadBalancer, or a cloud LB doing DNAT, so hi.Conn.LocalAddr is a private IP (10.x/172.x) while the SNI carries the public IP. !connIPOK (non-IP local addr) also triggers it when SNI is an IP.

Common situations: Containerized derper where the cert is for the public IP but the listener sees the pod IP; multi-homed hosts where the TLS listener binds the wrong interface; clients behind 1:1 NAT; health probes that set an arbitrary SNI IP.

Understand the failure class

Related errors


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