This commit is contained in:
Piotr Karatkevich 2026-04-25 02:25:43 +03:00 committed by GitHub
commit e1fdae644e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 105 additions and 2 deletions

View File

@ -156,6 +156,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")

View File

@ -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"`
}

View File

@ -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
}

View File

@ -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"))
})
})
})

View File

@ -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) {

View File

@ -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())
})