1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2026-10-29 05:32:31 +00:00

feat: reload TLS certificates when their files change (#625)

This commit is contained in:
Antoine Deschênes
2026-10-05 17:49:35 -04:00
committed by GitHub
parent 9876bb2bd9
commit 8075e8d2a3
6 changed files with 447 additions and 6 deletions
+3 -3
View File
@@ -9,6 +9,7 @@ import (
"time"
"github.com/inbucket/inbucket/v3/pkg/config"
"github.com/inbucket/inbucket/v3/pkg/server/tlscert"
"github.com/inbucket/inbucket/v3/pkg/storage"
"github.com/rs/zerolog/log"
)
@@ -28,14 +29,13 @@ func NewServer(pop3Config config.POP3, store storage.Store) (*Server, error) {
slog := log.With().Str("module", "pop3").Str("phase", "tls").Logger()
tlsConfig := &tls.Config{}
if pop3Config.TLSEnabled {
var err error
tlsConfig.Certificates = make([]tls.Certificate, 1)
tlsConfig.Certificates[0], err = tls.LoadX509KeyPair(pop3Config.TLSCert, pop3Config.TLSPrivKey)
reloader, err := tlscert.NewReloader(pop3Config.TLSCert, pop3Config.TLSPrivKey, slog)
if err != nil {
slog.Error().Msgf("Failed loading X509 KeyPair: %v", err)
return nil, fmt.Errorf("failed to configure TLS; %v", err)
// Do not silently turn off Security.
}
tlsConfig.GetCertificate = reloader.GetCertificate
slog.Debug().Msg("TLS config available")
} else {
tlsConfig = nil
+63
View File
@@ -3,12 +3,15 @@ package pop3
import (
"bufio"
"context"
"crypto/tls"
"errors"
"fmt"
"io"
"net"
"net/mail"
"net/textproto"
"os"
"path/filepath"
"slices"
"strings"
"testing"
@@ -765,6 +768,66 @@ func TestNewServerCertificateError(t *testing.T) {
require.Error(t, err)
}
// TestForceTLSServesRotatedCertificate verifies that replacing the certificate files on disk
// changes the certificate presented by later full handshakes, without restarting the server,
// while a client resuming an earlier TLS session keeps that session's certificate.
func TestForceTLSServesRotatedCertificate(t *testing.T) {
dir := t.TempDir()
certPath, keyPath := filepath.Join(dir, "cert.pem"), filepath.Join(dir, "key.pem")
writeKeyPair := func(version int) []byte {
cert, privKey, err := generateCertificate(t)
require.NoError(t, err)
mtime := time.Date(2026, 1, 1, 0, 0, version, 0, time.UTC)
require.NoError(t, os.WriteFile(certPath, certToPem(cert), 0o600))
require.NoError(t, os.WriteFile(keyPath, privKeyToPem(privKey), 0o600))
require.NoError(t, os.Chtimes(certPath, mtime, mtime))
require.NoError(t, os.Chtimes(keyPath, mtime, mtime))
return cert
}
first := writeKeyPair(1)
server, err := NewServer(config.POP3{
Addr: "127.0.0.1:0",
Domain: "inbucket.local",
Timeout: 5 * time.Second,
ForceTLS: true,
TLSEnabled: true,
TLSCert: certPath,
TLSPrivKey: keyPath,
}, test.NewStore())
require.NoError(t, err)
t.Cleanup(server.Drain)
// peerCertificate starts a session and returns the certificate the server presents, and
// whether the TLS session was resumed.
peerCertificate := func(tlsConfig *tls.Config) ([]byte, bool) {
tlsConn := tls.Client(setupPOPSession(t, server), tlsConfig)
defer func() { _ = tlsConn.Close() }()
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
require.NoError(t, tlsConn.HandshakeContext(ctx))
// Read the greeting so the server is waiting on input when the client closes. Reading
// also processes the session ticket the server sends after the handshake.
readPOP3Greeting(t, textproto.NewConn(tlsConn))
state := tlsConn.ConnectionState()
return state.PeerCertificates[0].Raw, state.DidResume
}
fresh := &tls.Config{InsecureSkipVerify: true}
caching := &tls.Config{InsecureSkipVerify: true, ClientSessionCache: tls.NewLRUClientSessionCache(1)}
cert, _ := peerCertificate(fresh)
assert.Equal(t, first, cert)
cert, _ = peerCertificate(caching)
assert.Equal(t, first, cert)
second := writeKeyPair(2)
cert, _ = peerCertificate(fresh)
assert.Equal(t, second, cert, "full handshake after rotation")
cert, resumed := peerCertificate(caching)
assert.True(t, resumed, "caching client should resume its session")
assert.Equal(t, first, cert, "resumed session after rotation")
}
// TestValidateMsgNum exercises the shared message-number validation helper
// directly.
func TestValidateMsgNum(t *testing.T) {