tailscale/tailscale · error

cert expired %v

Error message

cert expired %v

What it means

Pinned-cert verification checks validity dates manually because crypto/tls path verification is skipped (net/tlsdial/tlsdial.go:345-348). The pinned leaf's NotAfter is in the past: the hash matched, so the pin points at a certificate that has since expired. Renewals always require a new pin.

Source

Thrown at net/tlsdial/tlsdial.go:347

		for _, cert := range cs.PeerCertificates {
			if strings.HasPrefix(cert.Subject.CommonName, derpconst.MetaCertCommonNamePrefix) {
				continue
			}
			if sawGoodCert {
				return errors.New("unexpected multiple certs presented")
			}
			if fmt.Sprintf("%02x", sha256.Sum256(cert.Raw)) != wantFullCertSHA256Hex {
				return fmt.Errorf("cert hash does not match expected cert hash")
			}
			if dialedHost != "" { // it's empty when dialing a derper by IP with no hostname
				if err := cert.VerifyHostname(dialedHost); err != nil {
					return fmt.Errorf("cert does not match server name %q: %w", dialedHost, err)
				}
			}
			now := time.Now()
			if now.After(cert.NotAfter) {
				return fmt.Errorf("cert expired %v", cert.NotAfter)
			}
			if now.Before(cert.NotBefore) {
				return fmt.Errorf("cert not yet valid until %v; is your clock correct?", cert.NotBefore)
			}
			sawGoodCert = true
		}
		if !sawGoodCert {
			return errors.New("expected cert not presented")
		}
		return nil
	}
}

// NewTransport returns a new HTTP transport that verifies TLS certs using this
// package, including its baked-in LetsEncrypt fallback roots.
func NewTransport() *http.Transport {
	return &http.Transport{
		DialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {

View on GitHub (pinned to 6e0912f979)

Solutions

  1. Fetch the server's current cert, verify it is otherwise legitimate, and re-pin its new SHA-256
  2. Confirm local time is correct — a badly wrong clock can fake expiry
  3. If you control the server, renew the certificate
  4. Prefer CA-based verification (tlsdial.Config) unless pinning is strictly required
Defensive patterns

Strategy: try-catch

Validate before calling

// before relying on a pin, check the served cert is still time-valid
if time.Now().After(leaf.NotAfter) {
	// pin is for an expired cert; refresh it before the dial fails
}

Try / catch

if err != nil && strings.Contains(err.Error(), "cert expired") {
	// deterministic, not transient: refresh the pin; retrying will not help
	return err
}

Prevention

When it happens

Trigger: Dialing with SetConfigExpectedCertHash after the pinned cert passed its expiry date; long-lived embedded deployments that pinned once and never rotated; cert renewal on the server while clients keep the old hash.

Common situations: Pins not maintained across 90-day LetsEncrypt renewals; air-gapped devices with rarely rotated certs but a stale pin table.

Related errors


AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18). Data as JSON: /api/errors/3221034606c2a491. Report an issue: GitHub.