Search code examples
gohttpswebserver

Is there a way to update the TLS certificates in a net/http server without any downtime?


I have a simple https server serving a simple page like so (no error handling for brevity):

package main

import (
    "crypto/tls"
    "fmt"
    "net/http"
)

func main() {
    mux := http.NewServeMux()

    mux.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) {
        fmt.Fprintf(w, "hello!")
    })

    xcert, _ := tls.LoadX509KeyPair("cert1.crt", "key1.pem")

    tlsConf := &tls.Config{
        Certificates: []tls.Certificate{xcert},
    }

    srv := &http.Server{
        Addr:      ":https",
        Handler:   mux,
        TLSConfig: tlsConf,
    }

    srv.ListenAndServeTLS("", "")
}

I want to use a Let's Encrypt TLS certificate to serve the content over https. I would like to be able to do certificate renewals and update the certificate in the server without any downtime.

I tried running a goroutine to update the tlsConf:

go func(c *tls.Config) {
        xcert, _ := tls.LoadX509KeyPair("cert2.crt", "key2.pem")

        select {
        case <-time.After(3 * time.Minute):
            c.Certificates = []tls.Certificate{xcert}
            c.BuildNameToCertificate()
            fmt.Println("cert switched!")
        }

    }(tlsConf)

However, that doesn't work because the server does not "read in" the changed config. Is there anyway to ask the server to reload the TLSConfig?


Solution

  • There is: you can use tls.Config’s GetCertificate member instead of populating Certificates. First, define a data structure that encapsulates the certificate and reload functionality (on receiving the SIGHUP signal in this example):

    type keypairReloader struct {
            certMu   sync.RWMutex
            cert     *tls.Certificate
            certPath string
            keyPath  string
    }
    
    func NewKeypairReloader(certPath, keyPath string) (*keypairReloader, error) { 
            result := &keypairReloader{
                    certPath: certPath,
                    keyPath:  keyPath,
            }
            cert, err := tls.LoadX509KeyPair(certPath, keyPath)
            if err != nil {
                    return nil, err
            }
            result.cert = &cert
            go func() {
                    c := make(chan os.Signal, 1)
                    signal.Notify(c, syscall.SIGHUP)
                    for range c {
                            log.Printf("Received SIGHUP, reloading TLS certificate and key from %q and %q", *tlsCertPath, *tlsKeyPath)
                            if err := result.maybeReload(); err != nil {
                                    log.Printf("Keeping old TLS certificate because the new one could not be loaded: %v", err)
                            }
                    }
            }()
            return result, nil
    }
    
    func (kpr *keypairReloader) maybeReload() error { 
            newCert, err := tls.LoadX509KeyPair(kpr.certPath, kpr.keyPath)
            if err != nil {
                    return err
            }
            kpr.certMu.Lock()
            defer kpr.certMu.Unlock()
            kpr.cert = &newCert
            return nil
    }
    
    func (kpr *keypairReloader) GetCertificateFunc() func(*tls.ClientHelloInfo) (*tls.Certificate, error) { 
            return func(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) {
                    kpr.certMu.RLock()
                    defer kpr.certMu.RUnlock()
                    return kpr.cert, nil
            }
    }
    

    Then, in your server code, use:

    kpr, err := NewKeypairReloader(*tlsCertPath, *tlsKeyPath)
    if err != nil {
        log.Fatal(err)
    }
    srv.TLSConfig.GetCertificate = kpr.GetCertificateFunc()
    

    I recently implemented this pattern in RobustIRC.