Google "golang check ssl certificate" and every example looks like this:
conn, _ := tls.Dial("tcp", "example.com:443",
&tls.Config{InsecureSkipVerify: true})
cert := conn.ConnectionState().PeerCertificates[0]
fmt.Println(cert.NotAfter) // 2026-03-16
53 days left. Great. Ship it.
But your monitor just became blind to 3 out of 4 certificate problems.
The InsecureSkipVerify trap
InsecureSkipVerify: true skips all certificate validation - expiry, trust chain, hostname match. The connection is still encrypted, but Go won't check if the cert is actually valid.
Why do examples use it? Without it, Go refuses to connect if anything is wrong. You get an error instead of certificate details. For a quick script, that's inconvenient.
For monitoring, it's dangerous:
| Problem | What users see | What your monitor shows |
|---|---|---|
| Self-signed cert | Browser warning | "53 days left" |
| Wrong domain | Browser warning | "53 days left" |
| Untrusted CA | Browser warning | "53 days left" |
| Expired cert | Browser warning | "Expired" |
Three problems go undetected. Your monitor says everything is fine while users can't access your site.
The fix: two-step check
First, try a normal connection. If it succeeds, the certificate is valid. If it fails, make a second connection with InsecureSkipVerify to get the details anyway.
func checkCert(host string) CertStatus {
status := CertStatus{Host: host}
dialer := &net.Dialer{Timeout: 10 * time.Second}
// Step 1: Try normal connection (validates certificate)
conn, err := tls.DialWithDialer(dialer, "tcp", host+":443", nil)
if err == nil {
// Certificate is valid - get details
cert := conn.ConnectionState().PeerCertificates[0]
status.IsValid = true
status.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
conn.Close()
return status
}
// Step 2: Validation failed - get details anyway
status.ValidationError = err.Error()
conn, err = tls.DialWithDialer(dialer, "tcp", host+":443",
&tls.Config{InsecureSkipVerify: true})
if err != nil {
status.Error = err.Error()
return status
}
defer conn.Close()
cert := conn.ConnectionState().PeerCertificates[0]
status.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
return status
}
Now you see both: validity status AND expiry dates. A self-signed cert with 2 years left is still a problem your users will hit.
Complete monitor
package main
import (
"crypto/tls"
"fmt"
"net"
"time"
)
type CertStatus struct {
Host string
ExpiresAt time.Time
DaysLeft int
Issuer string
IsValid bool
ValidationError string
Error string
}
func checkCert(host string) CertStatus {
status := CertStatus{Host: host}
dialer := &net.Dialer{Timeout: 10 * time.Second}
// Step 1: Try normal connection (validates certificate)
conn, err := tls.DialWithDialer(dialer, "tcp", host+":443", nil)
if err == nil {
cert := conn.ConnectionState().PeerCertificates[0]
status.ExpiresAt = cert.NotAfter
status.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
status.Issuer = cert.Issuer.CommonName
status.IsValid = true
conn.Close()
return status
}
// Step 2: Validation failed - get details anyway
status.ValidationError = err.Error()
conn, err = tls.DialWithDialer(dialer, "tcp", host+":443",
&tls.Config{InsecureSkipVerify: true})
if err != nil {
status.Error = err.Error()
return status
}
defer conn.Close()
cert := conn.ConnectionState().PeerCertificates[0]
status.ExpiresAt = cert.NotAfter
status.DaysLeft = int(time.Until(cert.NotAfter).Hours() / 24)
status.Issuer = cert.Issuer.CommonName
return status
}
func main() {
domains := []string{
"github.com",
"google.com",
"expired.badssl.com",
"self-signed.badssl.com",
"wrong.host.badssl.com",
}
for _, domain := range domains {
status := checkCert(domain)
if status.Error != "" {
fmt.Printf("ERR %s: %s\n", domain, status.Error)
continue
}
if !status.IsValid {
fmt.Printf("FAIL %s: %s\n", domain, status.ValidationError)
fmt.Printf(" expires: %s\n", status.ExpiresAt.Format("2006-01-02"))
continue
}
switch {
case status.DaysLeft < 7:
fmt.Printf("CRIT %s: %d days left\n", domain, status.DaysLeft)
case status.DaysLeft < 30:
fmt.Printf("WARN %s: %d days left\n", domain, status.DaysLeft)
default:
fmt.Printf("OK %s: %d days left\n", domain, status.DaysLeft)
}
}
}
Output:
OK github.com: 73 days left
OK google.com: 40 days left
FAIL expired.badssl.com: tls: failed to verify certificate: x509: certificate has expired or is not yet valid: "*.badssl.com" certificate is expired
expires: 2015-04-12
FAIL self-signed.badssl.com: tls: failed to verify certificate: x509: certificate signed by unknown authority
expires: 2028-01-20
FAIL wrong.host.badssl.com: tls: failed to verify certificate: x509: certificate is valid for *.badssl.com, badssl.com, not wrong.host.badssl.com
expires: 2026-04-20
"But I have auto-renewal"
Let's Encrypt auto-renewal fails silently. DNS changed, server migrated, disk full, rate limits hit, nginx didn't reload after renewal. You won't know until users see browser warnings.
Always monitor even with auto-renewal.
Non-obvious gotchas
Wildcard doesn't cover what you think
*.example.com does NOT cover example.com (root) or api.staging.example.com (two levels deep). Monitor them separately.
Different certs behind load balancer
Three servers, each with its own certbot. One renewed, two didn't. 33% of users see errors. You can't reproduce it - you keep hitting the working server.
Monitor from multiple locations or run checks repeatedly.
This is how we monitor certificates at upsonar.io - but the code above works if you prefer to run your own.
How do you monitor your certificates?
Top comments (0)