diff --git a/pkg/apis/options/options.go b/pkg/apis/options/options.go index b57d5aed..2ca3a766 100644 --- a/pkg/apis/options/options.go +++ b/pkg/apis/options/options.go @@ -154,6 +154,8 @@ func NewFlagSet() *pflag.FlagSet { flagSet.String("redis-sentinel-password", "", "Redis sentinel password. Used only for sentinel connection; any redis node passwords need to use `--redis-password`") flagSet.String("redis-sentinel-master-name", "", "Redis sentinel master name. Used in conjunction with --redis-use-sentinel") flagSet.String("redis-ca-path", "", "Redis custom CA path") + flagSet.String("redis-client-cert-path", "", "Path to PEM-encoded client certificate for mutual TLS when connecting to Redis") + flagSet.String("redis-client-key-path", "", "Path to PEM-encoded client private key for mutual TLS when connecting to Redis") flagSet.Bool("redis-insecure-skip-tls-verify", false, "Use insecure TLS connection to redis") flagSet.StringSlice("redis-sentinel-connection-urls", []string{}, "List of Redis sentinel connection URLs (eg redis://[USER[:PASSWORD]@]HOST[:PORT]). Used in conjunction with --redis-use-sentinel") flagSet.Bool("redis-use-cluster", false, "Connect to redis cluster. Must set --redis-cluster-connection-urls to use this feature") diff --git a/pkg/apis/options/sessions.go b/pkg/apis/options/sessions.go index c90c0ac2..ee49f06f 100644 --- a/pkg/apis/options/sessions.go +++ b/pkg/apis/options/sessions.go @@ -32,6 +32,8 @@ type RedisStoreOptions struct { UseCluster bool `flag:"redis-use-cluster" cfg:"redis_use_cluster"` ClusterConnectionURLs []string `flag:"redis-cluster-connection-urls" cfg:"redis_cluster_connection_urls"` CAPath string `flag:"redis-ca-path" cfg:"redis_ca_path"` + ClientCertPath string `flag:"redis-client-cert-path" cfg:"redis_client_cert_path"` + ClientKeyPath string `flag:"redis-client-key-path" cfg:"redis_client_key_path"` InsecureSkipTLSVerify bool `flag:"redis-insecure-skip-tls-verify" cfg:"redis_insecure_skip_tls_verify"` IdleTimeout int `flag:"redis-connection-idle-timeout" cfg:"redis_connection_idle_timeout"` } diff --git a/pkg/sessions/redis/redis_store.go b/pkg/sessions/redis/redis_store.go index 79f8f7d1..a102ac56 100644 --- a/pkg/sessions/redis/redis_store.go +++ b/pkg/sessions/redis/redis_store.go @@ -222,6 +222,21 @@ func setupTLSConfig(opts options.RedisStoreOptions, opt *redis.Options) error { opt.TLSConfig.RootCAs = rootCAs } + + if opts.ClientCertPath != "" || opts.ClientKeyPath != "" { + if opts.ClientCertPath == "" || opts.ClientKeyPath == "" { + return fmt.Errorf("redis client TLS: client certificate path and client private key path must both be set") + } + clientCert, err := tls.LoadX509KeyPair(opts.ClientCertPath, opts.ClientKeyPath) + if err != nil { + return fmt.Errorf("failed to load redis client certificate/key pair: %w", err) + } + if opt.TLSConfig == nil { + /* #nosec */ + opt.TLSConfig = &tls.Config{} + } + opt.TLSConfig.Certificates = []tls.Certificate{clientCert} + } return nil } diff --git a/pkg/sessions/redis/redis_store_test.go b/pkg/sessions/redis/redis_store_test.go index 18dbe934..cac01411 100644 --- a/pkg/sessions/redis/redis_store_test.go +++ b/pkg/sessions/redis/redis_store_test.go @@ -321,4 +321,23 @@ var _ = Describe("Redis SessionStore Tests", func() { Expect(opts).To(BeNil()) }) }) + + Describe("Redis TLS client credentials", func() { + It("returns an error when only client certificate path is set", func() { + _, err := buildStandaloneClient(options.RedisStoreOptions{ + ConnectionURL: "redis://localhost:6379", + ClientCertPath: "/some/path/cert.pem", + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must both be set")) + }) + It("returns an error when only client private key path is set", func() { + _, err := buildStandaloneClient(options.RedisStoreOptions{ + ConnectionURL: "redis://localhost:6379", + ClientKeyPath: "/some/path/key.pem", + }) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("must both be set")) + }) + }) }) diff --git a/pkg/sessions/redis/redis_store_tls_test.go b/pkg/sessions/redis/redis_store_tls_test.go index 54d94b1f..bdb80df9 100644 --- a/pkg/sessions/redis/redis_store_tls_test.go +++ b/pkg/sessions/redis/redis_store_tls_test.go @@ -147,6 +147,62 @@ var _ = Describe("Redis SessionStore Tests", func() { ) }) + Context("with mutual TLS (client certificate)", func() { + BeforeEach(func() { + mr.Close() + // Require a client certificate on the wire; RequireAndVerifyClientCert would + // reject the test suite cert (ExtKeyUsageServerAuth only, no clientAuth EKU). + var err error + mr, err = miniredis.RunTLS(&tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAnyClientCert, + }) + Expect(err).ToNot(HaveOccurred()) + }) + + Context("with standalone", func() { + tests.RunSessionStoreTests( + func(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessionsapi.SessionStore, error) { + opts.Type = options.RedisSessionStoreType + opts.Redis.ConnectionURL = redissProtocol + mr.Addr() + opts.Redis.CAPath = caPath + opts.Redis.ClientCertPath = caPath + opts.Redis.ClientKeyPath = keyPath + + var err error + ss, err = NewRedisSessionStore(opts, cookieOpts) + return ss, err + }, + func(d time.Duration) error { + mr.FastForward(d) + return nil + }, + ) + }) + + Context("with cluster", func() { + tests.RunSessionStoreTests( + func(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessionsapi.SessionStore, error) { + clusterAddr := redissProtocol + mr.Addr() + opts.Type = options.RedisSessionStoreType + opts.Redis.ClusterConnectionURLs = []string{clusterAddr} + opts.Redis.UseCluster = true + opts.Redis.CAPath = caPath + opts.Redis.ClientCertPath = caPath + opts.Redis.ClientKeyPath = keyPath + + var err error + ss, err = NewRedisSessionStore(opts, cookieOpts) + return ss, err + }, + func(d time.Duration) error { + mr.FastForward(d) + return nil + }, + ) + }) + }) + Context("with insecure TLS connection", func() { tests.RunSessionStoreTests( func(opts *options.SessionOptions, cookieOpts *options.Cookie) (sessionsapi.SessionStore, error) { diff --git a/pkg/sessions/redis/redis_test.go b/pkg/sessions/redis/redis_test.go index c38553cb..df81349d 100644 --- a/pkg/sessions/redis/redis_test.go +++ b/pkg/sessions/redis/redis_test.go @@ -27,8 +27,9 @@ func (l *wrappedRedisLogger) Printf(_ context.Context, format string, v ...inter } var ( - cert tls.Certificate - caPath string + cert tls.Certificate + caPath string + keyPath string ) func TestRedis(t *testing.T) { @@ -61,8 +62,16 @@ var _ = BeforeSuite(func() { _, err = certFile.Write(certData) defer certFile.Close() Expect(err).ToNot(HaveOccurred()) + + keyFile, err := os.CreateTemp("", "key.*.pem") + Expect(err).ToNot(HaveOccurred()) + keyPath = keyFile.Name() + _, err = keyFile.Write(keyOut.Bytes()) + defer keyFile.Close() + Expect(err).ToNot(HaveOccurred()) }) var _ = AfterSuite(func() { Expect(os.Remove(caPath)).ToNot(HaveOccurred()) + Expect(os.Remove(keyPath)).ToNot(HaveOccurred()) })