From dac97aac9eb2de7c246692c340d82328f5c72b51 Mon Sep 17 00:00:00 2001 From: Saswata Mukherjee Date: Wed, 16 Sep 2026 11:06:39 +0100 Subject: [PATCH] tls: Add client CA configuration for mTLS verification Add --tls.client-ca-file flag and configure tls.Config.ClientCAs to enable proper verification of client certificates. Without this, the gateway cannot verify client certs against a specific CA. Note, this is for mTLS between sender and Observatorium API Signed-off-by: Saswata Mukherjee --- main.go | 6 ++++++ tls/config.go | 31 ++++++++++++++++++++++++++++++- tls/config_test.go | 1 + 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 217ff0c4e..f760b73ad 100644 --- a/main.go +++ b/main.go @@ -131,6 +131,7 @@ type tlsConfig struct { cipherSuites []string curvePreferences []string clientAuthType string + clientCAFile string reloadInterval time.Duration serverCertFile string @@ -894,6 +895,7 @@ func main() { cfg.tls.minVersion, cfg.tls.maxVersion, cfg.tls.clientAuthType, + cfg.tls.clientCAFile, cfg.tls.cipherSuites, cfg.tls.curvePreferences, ) @@ -1005,6 +1007,7 @@ func main() { cfg.tls.minVersion, cfg.tls.maxVersion, cfg.tls.clientAuthType, + cfg.tls.clientCAFile, cfg.tls.cipherSuites, cfg.tls.curvePreferences, ) @@ -1289,6 +1292,8 @@ func parseFlags() (config, error) { " The list is a filter of allowed groups; crypto/tls chooses the preference order.") flag.StringVar(&cfg.tls.clientAuthType, "tls.client-auth-type", "RequestClientCert", "Policy for TLS client-side authentication. Values are from ClientAuthType constants in https://pkg.go.dev/crypto/tls#ClientAuthType") + flag.StringVar(&cfg.tls.clientCAFile, "tls.client-ca-file", "", + "File containing the CA certificate for verifying client certificates. Required when using RequireAndVerifyClientCert or VerifyClientCertIfGiven.") flag.DurationVar(&cfg.tls.reloadInterval, "tls.reload-interval", time.Minute, "The interval at which to watch for TLS certificate changes.") flag.StringVar(&cfg.middleware.grpcRateLimiterAddress, "middleware.rate-limiter.grpc-address", "", @@ -1630,6 +1635,7 @@ func newGRPCServer(cfg *config, tenantHeader string, tenantIDs map[string]string cfg.tls.minVersion, cfg.tls.maxVersion, cfg.tls.clientAuthType, + cfg.tls.clientCAFile, cfg.tls.cipherSuites, cfg.tls.curvePreferences, ) diff --git a/tls/config.go b/tls/config.go index a0d393107..8657fcf8e 100644 --- a/tls/config.go +++ b/tls/config.go @@ -2,7 +2,9 @@ package tls import ( "crypto/tls" + "crypto/x509" "fmt" + "os" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -27,7 +29,7 @@ var curveIDs = map[string]tls.CurveID{ } // NewServerConfig provides new server TLS configuration. -func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersion, clientAuthType string, cipherSuites, curvePreferences []string) (*tls.Config, error) { +func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersion, clientAuthType, clientCAFile string, cipherSuites, curvePreferences []string) (*tls.Config, error) { if certFile == "" && keyFile == "" { level.Info(logger).Log("msg", "TLS disabled; key and cert must be set to enable") @@ -70,6 +72,32 @@ func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersio return nil, fmt.Errorf("can not parse TLS Client authentication policy: %w", err) } + // Load client CA certificate pool for verifying client certificates + var clientCAs *x509.CertPool + if clientCAFile != "" { + // Only load client CA if client authentication requires verification + if tlsClientAuthType == tls.RequireAndVerifyClientCert || tlsClientAuthType == tls.VerifyClientCertIfGiven { + caCert, err := os.ReadFile(clientCAFile) + if err != nil { + return nil, fmt.Errorf("failed to read client CA file: %w", err) + } + + clientCAs = x509.NewCertPool() + if !clientCAs.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("failed to parse client CA certificate from %s", clientCAFile) + } + + level.Info(logger).Log("msg", "loaded client CA certificate for mTLS verification", "file", clientCAFile) + } else { + level.Warn(logger).Log("msg", "client CA file provided but client auth type does not require verification", "clientAuthType", clientAuthType) + } + } else if tlsClientAuthType == tls.RequireAndVerifyClientCert || tlsClientAuthType == tls.VerifyClientCertIfGiven { + level.Warn(logger).Log( + "msg", "client authentication requires verification but no client CA file provided, will use system CA pool", + "clientAuthType", clientAuthType, + ) + } + tlsCfg := &tls.Config{ Certificates: []tls.Certificate{tlsCert}, // A list of supported cipher suites for TLS versions up to TLS 1.2. @@ -78,6 +106,7 @@ func NewServerConfig(logger log.Logger, certFile, keyFile, minVersion, maxVersio CipherSuites: cipherSuiteIDs, // If CurvePreferences is nil, a default list of secure curves is used. CurvePreferences: curvePreferenceIDs, + ClientCAs: clientCAs, ClientAuth: tlsClientAuthType, MinVersion: tlsMinVersion, MaxVersion: tlsMaxVersion, diff --git a/tls/config_test.go b/tls/config_test.go index 88df557af..a4942a7cd 100644 --- a/tls/config_test.go +++ b/tls/config_test.go @@ -129,6 +129,7 @@ func TestNewServerConfigCurvePreferences(t *testing.T) { "VersionTLS13", "VersionTLS13", "RequestClientCert", + "", nil, tc.curves, )