Skip to content

Recarregar certificado TLS dinamicamente em Go

Posted on:5 de outubro de 2023 at 01:07

Renovando certificado TLS dinamicamente em Go

Introdução

As vezes não temos uma camada de infraestrutura para lidar com certificados TLS e todas as outras questões que precisam para que possamos expor uma API HTTPS.

Quando temos essa situação, cabe a aplicação ficar responsável por carregar o arquivo de certificado.

E então nos perguntamos: quando esse certificado expirar, como faço para recarregar um novo certificado sem a necessidade de reiniciar minha aplicação?

O foco desse artigo é demonstrar em código o recarregamento dinâmico do certificado na API, não irei abordar estratégias de renovação do certificado em si.

O problema

É comum quando vamos pesquisar em como criar uma API HTTPS em Go, nos depararmos com um código semelhante a esse:

package main

import (
	"fmt"
	"log"
	"net/http"
)

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

func main() {
	http.HandleFunc("/hello", hello)
	log.Fatal(http.ListenAndServeTLS(":443", "server.crt", "server.key", nil))
}

De certa forma esse código funciona, mas essa linha vai nos dar uma dor de cabeça no futuro:

http.ListenAndServeTLS(":443", "server.crt", "server.key", nil)

Dessa maneira estamos dizendo para carregar esses arquivos durante a inicialização do serviço e a única forma para recarregar esse certificado, será reiniciando a aplicação.

No resumo, quando o certificado expirar, teremos que reiniciar a aplicação para carregar o certificado novo.

Carregando o certificado dinamicamente

Uma solução para esse problema envolve o carregamento dinâmico do certificado TLS e conseguimos fazer isso através do callback GetCertificate, definido dentro de tls.Config e que podemos fazer da seguinte forma:

package main

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

func main() {
	m := http.NewServeMux()
	m.HandleFunc("/hello", hello)

	tlsConfig := &tls.Config{
		GetCertificate: getCert,
	}

	srv := http.Server{
		Addr:      ":9443",
		Handler:   m,
		TLSConfig: tlsConfig,
	}

	srv.ListenAndServeTLS("", "")
}

func getCert(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
	cer, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key")
	if err != nil {
		return nil, err
	}

	return &cer, nil
}

func hello(w http.ResponseWriter, r *http.Request) {
	fmt.Fprint(w, "hello")
}

Dessa forma a nossa função getCert será invocada sempre que for necessário carregar o certificado, se houver mudanças nesse arquivo, a próxima requisição irá recarregá-lo.

Recarregando o certificado apenas se o atual estiver expirado

No exemplo anterior, vimos como recarregar o certificado sem a necessidade de reiniciar a aplicação, mas estamos fazendo isso para todas as requisições, podemos evitar esse trabalho se validarmos se o certificado expirou.

Um exemplo bem simples de como podemos fazer isso:

var (
	certMu     sync.Mutex
	currentCer *tls.Certificate
	certExpiry time.Time
)

func getCert(info *tls.ClientHelloInfo) (*tls.Certificate, error) {
	certMu.Lock()
	defer certMu.Unlock()

	// Verifique se o certificado atual já expirou
	if time.Now().After(certExpiry) {
		// O certificado atual expirou, então carregue um novo
		cer, err := tls.LoadX509KeyPair("localhost.crt", "localhost.key")
		if err != nil {
			return nil, err
		}

		// Atualize o certificado atual e a data de expiração
		currentCer = &cer
		certExpiry = cer.Leaf.NotAfter
	}

	return currentCer, nil
}

Também poderíamos implementar alguma lógica de renovação do certificado, no exemplo de cima, estamos considerando que existe uma rotina que renova o certificado e substitui os arquivos antigos.