From 0c520a30cff3bffeacb42c20616b24229675ed2b Mon Sep 17 00:00:00 2001 From: Patrik Lundin Date: Thu, 22 Jan 2026 01:10:06 +0100 Subject: [PATCH] Reload TLS certificates on SIGHUP * Add utils.CertStorage for holding cert data that can be updated at runtime. * Add utils.NewTLSListener() to have a central place to control e.g. TLS MinVersion across different servers. * Add WithTLS() to webserver code so it looks more like the other servers. Fixes #1299 --- cmd/versitygw/main.go | 51 ++++++++++++++++++++++++++++++++++--------- s3api/admin-server.go | 18 +++++++++------ s3api/server.go | 16 +++++++++----- s3api/server_test.go | 12 +++++----- s3api/utils/utils.go | 38 ++++++++++++++++++++++++++++++++ webui/webserver.go | 24 ++++++++++++++------ 6 files changed, 123 insertions(+), 36 deletions(-) diff --git a/cmd/versitygw/main.go b/cmd/versitygw/main.go index c3ba045b..ab1909c1 100644 --- a/cmd/versitygw/main.go +++ b/cmd/versitygw/main.go @@ -16,7 +16,6 @@ package main import ( "context" - "crypto/tls" "fmt" "log" "net" @@ -733,11 +732,12 @@ func runGateway(ctx context.Context, be backend.Backend) error { return fmt.Errorf("TLS cert specified without key file") } - cert, err := tls.LoadX509KeyPair(certFile, keyFile) + cs := utils.NewCertStorage() + err := cs.SetCertificate(certFile, keyFile) if err != nil { return fmt.Errorf("tls: load certs: %v", err) } - opts = append(opts, s3api.WithTLS(cert)) + opts = append(opts, s3api.WithTLS(cs)) } if admPort == "" { opts = append(opts, s3api.WithAdminServer()) @@ -873,11 +873,12 @@ func runGateway(ctx context.Context, be backend.Backend) error { return fmt.Errorf("TLS cert specified without key file") } - cert, err := tls.LoadX509KeyPair(admCertFile, admKeyFile) + cs := utils.NewCertStorage() + err = cs.SetCertificate(admCertFile, admKeyFile) if err != nil { return fmt.Errorf("tls: load certs: %v", err) } - opts = append(opts, s3api.WithAdminSrvTLS(cert)) + opts = append(opts, s3api.WithAdminSrvTLS(cs)) } if quiet { opts = append(opts, s3api.WithAdminQuiet()) @@ -891,6 +892,8 @@ func runGateway(ctx context.Context, be backend.Backend) error { var webSrv *webui.Server webuiSSLEnabled := false + webTLSCert := "" + webTLSKey := "" if webuiAddr != "" { _, webPrt, err := net.SplitHostPort(webuiAddr) if err != nil { @@ -904,8 +907,7 @@ func runGateway(ctx context.Context, be backend.Backend) error { return fmt.Errorf("webui port must be between 0 and 65535") } - webTLSCert := "" - webTLSKey := "" + var webOpts []webui.Option if !webuiNoTLS { // WebUI can either use explicitly provided TLS files or reuse the // gateway's TLS files by default. @@ -923,6 +925,14 @@ func runGateway(ctx context.Context, be backend.Backend) error { return fmt.Errorf("webui TLS cert specified without key file") } webuiSSLEnabled = true + + cs := utils.NewCertStorage() + err := cs.SetCertificate(webTLSCert, webTLSKey) + if err != nil { + return fmt.Errorf("tls: load certs: %v", err) + } + + webOpts = append(webOpts, webui.WithTLS(cs)) } } @@ -945,7 +955,6 @@ func runGateway(ctx context.Context, be backend.Backend) error { } } - var webOpts []webui.Option if quiet { webOpts = append(webOpts, webui.WithQuiet()) } @@ -955,8 +964,6 @@ func runGateway(ctx context.Context, be backend.Backend) error { Gateways: gateways, AdminGateways: adminGateways, Region: region, - TLSCert: webTLSCert, - TLSKey: webTLSKey, }, webOpts...) } @@ -1003,6 +1010,30 @@ Loop: break Loop } } + if certFile != "" && keyFile != "" { + err = srv.CertStorage.SetCertificate(certFile, keyFile) + if err != nil { + debuglogger.InernalError(fmt.Errorf("srv cert reload failed: %w", err)) + } else { + fmt.Printf("srv cert reloaded (cert: %s, key: %s)\n", certFile, keyFile) + } + } + if admPort != "" && admCertFile != "" && admKeyFile != "" { + err = admSrv.CertStorage.SetCertificate(admCertFile, admKeyFile) + if err != nil { + debuglogger.InernalError(fmt.Errorf("admSrv cert reload failed: %w", err)) + } else { + fmt.Printf("admSrv cert reloaded (cert: %s, key: %s)\n", admCertFile, admKeyFile) + } + } + if webSrv != nil && webTLSCert != "" && webTLSKey != "" { + err := webSrv.CertStorage.SetCertificate(webTLSCert, webTLSKey) + if err != nil { + debuglogger.InernalError(fmt.Errorf("webSrv cert reload failed: %w", err)) + } else { + fmt.Printf("webSrv cert reloaded (cert: %s, key: %s)\n", webTLSCert, webTLSKey) + } + } } } saveErr := err diff --git a/s3api/admin-server.go b/s3api/admin-server.go index 7efa1a12..e314e071 100644 --- a/s3api/admin-server.go +++ b/s3api/admin-server.go @@ -15,8 +15,6 @@ package s3api import ( - "crypto/tls" - "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" @@ -25,6 +23,7 @@ import ( "github.com/versity/versitygw/debuglogger" "github.com/versity/versitygw/s3api/controllers" "github.com/versity/versitygw/s3api/middlewares" + "github.com/versity/versitygw/s3api/utils" "github.com/versity/versitygw/s3log" ) @@ -33,7 +32,7 @@ type S3AdminServer struct { backend backend.Backend router *S3AdminRouter port string - cert *tls.Certificate + CertStorage *utils.CertStorage quiet bool debug bool corsAllowOrigin string @@ -88,8 +87,8 @@ func NewAdminServer(be backend.Backend, root middlewares.RootUserConfig, port, r type AdminOpt func(s *S3AdminServer) -func WithAdminSrvTLS(cert tls.Certificate) AdminOpt { - return func(s *S3AdminServer) { s.cert = &cert } +func WithAdminSrvTLS(cs *utils.CertStorage) AdminOpt { + return func(s *S3AdminServer) { s.CertStorage = cs } } // WithQuiet silences default logging output @@ -109,8 +108,13 @@ func WithAdminCORSAllowOrigin(origin string) AdminOpt { } func (sa *S3AdminServer) Serve() (err error) { - if sa.cert != nil { - return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert) + if sa.CertStorage != nil { + ln, err := utils.NewTLSListener(sa.app.Config().Network, sa.port, sa.CertStorage.GetCertificate) + if err != nil { + return err + } + + return sa.app.Listener(ln) } return sa.app.Listen(sa.port) } diff --git a/s3api/server.go b/s3api/server.go index 5eaee8d3..6e146b96 100644 --- a/s3api/server.go +++ b/s3api/server.go @@ -15,7 +15,6 @@ package s3api import ( - "crypto/tls" "errors" "net/http" "strings" @@ -45,7 +44,7 @@ type S3ApiServer struct { app *fiber.App backend backend.Backend port string - cert *tls.Certificate + CertStorage *utils.CertStorage quiet bool readonly bool keepAlive bool @@ -133,8 +132,8 @@ func New( type Option func(*S3ApiServer) // WithTLS sets TLS Credentials -func WithTLS(cert tls.Certificate) Option { - return func(s *S3ApiServer) { s.cert = &cert } +func WithTLS(cs *utils.CertStorage) Option { + return func(s *S3ApiServer) { s.CertStorage = cs } } // WithAdminServer runs admin endpoints with the gateway in the same network @@ -173,8 +172,13 @@ func WithCORSAllowOrigin(origin string) Option { } func (sa *S3ApiServer) Serve() (err error) { - if sa.cert != nil { - return sa.app.ListenTLSWithCertificate(sa.port, *sa.cert) + if sa.CertStorage != nil { + ln, err := utils.NewTLSListener(sa.app.Config().Network, sa.port, sa.CertStorage.GetCertificate) + if err != nil { + return err + } + + return sa.app.Listener(ln) } return sa.app.Listen(sa.port) } diff --git a/s3api/server_test.go b/s3api/server_test.go index f50eb268..1b21eae5 100644 --- a/s3api/server_test.go +++ b/s3api/server_test.go @@ -15,11 +15,11 @@ package s3api import ( - "crypto/tls" "testing" "github.com/gofiber/fiber/v2" "github.com/versity/versitygw/backend" + "github.com/versity/versitygw/s3api/utils" ) func TestS3ApiServer_Serve(t *testing.T) { @@ -42,11 +42,11 @@ func TestS3ApiServer_Serve(t *testing.T) { name: "Serve-invalid-address-with-certificate", wantErr: true, sa: &S3ApiServer{ - app: fiber.New(), - backend: backend.BackendUnsupported{}, - port: "Invalid address", - Router: &S3ApiRouter{}, - cert: &tls.Certificate{}, + app: fiber.New(), + backend: backend.BackendUnsupported{}, + port: "Invalid address", + Router: &S3ApiRouter{}, + CertStorage: &utils.CertStorage{}, }, }, } diff --git a/s3api/utils/utils.go b/s3api/utils/utils.go index a60730f5..dd95abcb 100644 --- a/s3api/utils/utils.go +++ b/s3api/utils/utils.go @@ -16,11 +16,13 @@ package utils import ( "bytes" + "crypto/tls" "encoding/base64" "encoding/xml" "errors" "fmt" "io" + "net" "net/http" "net/url" "regexp" @@ -910,3 +912,39 @@ func GenerateObjectLocation(ctx *fiber.Ctx, virtualDomain, bucket, object string obj, ) } + +type CertStorage struct { + cert atomic.Pointer[tls.Certificate] +} + +func NewCertStorage() *CertStorage { + return &CertStorage{} +} + +func (cs *CertStorage) GetCertificate(_ *tls.ClientHelloInfo) (*tls.Certificate, error) { + return cs.cert.Load(), nil +} + +func (cs *CertStorage) SetCertificate(certFile string, keyFile string) error { + cert, err := tls.LoadX509KeyPair(certFile, keyFile) + if err != nil { + return fmt.Errorf("unable to set certificate: %w", err) + } + + cs.cert.Store(&cert) + + return nil +} + +func NewTLSListener(network string, address string, getCertificateFunc func(*tls.ClientHelloInfo) (*tls.Certificate, error)) (net.Listener, error) { + config := &tls.Config{ + MinVersion: tls.VersionTLS12, + GetCertificate: getCertificateFunc, + } + + ln, err := net.Listen(network, address) + if err != nil { + return nil, err + } + return tls.NewListener(ln, config), nil +} diff --git a/webui/webserver.go b/webui/webserver.go index 2c822b65..aae23724 100644 --- a/webui/webserver.go +++ b/webui/webserver.go @@ -23,6 +23,7 @@ import ( "github.com/gofiber/fiber/v2/middleware/filesystem" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/recover" + "github.com/versity/versitygw/s3api/utils" ) // ServerConfig holds the server configuration @@ -31,16 +32,15 @@ type ServerConfig struct { Gateways []string // S3 API gateways AdminGateways []string // Admin API gateways (defaults to Gateways if empty) Region string - TLSCert string - TLSKey string CORSOrigin string } // Server is the main GUI server type Server struct { - app *fiber.App - config *ServerConfig - quiet bool + app *fiber.App + CertStorage *utils.CertStorage + config *ServerConfig + quiet bool } // Option sets various options for NewServer() @@ -51,6 +51,11 @@ func WithQuiet() Option { return func(s *Server) { s.quiet = true } } +// WithTLS sets TLS Credentials +func WithTLS(cs *utils.CertStorage) Option { + return func(s *Server) { s.CertStorage = cs } +} + // NewServer creates a new GUI server instance func NewServer(cfg *ServerConfig, opts ...Option) *Server { app := fiber.New(fiber.Config{ @@ -127,8 +132,13 @@ func (s *Server) Serve() error { } // Check if TLS is configured - if s.config.TLSCert != "" && s.config.TLSKey != "" { - return s.app.ListenTLS(addr, s.config.TLSCert, s.config.TLSKey) + if s.CertStorage != nil { + ln, err := utils.NewTLSListener(s.app.Config().Network, addr, s.CertStorage.GetCertificate) + if err != nil { + return err + } + + return s.app.Listener(ln) } return s.app.Listen(addr)