{"record":{"id":"3221034606c2a491","repo":"tailscale/tailscale","slug":"cert-expired-v","errorCode":null,"errorMessage":"cert expired %v","messagePattern":"cert expired (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"net/tlsdial/tlsdial.go","lineNumber":347,"sourceCode":"\n\t\tfor _, cert := range cs.PeerCertificates {\n\t\t\tif strings.HasPrefix(cert.Subject.CommonName, derpconst.MetaCertCommonNamePrefix) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tif sawGoodCert {\n\t\t\t\treturn errors.New(\"unexpected multiple certs presented\")\n\t\t\t}\n\t\t\tif fmt.Sprintf(\"%02x\", sha256.Sum256(cert.Raw)) != wantFullCertSHA256Hex {\n\t\t\t\treturn fmt.Errorf(\"cert hash does not match expected cert hash\")\n\t\t\t}\n\t\t\tif dialedHost != \"\" { // it's empty when dialing a derper by IP with no hostname\n\t\t\t\tif err := cert.VerifyHostname(dialedHost); err != nil {\n\t\t\t\t\treturn fmt.Errorf(\"cert does not match server name %q: %w\", dialedHost, err)\n\t\t\t\t}\n\t\t\t}\n\t\t\tnow := time.Now()\n\t\t\tif now.After(cert.NotAfter) {\n\t\t\t\treturn fmt.Errorf(\"cert expired %v\", cert.NotAfter)\n\t\t\t}\n\t\t\tif now.Before(cert.NotBefore) {\n\t\t\t\treturn fmt.Errorf(\"cert not yet valid until %v; is your clock correct?\", cert.NotBefore)\n\t\t\t}\n\t\t\tsawGoodCert = true\n\t\t}\n\t\tif !sawGoodCert {\n\t\t\treturn errors.New(\"expected cert not presented\")\n\t\t}\n\t\treturn nil\n\t}\n}\n\n// NewTransport returns a new HTTP transport that verifies TLS certs using this\n// package, including its baked-in LetsEncrypt fallback roots.\nfunc NewTransport() *http.Transport {\n\treturn &http.Transport{\n\t\tDialTLSContext: func(ctx context.Context, network, addr string) (net.Conn, error) {","sourceCodeStart":329,"sourceCodeEnd":365,"githubUrl":"https://github.com/tailscale/tailscale/blob/6e0912f97994f927632b34ae9e63b53d6516a6ac/net/tlsdial/tlsdial.go#L329-L365","documentation":"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.","triggerScenarios":"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.","commonSituations":"Pins not maintained across 90-day LetsEncrypt renewals; air-gapped devices with rarely rotated certs but a stale pin table.","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"],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":"// before relying on a pin, check the served cert is still time-valid\nif time.Now().After(leaf.NotAfter) {\n\t// pin is for an expired cert; refresh it before the dial fails\n}","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"cert expired\") {\n\t// deterministic, not transient: refresh the pin; retrying will not help\n\treturn err\n}","preventionTips":["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"],"tags":["tls","certificate","certificate-pinning","security","go"],"backgroundTag":"certificate-expired","analyzedSha":"6e0912f97994f927632b34ae9e63b53d6516a6ac","analyzedAt":"2026-08-18T08:17:25.280Z","contentChangedAt":"2026-08-18T08:17:25.280Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}