Acquiring TLS certificates at runtime
Disclosure; this blogpost is NOT written by AI.
Usually when you have a web server that you'd like to procure TLS certificates for, that procurement is done out of band using tools like Certbot or any other ACME client.
Dushed is a knowledge management web app that connects your notes, bookmarks, files, feeds, kindle highlights and flashcards in one distraction-free library.
When creating dushed, we did not want to have to maintain another out of band process for certificate renewals that would then necessitate having to restart the dushed process post-renewal.
The web app is written in golang and we managed to come up with a solution which registers and renews TLS certificates from LetsEncrypt at runtime. In golang, you setup a https server using something like;
func hello(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello world. Welcome to my website"))
}
srv := &http.Server{
Addr: "127.0.0.1:8080",
Handler: http.HandlerFunc(hello),
}
err := srv.ListenAndServeTLS("/path/to/cert", "/path/to/key")
This assumes the existence of TLS certificate and key at the given location.
But http server also has an optional TLSConfig field that can be used to provide the TLS configuration for the server.
tls.Config is a struct with a field(GetCertificate) that is a function type which returns a TLS certificate based on a given clientHello. We can take advantage of this to procure/renew TLS certificate inside that function.
tlsConf := &tls.Config{
GetCertificate: func(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
crt, err := getCert(info)
if err != nil {return nil, err}
return crt, nil
},
}
srv := &http.Server{
Addr: "127.0.0.1:8080",
Handler: http.HandlerFunc(hello),
TLSConfig: tlsConf,
}
// Use empty string arguments so that srv.TLSConfig is used.
err := srv.ListenAndServeTLS("", "")
The bulk of the implementation would then be in the getCert function. In there you can call the appropriate ACME protocol APIs. This also gives you an opportunity to do other fancy stuff like caching the certificates, serving different certificates based on SNI, renewing certificates preemptively before expiry, etc. For dushed we used the golang.org/x/crypto/acme/autocert client to accomplish that instead of writing our own ACME client.
This is how dushed procures and renews TLS certificates at runtime. It even works across zero downtime deployments. We use a ListenConfig that sets SO_REUSEPORT & SO_REUSEADDR and this ensures that both the old and new process can listen at the same time. The certificates we procure from LetsEncrypt in getCert are persisted in both disk and memory, so they are available to both processes during a deployment.