Over the past few months, I’ve been redesigning and writing StatusCake’s SSL monitoring feature from Node to Go. This blog post describes one of the more subtle challenges we came across to help you master it if you find yourself with it too!
Writing a Go client that fetches an SSL certificate isn’t a new problem. A common approach is to use a http.Client. This limits you to just certificates served over HTTPS, when technically anything running TLS can have a certificate. We decided to use the tls package instead.
conn, err := tls.Dial("tcp", url, &t.config)
if err != nil {
return err
}
defer conn.Close()
cs := conn.ConnectionState()
// First is the entity certificate
// Second is the intermediate certificate (signs the entity)
switch len(cs.PeerCertificates) {
case 0:
return errors.New("entity certificate not found")
case 1:
return errors.New("intermediate certificate not found")
}
fmt.Println("Entity: ", cs.PeerCertificates[0].Subject.CommonName)
fmt.Println("Intermediate: ", cs.PeerCertificates[1].Subject.CommonName)
Running this for url = "statuscake.com:443", we get:
Entity: *.statuscake.com
Intermediate: Sectigo RSA Domain Validation Secure Server CA
The important thing to note here is that we receive both the entity and intermediate certificate.
Testing, testing, testing
I needed my tests to be able to:
Spoof a server that spits out a certificate for each link in the SSL chain (entity and intermediate; in this case I didn’t care about the root)
// Get a tls.Certificate
serverCert, err := certsetup()
if err != nil {
panic(err)
}
// Set up the httptest.Server using our certificate signed by our CA
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "success!")}))
srv.TLS = &tls.Config{
Certificates: []tls.Certificate{serverCert},
}
srv.StartTLS()
defer srv.Close()
Easy. Let’s TLS dial as we did earlier — surely it will return both of these certificates, right?
err: intermediate certificate not found
Whaaaaaat?! Turns out our server didn’t serve two certificates like it would in the real world. The issue is the entity certificate is only signed by the CA; the server doesn’t actually return the CA’s certificate.
So let’s fix this. Straight away, you notice the TLS config’s certificate attribute only includes the one certificate — just add the CA certificate to it, right?
The naive (wrong) solution
// Get two tls.Certificate:
// - Entity (our server's subject)
// - Intermediate (the certificate for the CA that signs the entity)
entityCert, intermediateCert, err := certsetup()
if err != nil {
panic(err)
}
// Set up the httptest.Server using our certificate signed by our CA
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {fmt.Fprintln(w, "success!")}))
srv.TLS = &tls.Config{
Certificates: []tls.Certificate{entityCert, intermediateCert},
}
srv.StartTLS()
defer srv.Close()
Oh, how I wished it were this simple.
The world if this was the solution
You’d be forgiven for thinking the Certificates attribute is a slice of certificates to serve to a client. Spoiler: It’s not.
It’s actually a series of certificates (chains) to serve to the client; the first certificate compatible with the client’s requirements is used. So with our new ‘solution’, we’re still just serving the first certificate, since it meets the client’s requirements.
The (right) solution
Create a certificate chain as a tls.Certificate struct and use this in the Certificates slice.
NOTE: This isn’t necessary, but for completeness, I’ve added a root certificate to sign our intermediate. It’s a bit more realistic, as we’re not signing the intermediate certificate with itself.
Create our intermediate cert
We want to create a private and public key for the intermediate certificate, have it signed by the root CA and then PEM encode it.
// Create our private and public key for intermediateCA
interCAPrivKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil {
return tls.Certificate{}, err
}
// Create the intermediate CA certificate
caBytes, err := x509.CreateCertificate(rand.Reader, &cfg.intermediateCA, &cfg.rootCA, &interCAPrivKey.PublicKey, interCAPrivKey)
if err != nil {
return tls.Certificate{}, err
}
// PEM encode the certificate and private key
interCAPEM := new(bytes.Buffer)
pem.Encode(interCAPEM, &pem.Block{
Type: "CERTIFICATE",
Bytes: caBytes,
})
interCAPrivKeyPEM := new(bytes.Buffer)
pem.Encode(interCAPrivKeyPEM, &pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: x509.MarshalPKCS1PrivateKey(interCAPrivKey),
})
To serve two certificates, we need to append the two certificates together in one-byte slice, then create our tls.Certificate from this.
var cert []byte
// Concatenate the two certs so they're both served to the client
cert = append(certPEM.Bytes(), interCAPEM.Bytes()...)
serverCert, err := tls.X509KeyPair(cert, certPrivKeyPEM.Bytes())
if err != nil {
return tls.Certificate{}, err
}
And that’s it! Finally, create the TLS server config and pass it to a httptest server:
4min read On 13th August 2026, a storm knocked out cooling at RadiusDC’s Phoenix data centre, which hosted essential Namecheap operations. To protect hardware from thermal damage, Namecheap took services offline while temporary chillers were brought in. The incident and recovery ran for roughly 30 hours, with Namecheap’s own site unreachable for about 11 hours 42 minutes
3min read On Wednesday 19 August, Monzo had an outage. DownDetector logged more than 3,000 reports by midday. Monzo’s own statement was direct about what it did next: it activated Monzo Stand-in, its fully independent backup bank, while it investigated an issue affecting customers. By the end of the day, Monzo said the issue was resolved and
3min read GitHub’s incident on 17 August 2026 ran from 13:28 to 21:15 UTC, seven hours and forty-seven minutes. At peak, web and API traffic saw error rates of around 20%, while archive and raw-content downloads reached roughly 50%. SAML and OIDC authentication, SCIM and Team Sync were affected alongside Git operations, Actions, Pages, Issues, Pull Requests
3min read Adding a new website, launching a customer portal, or handing a service to a new team should be straightforward. Setting up monitoring is part of that job, but it is easy for a manual step to be missed when information is spread across several systems. StatusCake now integrates with viaSocket, giving teams a way to connect
7min read A website may be standing and still be in trouble. It may answer a request, return a cheerful 200 OK, and yet load slowly enough that visitors begin to lose patience. Its certificate may be nearing expiry. Its domain records may have changed. A server may be filling its disk in the background, patient and
6min read StatusCake tells you that something might be broken. Hermes can check whether it really looks broken, decide who should hear about it, send the email, and keep the record for tomorrow morning’s summary.
Daniel
May 13, 2026
Sign up for the StatusCake newsletter
Want to know how much website downtime costs, and the impact it can have on your business?
Find out everything you need to know in our new uptime monitoring whitepaper 2021