Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 311
F21 Avatar asked May 27 '16 00:05

F21


1 Answers

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.

like image 152
Michael Avatar answered Nov 14 '22 05:11

Michael