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
- Fetch the server's current cert, verify it is otherwise legitimate, and re-pin its new SHA-256
- Confirm local time is correct — a badly wrong clock can fake expiry
- If you control the server, renew the certificate
- 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
- Track certificate expiry in monitoring wherever pins are used
- Rotate pins on the same schedule as server certificate renewals
- Verify local time is correct before concluding the cert expired
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
- cert hash does not match expected cert hash
- cert does not match server name %q: %w
- cert not yet valid until %v; is your clock correct?
- error: %w
- dialRegion(%d): %w
AI-assisted analysis of tailscale/tailscale@6e0912f979 (2026-08-18).
Data as JSON: /api/errors/3221034606c2a491.
Report an issue: GitHub.