diff --git a/hack/delete-redis-retry-keys/README.md b/hack/delete-redis-retry-keys/README.md index 984f2a8b31..4b39fd3f48 100644 --- a/hack/delete-redis-retry-keys/README.md +++ b/hack/delete-redis-retry-keys/README.md @@ -10,12 +10,29 @@ The tool deletes keys matching the pattern `{environment-id}:goal_event_retry:*` go run ./hack/delete-redis-retry-keys delete \ --redis-addr= \ --environment-id= \ - --redis-password= \ # optional - --scan-count= \ # optional, defaults to 100 + --redis-password= \ + --scan-count= \ + --redis-tls-enabled \ + --redis-tls-ca-cert= \ + --redis-tls-cert= \ + --redis-tls-key= \ + --redis-tls-insecure-skip-verify \ --no-profile \ --no-gcp-trace-enabled ``` +Optional flags: + +| Flag | Description | +| --- | --- | +| `--redis-password` | Redis password. | +| `--scan-count` | Number of keys to scan per iteration. Defaults to 100. | +| `--redis-tls-enabled` | Connect over TLS. Defaults to false. | +| `--redis-tls-ca-cert` | CA certificate path. Uses the system CA pool if unset. | +| `--redis-tls-cert` | Client certificate path, for mutual TLS. | +| `--redis-tls-key` | Client private key path, for mutual TLS. | +| `--redis-tls-insecure-skip-verify` | Skip server certificate verification. Not recommended for production. | + ### Example: Delete retry keys for e2e environment (minikube) ```bash diff --git a/hack/delete-redis-retry-keys/command.go b/hack/delete-redis-retry-keys/command.go index 8207e83236..1b1c13fe0a 100644 --- a/hack/delete-redis-retry-keys/command.go +++ b/hack/delete-redis-retry-keys/command.go @@ -36,6 +36,12 @@ type command struct { redisPassword *string environmentID *string scanCount *int64 + + redisTLSEnabled *bool + redisTLSCACert *string + redisTLSCert *string + redisTLSKey *string + redisTLSInsecureSkipVerify *bool } func registerCommand(r cli.CommandRegistry, p cli.ParentCommand) *command { @@ -46,6 +52,26 @@ func registerCommand(r cli.CommandRegistry, p cli.ParentCommand) *command { redisPassword: cmd.Flag("redis-password", "Redis password.").Default("").String(), environmentID: cmd.Flag("environment-id", "Environment ID to delete retry keys for.").Required().String(), scanCount: cmd.Flag("scan-count", "Number of keys to scan per iteration.").Default("100").Int64(), + redisTLSEnabled: cmd.Flag( + "redis-tls-enabled", + "Enable TLS when connecting to the Redis server.", + ).Default("false").Bool(), + redisTLSCACert: cmd.Flag( + "redis-tls-ca-cert", + "Path to the Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + redisTLSCert: cmd.Flag( + "redis-tls-cert", + "Path to the Redis TLS client certificate file (for mutual TLS).", + ).String(), + redisTLSKey: cmd.Flag( + "redis-tls-key", + "Path to the Redis TLS client private key file (for mutual TLS).", + ).String(), + redisTLSInsecureSkipVerify: cmd.Flag( + "redis-tls-insecure-skip-verify", + "Skip Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), } r.RegisterCommand(command) return command @@ -63,6 +89,13 @@ func (c *command) Run(ctx context.Context, metrics metrics.Metrics, logger *zap. opts := []redisv3.Option{ redisv3.WithLogger(logger), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *c.redisTLSEnabled, + CACert: *c.redisTLSCACert, + Cert: *c.redisTLSCert, + Key: *c.redisTLSKey, + InsecureSkipVerify: *c.redisTLSInsecureSkipVerify, + }), } if *c.redisPassword != "" { opts = append(opts, redisv3.WithPassword(*c.redisPassword)) diff --git a/hack/redis-data-copy/command.go b/hack/redis-data-copy/command.go index 71add59a24..93f7161146 100644 --- a/hack/redis-data-copy/command.go +++ b/hack/redis-data-copy/command.go @@ -35,6 +35,18 @@ type command struct { srcPassword *string destPassword *string overrideDestKey *bool + + srcTLSEnabled *bool + srcTLSCACert *string + srcTLSCert *string + srcTLSKey *string + srcTLSInsecureSkipVerify *bool + + destTLSEnabled *bool + destTLSCACert *string + destTLSCert *string + destTLSKey *string + destTLSInsecureSkipVerify *bool } func registerCommand(r cli.CommandRegistry, p cli.ParentCommand) *command { @@ -48,6 +60,46 @@ func registerCommand(r cli.CommandRegistry, p cli.ParentCommand) *command { overrideDestKey: cmd.Flag("override-dest-key", "Override existing keys in the destination Redis"). Default("false"). Bool(), + srcTLSEnabled: cmd.Flag( + "src-tls-enabled", + "Enable TLS when connecting to the source Redis server.", + ).Default("false").Bool(), + srcTLSCACert: cmd.Flag( + "src-tls-ca-cert", + "Path to the source Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + srcTLSCert: cmd.Flag( + "src-tls-cert", + "Path to the source Redis TLS client certificate file (for mutual TLS).", + ).String(), + srcTLSKey: cmd.Flag( + "src-tls-key", + "Path to the source Redis TLS client private key file (for mutual TLS).", + ).String(), + srcTLSInsecureSkipVerify: cmd.Flag( + "src-tls-insecure-skip-verify", + "Skip source Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), + destTLSEnabled: cmd.Flag( + "dest-tls-enabled", + "Enable TLS when connecting to the destination Redis server.", + ).Default("false").Bool(), + destTLSCACert: cmd.Flag( + "dest-tls-ca-cert", + "Path to the destination Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + destTLSCert: cmd.Flag( + "dest-tls-cert", + "Path to the destination Redis TLS client certificate file (for mutual TLS).", + ).String(), + destTLSKey: cmd.Flag( + "dest-tls-key", + "Path to the destination Redis TLS client private key file (for mutual TLS).", + ).String(), + destTLSInsecureSkipVerify: cmd.Flag( + "dest-tls-insecure-skip-verify", + "Skip destination Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), } r.RegisterCommand(command) return command @@ -61,6 +113,13 @@ func (c *command) Run(ctx context.Context, metrics metrics.Metrics, logger *zap. v3.WithMinIdleConns(5), v3.WithMaxRetries(3), v3.WithDialTimeout(10*time.Second), + v3.WithTLS(v3.TLSConfig{ + Enabled: *c.srcTLSEnabled, + CACert: *c.srcTLSCACert, + Cert: *c.srcTLSCert, + Key: *c.srcTLSKey, + InsecureSkipVerify: *c.srcTLSInsecureSkipVerify, + }), ) if err != nil { logger.Error("Error creating source Redis client", zap.Error(err)) @@ -75,6 +134,13 @@ func (c *command) Run(ctx context.Context, metrics metrics.Metrics, logger *zap. v3.WithMinIdleConns(5), v3.WithMaxRetries(3), v3.WithDialTimeout(10*time.Second), + v3.WithTLS(v3.TLSConfig{ + Enabled: *c.destTLSEnabled, + CACert: *c.destTLSCACert, + Cert: *c.destTLSCert, + Key: *c.destTLSKey, + InsecureSkipVerify: *c.destTLSInsecureSkipVerify, + }), ) if err != nil { logger.Error("Error creating destination Redis client", zap.Error(err)) diff --git a/manifests/bucketeer/charts/api/templates/deployment.yaml b/manifests/bucketeer/charts/api/templates/deployment.yaml index 4dc02f2e95..ec331cc90c 100644 --- a/manifests/bucketeer/charts/api/templates/deployment.yaml +++ b/manifests/bucketeer/charts/api/templates/deployment.yaml @@ -47,6 +47,11 @@ spec: secret: secretName: {{ .Values.global.operationalDatabase.postgres.sslSecretName }} {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + secret: + secretName: {{ .Values.global.pubsub.redis.tlsSecretName }} + {{- end }} {{- if .Values.serviceAccount.annotations }} serviceAccountName: {{ template "api.fullname" . }} {{- end }} @@ -114,6 +119,16 @@ spec: value: "{{ .Values.env.redis.poolMaxActive }}" - name: BUCKETEER_API_REDIS_MODE value: "{{ .Values.env.redis.mode }}" + - name: BUCKETEER_API_REDIS_TLS_ENABLED + value: "{{ .Values.env.redis.tlsEnabled }}" + - name: BUCKETEER_API_REDIS_TLS_CA_CERT + value: "{{ .Values.env.redis.tlsCACert }}" + - name: BUCKETEER_API_REDIS_TLS_CERT + value: "{{ .Values.env.redis.tlsCert }}" + - name: BUCKETEER_API_REDIS_TLS_KEY + value: "{{ .Values.env.redis.tlsKey }}" + - name: BUCKETEER_API_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.redis.tlsInsecureSkipVerify }}" - name: BUCKETEER_API_PUBSUB_REDIS_PARTITION_COUNT value: "{{ .Values.global.pubsub.redis.partitionCount }}" - name: BUCKETEER_API_OLDEST_EVENT_TIMESTAMP @@ -168,6 +183,16 @@ spec: value: "{{ .Values.global.pubsub.redis.minIdle }}" - name: BUCKETEER_API_PUBSUB_REDIS_MODE value: "{{ .Values.env.pubSubRedisMode }}" + - name: BUCKETEER_API_PUBSUB_REDIS_TLS_ENABLED + value: "{{ .Values.global.pubsub.redis.tlsEnabled }}" + - name: BUCKETEER_API_PUBSUB_REDIS_TLS_CA_CERT + value: "{{ .Values.global.pubsub.redis.tlsCACert }}" + - name: BUCKETEER_API_PUBSUB_REDIS_TLS_CERT + value: "{{ .Values.global.pubsub.redis.tlsCert }}" + - name: BUCKETEER_API_PUBSUB_REDIS_TLS_KEY + value: "{{ .Values.global.pubsub.redis.tlsKey }}" + - name: BUCKETEER_API_PUBSUB_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.global.pubsub.redis.tlsInsecureSkipVerify }}" {{- if .Values.env.featuresMemoryCacheTTL }} - name: BUCKETEER_API_FEATURES_MEMORY_CACHE_TTL value: "{{ .Values.env.featuresMemoryCacheTTL }}" @@ -222,6 +247,11 @@ spec: mountPath: /usr/local/certs/postgres readOnly: true {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + mountPath: /usr/local/certs/redis + readOnly: true + {{- end }} ports: - name: service containerPort: {{ .Values.env.port }} diff --git a/manifests/bucketeer/charts/api/values.yaml b/manifests/bucketeer/charts/api/values.yaml index b71892f713..de7012520d 100644 --- a/manifests/bucketeer/charts/api/values.yaml +++ b/manifests/bucketeer/charts/api/values.yaml @@ -45,6 +45,11 @@ env: poolMaxActive: 25 addr: mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false oldestEventTimestamp: furthestEventTimestamp: # Grace window applied to the diff filters for: diff --git a/manifests/bucketeer/charts/batch/templates/deployment.yaml b/manifests/bucketeer/charts/batch/templates/deployment.yaml index 0f9603dd79..0a6243876f 100644 --- a/manifests/bucketeer/charts/batch/templates/deployment.yaml +++ b/manifests/bucketeer/charts/batch/templates/deployment.yaml @@ -48,6 +48,11 @@ spec: secret: secretName: {{ .Values.global.operationalDatabase.postgres.sslSecretName }} {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + secret: + secretName: {{ .Values.global.pubsub.redis.tlsSecretName }} + {{- end }} - name: oauth-key-secret secret: secretName: {{ template "oauth-key-secret" . }} @@ -188,6 +193,16 @@ spec: value: "{{ .Values.env.persistentRedis.poolMaxActive }}" - name: BUCKETEER_BATCH_PERSISTENT_REDIS_MODE value: "{{ .Values.env.persistentRedis.mode }}" + - name: BUCKETEER_BATCH_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.persistentRedis.tlsEnabled }}" + - name: BUCKETEER_BATCH_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.persistentRedis.tlsCACert }}" + - name: BUCKETEER_BATCH_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.persistentRedis.tlsCert }}" + - name: BUCKETEER_BATCH_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.persistentRedis.tlsKey }}" + - name: BUCKETEER_BATCH_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.persistentRedis.tlsInsecureSkipVerify }}" - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_SERVER_NAME value: "{{ .Values.env.nonPersistentRedis.serverName }}" - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_ADDR @@ -198,6 +213,16 @@ spec: value: "{{ .Values.env.nonPersistentRedis.poolMaxActive }}" - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_MODE value: "{{ .Values.env.nonPersistentRedis.mode }}" + - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.nonPersistentRedis.tlsEnabled }}" + - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCACert }}" + - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCert }}" + - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.nonPersistentRedis.tlsKey }}" + - name: BUCKETEER_BATCH_NON_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.nonPersistentRedis.tlsInsecureSkipVerify }}" - name: BUCKETEER_BATCH_EXPERIMENT_LOCK_TTL value: "{{ .Values.env.experimentLockTTL }}" - name: BUCKETEER_BATCH_STAN_MODEL_ID @@ -223,6 +248,11 @@ spec: mountPath: /usr/local/certs/postgres readOnly: true {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + mountPath: /usr/local/certs/redis + readOnly: true + {{- end }} - name: oauth-key-secret mountPath: /usr/local/oauth-key readOnly: true diff --git a/manifests/bucketeer/charts/batch/values.yaml b/manifests/bucketeer/charts/batch/values.yaml index ecd91b7bcc..aa51c0d36f 100644 --- a/manifests/bucketeer/charts/batch/values.yaml +++ b/manifests/bucketeer/charts/batch/values.yaml @@ -60,12 +60,22 @@ env: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false nonPersistentRedis: serverName: addr: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false nonPersistentChildRedis: addresses: experimentLockTTL: 10m diff --git a/manifests/bucketeer/charts/subscriber/templates/deployment.yaml b/manifests/bucketeer/charts/subscriber/templates/deployment.yaml index 1903597cf9..ffef3cdae2 100644 --- a/manifests/bucketeer/charts/subscriber/templates/deployment.yaml +++ b/manifests/bucketeer/charts/subscriber/templates/deployment.yaml @@ -51,6 +51,11 @@ spec: secret: secretName: {{ .Values.global.operationalDatabase.postgres.sslSecretName }} {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + secret: + secretName: {{ .Values.global.pubsub.redis.tlsSecretName }} + {{- end }} - name: subscriber-config configMap: name: {{ template "subscriber.fullname" . }}-subscribers-config @@ -179,6 +184,16 @@ spec: value: "{{ .Values.env.persistentRedis.poolMaxActive }}" - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_MODE value: "{{ .Values.env.persistentRedis.mode }}" + - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.persistentRedis.tlsEnabled }}" + - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.persistentRedis.tlsCACert }}" + - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.persistentRedis.tlsCert }}" + - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.persistentRedis.tlsKey }}" + - name: BUCKETEER_SUBSCRIBER_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.persistentRedis.tlsInsecureSkipVerify }}" - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_SERVER_NAME value: "{{ .Values.env.nonPersistentRedis.serverName }}" - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_ADDR @@ -189,6 +204,16 @@ spec: value: "{{ .Values.env.nonPersistentRedis.poolMaxActive }}" - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_MODE value: "{{ .Values.env.nonPersistentRedis.mode }}" + - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.nonPersistentRedis.tlsEnabled }}" + - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCACert }}" + - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCert }}" + - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.nonPersistentRedis.tlsKey }}" + - name: BUCKETEER_SUBSCRIBER_NON_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.nonPersistentRedis.tlsInsecureSkipVerify }}" volumeMounts: - name: service-cert-secret @@ -202,6 +227,11 @@ spec: mountPath: /usr/local/certs/postgres readOnly: true {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + mountPath: /usr/local/certs/redis + readOnly: true + {{- end }} - name: email-config mountPath: /usr/local/email-config readOnly: true diff --git a/manifests/bucketeer/charts/subscriber/templates/subscribers-configmap.yaml b/manifests/bucketeer/charts/subscriber/templates/subscribers-configmap.yaml index e45b091111..a77457c83d 100644 --- a/manifests/bucketeer/charts/subscriber/templates/subscribers-configmap.yaml +++ b/manifests/bucketeer/charts/subscriber/templates/subscribers-configmap.yaml @@ -26,6 +26,11 @@ data: {{- $_ := set $config "redisMode" $.Values.global.pubsub.redis.mode }} {{- $_ := set $config "project" $.Values.global.pubsub.project }} {{- $_ := set $config "redisPartitionCount" $.Values.global.pubsub.redis.partitionCount }} + {{- $_ := set $config "redisTLSEnabled" $.Values.global.pubsub.redis.tlsEnabled }} + {{- $_ := set $config "redisTLSCACert" $.Values.global.pubsub.redis.tlsCACert }} + {{- $_ := set $config "redisTLSCert" $.Values.global.pubsub.redis.tlsCert }} + {{- $_ := set $config "redisTLSKey" $.Values.global.pubsub.redis.tlsKey }} + {{- $_ := set $config "redisTLSInsecureSkipVerify" $.Values.global.pubsub.redis.tlsInsecureSkipVerify }} {{- end }} {{ toJson $subscribers }} @@ -40,6 +45,11 @@ data: {{- $_ := set $config "redisMode" $.Values.global.pubsub.redis.mode }} {{- $_ := set $config "project" $.Values.global.pubsub.project }} {{- $_ := set $config "redisPartitionCount" $.Values.global.pubsub.redis.partitionCount }} + {{- $_ := set $config "redisTLSEnabled" $.Values.global.pubsub.redis.tlsEnabled }} + {{- $_ := set $config "redisTLSCACert" $.Values.global.pubsub.redis.tlsCACert }} + {{- $_ := set $config "redisTLSCert" $.Values.global.pubsub.redis.tlsCert }} + {{- $_ := set $config "redisTLSKey" $.Values.global.pubsub.redis.tlsKey }} + {{- $_ := set $config "redisTLSInsecureSkipVerify" $.Values.global.pubsub.redis.tlsInsecureSkipVerify }} {{- end }} {{ toJson .Values.onDemandSubscribers }} @@ -59,6 +69,11 @@ data: {{- $_ := set $config "redisMode" $.Values.global.pubsub.redis.mode }} {{- $_ := set $config "project" $.Values.global.pubsub.project }} {{- $_ := set $config "redisPartitionCount" $.Values.global.pubsub.redis.partitionCount }} + {{- $_ := set $config "redisTLSEnabled" $.Values.global.pubsub.redis.tlsEnabled }} + {{- $_ := set $config "redisTLSCACert" $.Values.global.pubsub.redis.tlsCACert }} + {{- $_ := set $config "redisTLSCert" $.Values.global.pubsub.redis.tlsCert }} + {{- $_ := set $config "redisTLSKey" $.Values.global.pubsub.redis.tlsKey }} + {{- $_ := set $config "redisTLSInsecureSkipVerify" $.Values.global.pubsub.redis.tlsInsecureSkipVerify }} {{- end }} {{- end }} {{ toJson $processors }} diff --git a/manifests/bucketeer/charts/subscriber/values.yaml b/manifests/bucketeer/charts/subscriber/values.yaml index fd959f22e7..e0ac33c058 100644 --- a/manifests/bucketeer/charts/subscriber/values.yaml +++ b/manifests/bucketeer/charts/subscriber/values.yaml @@ -55,12 +55,22 @@ env: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false nonPersistentRedis: serverName: addr: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false affinity: {} diff --git a/manifests/bucketeer/charts/web/templates/deployment.yaml b/manifests/bucketeer/charts/web/templates/deployment.yaml index b1f32c2504..1168222b1f 100644 --- a/manifests/bucketeer/charts/web/templates/deployment.yaml +++ b/manifests/bucketeer/charts/web/templates/deployment.yaml @@ -53,6 +53,11 @@ spec: secret: secretName: {{ .Values.global.operationalDatabase.postgres.sslSecretName }} {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + secret: + secretName: {{ .Values.global.pubsub.redis.tlsSecretName }} + {{- end }} - name: oauth-key-secret secret: secretName: {{ template "oauth-key-secret" . }} @@ -123,6 +128,16 @@ spec: value: "{{ .Values.env.persistentRedis.poolMaxActive }}" - name: BUCKETEER_WEB_PERSISTENT_REDIS_MODE value: "{{ .Values.env.persistentRedis.mode }}" + - name: BUCKETEER_WEB_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.persistentRedis.tlsEnabled }}" + - name: BUCKETEER_WEB_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.persistentRedis.tlsCACert }}" + - name: BUCKETEER_WEB_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.persistentRedis.tlsCert }}" + - name: BUCKETEER_WEB_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.persistentRedis.tlsKey }}" + - name: BUCKETEER_WEB_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.persistentRedis.tlsInsecureSkipVerify }}" - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_SERVER_NAME value: "{{ .Values.env.nonPersistentRedis.serverName }}" - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_ADDR @@ -133,6 +148,16 @@ spec: value: "{{ .Values.env.nonPersistentRedis.poolMaxActive }}" - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_MODE value: "{{ .Values.env.nonPersistentRedis.mode }}" + - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_TLS_ENABLED + value: "{{ .Values.env.nonPersistentRedis.tlsEnabled }}" + - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_TLS_CA_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCACert }}" + - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_TLS_CERT + value: "{{ .Values.env.nonPersistentRedis.tlsCert }}" + - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_TLS_KEY + value: "{{ .Values.env.nonPersistentRedis.tlsKey }}" + - name: BUCKETEER_WEB_NON_PERSISTENT_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.env.nonPersistentRedis.tlsInsecureSkipVerify }}" - name: BUCKETEER_WEB_BIGQUERY_DATA_SET value: "{{ .Values.env.bigQueryDataSet }}" - name: BUCKETEER_WEB_BIGQUERY_DATA_LOCATION @@ -217,6 +242,16 @@ spec: value: "{{ .Values.global.pubsub.redis.minIdle }}" - name: BUCKETEER_WEB_PUBSUB_REDIS_MODE value: "{{ .Values.env.pubSubRedisMode }}" + - name: BUCKETEER_WEB_PUBSUB_REDIS_TLS_ENABLED + value: "{{ .Values.global.pubsub.redis.tlsEnabled }}" + - name: BUCKETEER_WEB_PUBSUB_REDIS_TLS_CA_CERT + value: "{{ .Values.global.pubsub.redis.tlsCACert }}" + - name: BUCKETEER_WEB_PUBSUB_REDIS_TLS_CERT + value: "{{ .Values.global.pubsub.redis.tlsCert }}" + - name: BUCKETEER_WEB_PUBSUB_REDIS_TLS_KEY + value: "{{ .Values.global.pubsub.redis.tlsKey }}" + - name: BUCKETEER_WEB_PUBSUB_REDIS_TLS_INSECURE_SKIP_VERIFY + value: "{{ .Values.global.pubsub.redis.tlsInsecureSkipVerify }}" - name: BUCKETEER_WEB_PUBSUB_REDIS_PARTITION_COUNT value: "{{ .Values.global.pubsub.redis.partitionCount }}" - name: BUCKETEER_WEB_PROJECT @@ -284,6 +319,11 @@ spec: mountPath: /usr/local/certs/postgres readOnly: true {{- end }} + {{- if .Values.global.pubsub.redis.tlsSecretName }} + - name: redis-cert-secret + mountPath: /usr/local/certs/redis + readOnly: true + {{- end }} - name: oauth-key-secret mountPath: /usr/local/oauth-key readOnly: true diff --git a/manifests/bucketeer/charts/web/values.yaml b/manifests/bucketeer/charts/web/values.yaml index 046e822935..aa954e276c 100644 --- a/manifests/bucketeer/charts/web/values.yaml +++ b/manifests/bucketeer/charts/web/values.yaml @@ -45,12 +45,22 @@ env: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false nonPersistentRedis: serverName: addr: poolMaxIdle: 25 poolMaxActive: 25 mode: auto + tlsEnabled: false + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false bigQueryDataSet: bigQueryDataLocation: domainTopic: diff --git a/manifests/bucketeer/values.yaml b/manifests/bucketeer/values.yaml index 098a3c49b4..e3c80fecea 100644 --- a/manifests/bucketeer/values.yaml +++ b/manifests/bucketeer/values.yaml @@ -83,6 +83,24 @@ global: partitionCount: 16 # Idle time in seconds for pending message reclaim idleTime: 600 + # TLS configuration, for connecting to a TLS-enabled Redis/Valkey + # deployment (e.g. AWS ElastiCache/MemoryDB with in-transit encryption). + tlsEnabled: false + # Path inside the container to a PEM-encoded CA certificate. Leave + # empty to use the system CA pool (sufficient for AWS ElastiCache). + tlsCACert: "" + tlsCert: "" + tlsKey: "" + tlsInsecureSkipVerify: false + # Existing secret holding Redis/Valkey TLS client materials (CA cert, + # client cert/key for mutual TLS). Mounted read-only at + # /usr/local/certs/redis in every service, so tlsCACert/tlsCert/tlsKey + # above (and the equivalent persistent/non-persistent redis TLS paths + # in each chart's values.yaml) can reference files under that path. + # Shared across every redis block, not just pubsub redis. Only needed + # when those paths point inside this secret; AWS ElastiCache/MemoryDB + # with a publicly-trusted CA needs only tlsEnabled: true and no secret. + tlsSecretName: "" # PubSub emulator host (for local development) emulatorHost: "" # Google Cloud project ID diff --git a/pkg/api/cmd/server.go b/pkg/api/cmd/server.go index 964cda34a1..7ef2401c67 100644 --- a/pkg/api/cmd/server.go +++ b/pkg/api/cmd/server.go @@ -121,6 +121,11 @@ type server struct { redisServerName *string redisAddr *string redisMode *string + redisTLSEnabled *bool + redisTLSCACert *string + redisTLSCert *string + redisTLSKey *string + redisTLSInsecureSkipVerify *bool certPath *string keyPath *string serviceTokenPath *string @@ -134,17 +139,22 @@ type server struct { segmentUsersMemoryCacheTTL *time.Duration featureFlagDiffGracePeriod *time.Duration // PubSub configurations - pubSubType *string - pubSubRedisServerName *string - pubSubRedisAddr *string - pubSubRedisPoolSize *int - pubSubRedisMinIdle *int - pubSubRedisPartitionCount *int - pubSubRedisMode *string - cacheInvalidationTopic *string - sseHeartbeatInterval *time.Duration - sseMaxConnections *int - sseReadinessThreshold *float64 + pubSubType *string + pubSubRedisServerName *string + pubSubRedisAddr *string + pubSubRedisPoolSize *int + pubSubRedisMinIdle *int + pubSubRedisPartitionCount *int + pubSubRedisMode *string + pubSubRedisTLSEnabled *bool + pubSubRedisTLSCACert *string + pubSubRedisTLSCert *string + pubSubRedisTLSKey *string + pubSubRedisTLSInsecureSkipVerify *bool + cacheInvalidationTopic *string + sseHeartbeatInterval *time.Duration + sseMaxConnections *int + sseReadinessThreshold *float64 } func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { @@ -261,6 +271,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { redisMode: cmd.Flag("redis-mode", "Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + redisTLSEnabled: cmd.Flag( + "redis-tls-enabled", + "Enable TLS when connecting to Redis (e.g. AWS ElastiCache/MemoryDB with in-transit encryption).", + ).Default("false").Bool(), + redisTLSCACert: cmd.Flag( + "redis-tls-ca-cert", + "Path to the Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + redisTLSCert: cmd.Flag( + "redis-tls-cert", + "Path to the Redis TLS client certificate file (for mutual TLS).", + ).String(), + redisTLSKey: cmd.Flag( + "redis-tls-key", + "Path to the Redis TLS client private key file (for mutual TLS).", + ).String(), + redisTLSInsecureSkipVerify: cmd.Flag( + "redis-tls-insecure-skip-verify", + "Skip Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), certPath: cmd.Flag("cert", "Path to TLS certificate.").Required().String(), keyPath: cmd.Flag("key", "Path to TLS key.").Required().String(), serviceTokenPath: cmd.Flag("service-token", "Path to service token.").Required().String(), @@ -327,6 +357,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { pubSubRedisMode: cmd.Flag("pubsub-redis-mode", "PubSub Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + pubSubRedisTLSEnabled: cmd.Flag( + "pubsub-redis-tls-enabled", + "Enable TLS when connecting to the PubSub Redis server.", + ).Default("false").Bool(), + pubSubRedisTLSCACert: cmd.Flag( + "pubsub-redis-tls-ca-cert", + "Path to the PubSub Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + pubSubRedisTLSCert: cmd.Flag( + "pubsub-redis-tls-cert", + "Path to the PubSub Redis TLS client certificate file (for mutual TLS).", + ).String(), + pubSubRedisTLSKey: cmd.Flag( + "pubsub-redis-tls-key", + "Path to the PubSub Redis TLS client private key file (for mutual TLS).", + ).String(), + pubSubRedisTLSInsecureSkipVerify: cmd.Flag( + "pubsub-redis-tls-insecure-skip-verify", + "Skip PubSub Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), cacheInvalidationTopic: cmd.Flag("cache-invalidation-topic", "PubSub topic on which the subscriber announces L2 cache refreshes. "+ "When set, this pod evicts its L1 (in-memory) cache entries on each "+ @@ -371,6 +421,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.pubSubRedisMinIdle), redisv3.WithServerName(*s.pubSubRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.pubSubRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.pubSubRedisTLSEnabled, + CACert: *s.pubSubRedisTLSCACert, + Cert: *s.pubSubRedisTLSCert, + Key: *s.pubSubRedisTLSKey, + InsecureSkipVerify: *s.pubSubRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -565,6 +622,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.redisPoolMaxIdle), redisv3.WithServerName(*s.redisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.redisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.redisTLSEnabled, + CACert: *s.redisTLSCACert, + Cert: *s.redisTLSCert, + Key: *s.redisTLSKey, + InsecureSkipVerify: *s.redisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) diff --git a/pkg/batch/cmd/server/server.go b/pkg/batch/cmd/server/server.go index 7e0bf45c39..e2e6862eab 100644 --- a/pkg/batch/cmd/server/server.go +++ b/pkg/batch/cmd/server/server.go @@ -156,22 +156,32 @@ type server struct { experimentCalculatorService *string batchService *string // Persistent Redis - persistentRedisServerName *string - persistentRedisAddr *string - persistentRedisPoolMaxIdle *int - persistentRedisPoolMaxActive *int - persistentRedisMode *string + persistentRedisServerName *string + persistentRedisAddr *string + persistentRedisPoolMaxIdle *int + persistentRedisPoolMaxActive *int + persistentRedisMode *string + persistentRedisTLSEnabled *bool + persistentRedisTLSCACert *string + persistentRedisTLSCert *string + persistentRedisTLSKey *string + persistentRedisTLSInsecureSkipVerify *bool // Non Persistent Redis - nonPersistentRedisServerName *string - nonPersistentRedisAddr *string - nonPersistentChildRedisAddresses *[]string - nonPersistentRedisPoolMaxIdle *int - nonPersistentRedisPoolMaxActive *int - nonPersistentRedisMode *string - prometheusURL *string - httpReadTimeout *time.Duration - httpWriteTimeout *time.Duration - httpIdleTimeout *time.Duration + nonPersistentRedisServerName *string + nonPersistentRedisAddr *string + nonPersistentChildRedisAddresses *[]string + nonPersistentRedisPoolMaxIdle *int + nonPersistentRedisPoolMaxActive *int + nonPersistentRedisMode *string + nonPersistentRedisTLSEnabled *bool + nonPersistentRedisTLSCACert *string + nonPersistentRedisTLSCert *string + nonPersistentRedisTLSKey *string + nonPersistentRedisTLSInsecureSkipVerify *bool + prometheusURL *string + httpReadTimeout *time.Duration + httpWriteTimeout *time.Duration + httpIdleTimeout *time.Duration } func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { @@ -290,6 +300,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { persistentRedisMode: cmd.Flag("persistent-redis-mode", "Persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + persistentRedisTLSEnabled: cmd.Flag( + "persistent-redis-tls-enabled", + "Enable TLS when connecting to the persistent Redis server.", + ).Default("false").Bool(), + persistentRedisTLSCACert: cmd.Flag( + "persistent-redis-tls-ca-cert", + "Path to the persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + persistentRedisTLSCert: cmd.Flag( + "persistent-redis-tls-cert", + "Path to the persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + persistentRedisTLSKey: cmd.Flag( + "persistent-redis-tls-key", + "Path to the persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + persistentRedisTLSInsecureSkipVerify: cmd.Flag( + "persistent-redis-tls-insecure-skip-verify", + "Skip persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), nonPersistentRedisServerName: cmd.Flag( "non-persistent-redis-server-name", "Name of the non-persistent redis.", @@ -309,6 +339,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { nonPersistentRedisMode: cmd.Flag("non-persistent-redis-mode", "Non-persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + nonPersistentRedisTLSEnabled: cmd.Flag( + "non-persistent-redis-tls-enabled", + "Enable TLS when connecting to the non-persistent Redis server.", + ).Default("false").Bool(), + nonPersistentRedisTLSCACert: cmd.Flag( + "non-persistent-redis-tls-ca-cert", + "Path to the non-persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + nonPersistentRedisTLSCert: cmd.Flag( + "non-persistent-redis-tls-cert", + "Path to the non-persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSKey: cmd.Flag( + "non-persistent-redis-tls-key", + "Path to the non-persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSInsecureSkipVerify: cmd.Flag( + "non-persistent-redis-tls-insecure-skip-verify", + "Skip non-persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), nonPersistentChildRedisAddresses: cmd.Flag( "non-persistent-child-redis-addresses", "A list of non-persistent child Redis addresses.", @@ -516,6 +566,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.persistentRedisPoolMaxIdle), redisv3.WithServerName(*s.persistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.persistentRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.persistentRedisTLSEnabled, + CACert: *s.persistentRedisTLSCACert, + Cert: *s.persistentRedisTLSCert, + Key: *s.persistentRedisTLSKey, + InsecureSkipVerify: *s.persistentRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -523,12 +580,21 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L return err } + nonPersistentRedisTLS := redisv3.TLSConfig{ + Enabled: *s.nonPersistentRedisTLSEnabled, + CACert: *s.nonPersistentRedisTLSCACert, + Cert: *s.nonPersistentRedisTLSCert, + Key: *s.nonPersistentRedisTLSKey, + InsecureSkipVerify: *s.nonPersistentRedisTLSInsecureSkipVerify, + } + nonPersistentRedisClient, err := redisv3.NewClient( *s.nonPersistentRedisAddr, redisv3.WithPoolSize(*s.nonPersistentRedisPoolMaxActive), redisv3.WithMinIdleConns(*s.nonPersistentRedisPoolMaxIdle), redisv3.WithServerName(*s.nonPersistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.nonPersistentRedisMode)), + redisv3.WithTLS(nonPersistentRedisTLS), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -557,6 +623,7 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.nonPersistentRedisPoolMaxIdle), redisv3.WithServerName(s.getRedisHostname(address)), redisv3.WithRedisMode(redisv3.RedisMode(*s.nonPersistentRedisMode)), + redisv3.WithTLS(nonPersistentRedisTLS), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) diff --git a/pkg/redis/v3/redis.go b/pkg/redis/v3/redis.go index fe81205ad5..0ec56bace1 100644 --- a/pkg/redis/v3/redis.go +++ b/pkg/redis/v3/redis.go @@ -17,8 +17,11 @@ package v3 import ( "context" + "crypto/tls" + "crypto/x509" "errors" "fmt" + "os" "strings" "time" @@ -176,6 +179,21 @@ type pipeClient struct { logger *zap.Logger } +// TLSConfig configures TLS for connecting to a TLS-enabled Redis/Valkey +// deployment (e.g. AWS ElastiCache/MemoryDB with in-transit encryption). +// When Enabled is false, the connection is plaintext. +type TLSConfig struct { + Enabled bool + // CACert is the path to a PEM-encoded CA certificate used to verify the + // server certificate. If empty, the host's system CA pool is used. + CACert string + // Cert and Key are paths to a PEM-encoded client certificate/key pair, + // used for mutual TLS. Both must be set together, or left empty. + Cert string + Key string + InsecureSkipVerify bool +} + type options struct { password string maxRetries int @@ -185,6 +203,8 @@ type options struct { poolTimeout time.Duration serverName string redisMode RedisMode + tls TLSConfig + tlsConfig *tls.Config metrics metrics.Registerer logger *zap.Logger } @@ -257,6 +277,15 @@ func WithLogger(logger *zap.Logger) Option { } } +// WithTLS enables TLS for the connection to Redis/Valkey. Use this to +// connect to managed deployments with in-transit encryption enabled, such +// as AWS ElastiCache/MemoryDB. +func WithTLS(cfg TLSConfig) Option { + return func(opts *options) { + opts.tls = cfg + } +} + // WithRedisMode sets the Redis client creation mode. // "cluster" always creates a ClusterClient, "standalone" always creates a standard Client, // "auto" (default) tries detection and runs background mismatch checking. @@ -279,6 +308,12 @@ func NewClient(addr string, opts ...Option) (Client, error) { } logger := options.logger.Named("redis-v3") + tlsConfig, err := buildTLSConfig(options.tls) + if err != nil { + return nil, err + } + options.tlsConfig = tlsConfig + clusterOpts := &goredis.ClusterOptions{ Addrs: []string{addr}, Password: options.password, @@ -287,6 +322,7 @@ func NewClient(addr string, opts ...Option) (Client, error) { PoolSize: options.poolSize, MinIdleConns: options.minIdleConns, PoolTimeout: options.poolTimeout, + TLSConfig: tlsConfig, } standardOpts := &goredis.Options{ Addr: addr, @@ -296,6 +332,7 @@ func NewClient(addr string, opts ...Option) (Client, error) { PoolSize: options.poolSize, MinIdleConns: options.minIdleConns, PoolTimeout: options.poolTimeout, + TLSConfig: tlsConfig, } var rc goredis.UniversalClient @@ -358,6 +395,41 @@ func NewClient(addr string, opts ...Option) (Client, error) { return c, nil } +// buildTLSConfig converts a TLSConfig into a *tls.Config suitable for +// go-redis Options/ClusterOptions.TLSConfig. Returns nil (plaintext) when +// TLS is not enabled. +func buildTLSConfig(cfg TLSConfig) (*tls.Config, error) { + if !cfg.Enabled { + return nil, nil + } + tlsConfig := &tls.Config{ + MinVersion: tls.VersionTLS12, + InsecureSkipVerify: cfg.InsecureSkipVerify, //nolint:gosec + } + if cfg.CACert != "" { + caCert, err := os.ReadFile(cfg.CACert) + if err != nil { + return nil, fmt.Errorf("redis: failed to read TLS CA certificate: %w", err) + } + caCertPool := x509.NewCertPool() + if !caCertPool.AppendCertsFromPEM(caCert) { + return nil, fmt.Errorf("redis: failed to parse TLS CA certificate") + } + tlsConfig.RootCAs = caCertPool + } + if cfg.Cert != "" || cfg.Key != "" { + if cfg.Cert == "" || cfg.Key == "" { + return nil, fmt.Errorf("redis: both TLS client certificate and key must be set together") + } + cert, err := tls.LoadX509KeyPair(cfg.Cert, cfg.Key) + if err != nil { + return nil, fmt.Errorf("redis: failed to load TLS client certificate/key: %w", err) + } + tlsConfig.Certificates = []tls.Certificate{cert} + } + return tlsConfig, nil +} + // detectRedisMode tries to determine whether the Redis server is a cluster or standalone // by issuing CLUSTER INFO with a short timeout. Falls back to standalone if detection fails. // Note: if Redis is unreachable at startup and the actual topology is a cluster, @@ -416,6 +488,7 @@ func (c *client) runMismatchDetector(addr string) { Addr: addr, Password: c.opts.password, DialTimeout: c.opts.dialTimeout, + TLSConfig: c.opts.tlsConfig, }) actualCluster := false diff --git a/pkg/redis/v3/redis_test.go b/pkg/redis/v3/redis_test.go index ca78bb0d91..b556be7f9d 100644 --- a/pkg/redis/v3/redis_test.go +++ b/pkg/redis/v3/redis_test.go @@ -15,13 +15,61 @@ package v3 import ( + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rand" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "os" + "path/filepath" "testing" + "time" goredis "github.com/redis/go-redis/v9" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/zap" ) +// writeTestCertKeyPair generates a self-signed EC certificate/key pair and +// writes them as PEM files under dir, returning their paths. +func writeTestCertKeyPair(t *testing.T, dir, prefix string) (certPath, keyPath string) { + t.Helper() + + priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + template := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "redis-test"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + IsCA: true, + } + derBytes, err := x509.CreateCertificate(rand.Reader, template, template, &priv.PublicKey, priv) + require.NoError(t, err) + + certPath = filepath.Join(dir, prefix+".crt") + keyPath = filepath.Join(dir, prefix+".key") + + certOut, err := os.Create(certPath) + require.NoError(t, err) + defer certOut.Close() + require.NoError(t, pem.Encode(certOut, &pem.Block{Type: "CERTIFICATE", Bytes: derBytes})) + + keyBytes, err := x509.MarshalECPrivateKey(priv) + require.NoError(t, err) + keyOut, err := os.Create(keyPath) + require.NoError(t, err) + defer keyOut.Close() + require.NoError(t, pem.Encode(keyOut, &pem.Block{Type: "EC PRIVATE KEY", Bytes: keyBytes})) + + return certPath, keyPath +} + func TestNewClientIntegration(t *testing.T) { if testing.Short() { t.Skip("Skipping integration test in short mode") @@ -230,3 +278,122 @@ func TestClientTypeString(t *testing.T) { assert.Equal(t, "cluster", clientTypeString(ClientTypeCluster)) assert.Equal(t, "standalone", clientTypeString(ClientTypeStandard)) } + +func TestWithTLS(t *testing.T) { + t.Parallel() + + cfg := TLSConfig{Enabled: true, CACert: "/path/to/ca.crt"} + opts := defaultOptions() + WithTLS(cfg)(opts) + assert.Equal(t, cfg, opts.tls) +} + +func TestBuildTLSConfig(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + caCertPath, _ := writeTestCertKeyPair(t, dir, "ca") + certPath, keyPath := writeTestCertKeyPair(t, dir, "client") + + t.Run("disabled returns nil config", func(t *testing.T) { + t.Parallel() + tlsConfig, err := buildTLSConfig(TLSConfig{Enabled: false}) + require.NoError(t, err) + assert.Nil(t, tlsConfig) + }) + + t.Run("enabled with no cert paths uses system pool", func(t *testing.T) { + t.Parallel() + tlsConfig, err := buildTLSConfig(TLSConfig{Enabled: true}) + require.NoError(t, err) + require.NotNil(t, tlsConfig) + assert.Nil(t, tlsConfig.RootCAs) + assert.False(t, tlsConfig.InsecureSkipVerify) + }) + + t.Run("insecure skip verify is propagated", func(t *testing.T) { + t.Parallel() + tlsConfig, err := buildTLSConfig(TLSConfig{Enabled: true, InsecureSkipVerify: true}) + require.NoError(t, err) + require.NotNil(t, tlsConfig) + assert.True(t, tlsConfig.InsecureSkipVerify) + }) + + t.Run("valid CA cert is loaded", func(t *testing.T) { + t.Parallel() + tlsConfig, err := buildTLSConfig(TLSConfig{Enabled: true, CACert: caCertPath}) + require.NoError(t, err) + require.NotNil(t, tlsConfig) + assert.NotNil(t, tlsConfig.RootCAs) + }) + + t.Run("missing CA cert file errors", func(t *testing.T) { + t.Parallel() + _, err := buildTLSConfig(TLSConfig{Enabled: true, CACert: "/nonexistent/ca.crt"}) + assert.Error(t, err) + }) + + t.Run("invalid CA cert content errors", func(t *testing.T) { + t.Parallel() + badCACert := filepath.Join(dir, "bad-ca.crt") + require.NoError(t, os.WriteFile(badCACert, []byte("not a pem cert"), 0o600)) + _, err := buildTLSConfig(TLSConfig{Enabled: true, CACert: badCACert}) + assert.Error(t, err) + }) + + t.Run("valid client cert and key are loaded", func(t *testing.T) { + t.Parallel() + tlsConfig, err := buildTLSConfig(TLSConfig{Enabled: true, Cert: certPath, Key: keyPath}) + require.NoError(t, err) + require.NotNil(t, tlsConfig) + assert.Len(t, tlsConfig.Certificates, 1) + }) + + t.Run("cert without key errors", func(t *testing.T) { + t.Parallel() + _, err := buildTLSConfig(TLSConfig{Enabled: true, Cert: certPath}) + assert.Error(t, err) + }) + + t.Run("key without cert errors", func(t *testing.T) { + t.Parallel() + _, err := buildTLSConfig(TLSConfig{Enabled: true, Key: keyPath}) + assert.Error(t, err) + }) + + t.Run("mismatched cert and key errors", func(t *testing.T) { + t.Parallel() + _, otherKeyPath := writeTestCertKeyPair(t, dir, "other") + _, err := buildTLSConfig(TLSConfig{Enabled: true, Cert: certPath, Key: otherKeyPath}) + assert.Error(t, err) + }) +} + +func TestNewClientWithTLS(t *testing.T) { + t.Parallel() + + t.Run("TLS enabled against unreachable host does not fail startup", func(t *testing.T) { + t.Parallel() + logger := zap.NewNop() + c, err := NewClient( + "localhost:9999", + WithLogger(logger), + WithTLS(TLSConfig{Enabled: true}), + ) + require.NoError(t, err) + require.NotNil(t, c) + c.Close() + }) + + t.Run("invalid TLS config returns error", func(t *testing.T) { + t.Parallel() + logger := zap.NewNop() + c, err := NewClient( + "localhost:9999", + WithLogger(logger), + WithTLS(TLSConfig{Enabled: true, CACert: "/nonexistent/ca.crt"}), + ) + assert.Error(t, err) + assert.Nil(t, c) + }) +} diff --git a/pkg/subscriber/cmd/server/server.go b/pkg/subscriber/cmd/server/server.go index 0db946c4d8..9cc77c4d3b 100644 --- a/pkg/subscriber/cmd/server/server.go +++ b/pkg/subscriber/cmd/server/server.go @@ -125,17 +125,27 @@ type server struct { processorsConfig *string onDemandProcessorsConfig *string // Persistent Redis - persistentRedisServerName *string - persistentRedisAddr *string - persistentRedisPoolMaxIdle *int - persistentRedisPoolMaxActive *int - persistentRedisMode *string + persistentRedisServerName *string + persistentRedisAddr *string + persistentRedisPoolMaxIdle *int + persistentRedisPoolMaxActive *int + persistentRedisMode *string + persistentRedisTLSEnabled *bool + persistentRedisTLSCACert *string + persistentRedisTLSCert *string + persistentRedisTLSKey *string + persistentRedisTLSInsecureSkipVerify *bool // Non Persistent Redis - nonPersistentRedisServerName *string - nonPersistentRedisAddr *string - nonPersistentRedisPoolMaxIdle *int - nonPersistentRedisPoolMaxActive *int - nonPersistentRedisMode *string + nonPersistentRedisServerName *string + nonPersistentRedisAddr *string + nonPersistentRedisPoolMaxIdle *int + nonPersistentRedisPoolMaxActive *int + nonPersistentRedisMode *string + nonPersistentRedisTLSEnabled *bool + nonPersistentRedisTLSCACert *string + nonPersistentRedisTLSCert *string + nonPersistentRedisTLSKey *string + nonPersistentRedisTLSInsecureSkipVerify *bool } func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { @@ -250,6 +260,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { persistentRedisMode: cmd.Flag("persistent-redis-mode", "Persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + persistentRedisTLSEnabled: cmd.Flag( + "persistent-redis-tls-enabled", + "Enable TLS when connecting to the persistent Redis server.", + ).Default("false").Bool(), + persistentRedisTLSCACert: cmd.Flag( + "persistent-redis-tls-ca-cert", + "Path to the persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + persistentRedisTLSCert: cmd.Flag( + "persistent-redis-tls-cert", + "Path to the persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + persistentRedisTLSKey: cmd.Flag( + "persistent-redis-tls-key", + "Path to the persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + persistentRedisTLSInsecureSkipVerify: cmd.Flag( + "persistent-redis-tls-insecure-skip-verify", + "Skip persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), nonPersistentRedisServerName: cmd.Flag( "non-persistent-redis-server-name", "Name of the non-persistent redis.", @@ -269,6 +299,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { nonPersistentRedisMode: cmd.Flag("non-persistent-redis-mode", "Non-persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + nonPersistentRedisTLSEnabled: cmd.Flag( + "non-persistent-redis-tls-enabled", + "Enable TLS when connecting to the non-persistent Redis server.", + ).Default("false").Bool(), + nonPersistentRedisTLSCACert: cmd.Flag( + "non-persistent-redis-tls-ca-cert", + "Path to the non-persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + nonPersistentRedisTLSCert: cmd.Flag( + "non-persistent-redis-tls-cert", + "Path to the non-persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSKey: cmd.Flag( + "non-persistent-redis-tls-key", + "Path to the non-persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSInsecureSkipVerify: cmd.Flag( + "non-persistent-redis-tls-insecure-skip-verify", + "Skip non-persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), } r.RegisterCommand(server) return server @@ -395,6 +445,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.nonPersistentRedisPoolMaxIdle), redisv3.WithServerName(*s.nonPersistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.nonPersistentRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.nonPersistentRedisTLSEnabled, + CACert: *s.nonPersistentRedisTLSCACert, + Cert: *s.nonPersistentRedisTLSCert, + Key: *s.nonPersistentRedisTLSKey, + InsecureSkipVerify: *s.nonPersistentRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -408,6 +465,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.persistentRedisPoolMaxIdle), redisv3.WithServerName(*s.persistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.persistentRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.persistentRedisTLSEnabled, + CACert: *s.persistentRedisTLSCACert, + Cert: *s.persistentRedisTLSCert, + Key: *s.persistentRedisTLSKey, + InsecureSkipVerify: *s.persistentRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -652,6 +716,13 @@ func (s *server) createCacheInvalidationPublisher( redisv3.WithMinIdleConns(conf.RedisMinIdle), redisv3.WithServerName(conf.RedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(conf.RedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: conf.RedisTLSEnabled, + CACert: conf.RedisTLSCACert, + Cert: conf.RedisTLSCert, + Key: conf.RedisTLSKey, + InsecureSkipVerify: conf.RedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) diff --git a/pkg/subscriber/processor/segment_user_persister.go b/pkg/subscriber/processor/segment_user_persister.go index 75cc4b97a3..fd5af0a840 100644 --- a/pkg/subscriber/processor/segment_user_persister.go +++ b/pkg/subscriber/processor/segment_user_persister.go @@ -63,6 +63,12 @@ type segmentUserPersisterConfig struct { RedisPartitionCount int `json:"redisPartitionCount"` // Redis partition count RedisMode string `json:"redisMode"` // Redis client mode: cluster, standalone, or auto Project string `json:"project"` // Google Cloud project ID + // Redis TLS configuration + RedisTLSEnabled bool `json:"redisTLSEnabled"` + RedisTLSCACert string `json:"redisTLSCACert"` + RedisTLSCert string `json:"redisTLSCert"` + RedisTLSKey string `json:"redisTLSKey"` + RedisTLSInsecureSkipVerify bool `json:"redisTLSInsecureSkipVerify"` } type segmentUserPersister struct { @@ -203,6 +209,13 @@ func createRedisClientForSegmentPersister( v3.WithRedisMode(redisMode), v3.WithMetrics(registerer), v3.WithLogger(logger), + v3.WithTLS(v3.TLSConfig{ + Enabled: conf.RedisTLSEnabled, + CACert: conf.RedisTLSCACert, + Cert: conf.RedisTLSCert, + Key: conf.RedisTLSKey, + InsecureSkipVerify: conf.RedisTLSInsecureSkipVerify, + }), ) } diff --git a/pkg/subscriber/subscriber.go b/pkg/subscriber/subscriber.go index a8f455419a..66500f934e 100644 --- a/pkg/subscriber/subscriber.go +++ b/pkg/subscriber/subscriber.go @@ -98,6 +98,13 @@ type Configuration struct { RedisPartitionCount int `json:"redisPartitionCount,omitempty"` RedisIdleTime int `json:"redisIdleTime,omitempty"` RedisMode string `json:"redisMode,omitempty"` + // Redis TLS configuration (used to connect to TLS-enabled Redis/Valkey + // deployments, e.g. AWS ElastiCache/MemoryDB with in-transit encryption) + RedisTLSEnabled bool `json:"redisTLSEnabled,omitempty"` + RedisTLSCACert string `json:"redisTLSCACert,omitempty"` + RedisTLSCert string `json:"redisTLSCert,omitempty"` + RedisTLSKey string `json:"redisTLSKey,omitempty"` + RedisTLSInsecureSkipVerify bool `json:"redisTLSInsecureSkipVerify,omitempty"` } type pubSubSubscriber struct { @@ -289,6 +296,13 @@ func createRedisClient(ctx context.Context, redisv3.WithMinIdleConns(redisMinIdle), redisv3.WithServerName(conf.RedisServerName), redisv3.WithRedisMode(redisMode), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: conf.RedisTLSEnabled, + CACert: conf.RedisTLSCACert, + Cert: conf.RedisTLSCert, + Key: conf.RedisTLSKey, + InsecureSkipVerify: conf.RedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(metrics), redisv3.WithLogger(logger), ) diff --git a/pkg/web/cmd/server/server.go b/pkg/web/cmd/server/server.go index 00ac0de046..644f266824 100644 --- a/pkg/web/cmd/server/server.go +++ b/pkg/web/cmd/server/server.go @@ -165,87 +165,102 @@ type GracefulStopper interface { type server struct { *kingpin.CmdClause - port *int - project *string - isDemoSiteEnabled *bool - timezone *string - certPath *string - keyPath *string - serviceTokenPath *string - operationalDatabaseType *string - mysqlUser *string - mysqlPass *string - mysqlHost *string - mysqlPort *int - mysqlDBName *string - postgresUser *string - postgresPass *string - postgresHost *string - postgresPort *int - postgresDBName *string - postgresSSLMode *string - postgresSSLRootCert *string - postgresSSLCert *string - postgresSSLKey *string - persistentRedisServerName *string - persistentRedisAddr *string - persistentRedisPoolMaxIdle *int - persistentRedisPoolMaxActive *int - persistentRedisMode *string - nonPersistentRedisServerName *string - nonPersistentRedisAddr *string - nonPersistentRedisPoolMaxIdle *int - nonPersistentRedisPoolMaxActive *int - nonPersistentRedisMode *string - bigQueryDataSet *string - bigQueryDataLocation *string - domainTopic *string - bulkSegmentUsersReceivedTopic *string - accountServicePort *int - authServicePort *int - auditLogServicePort *int - autoOpsServicePort *int - environmentServicePort *int - eventCounterServicePort *int - experimentServicePort *int - featureServicePort *int - subscriptionServicePort *int - pushServicePort *int - dashboardServicePort *int - tagServicePort *int - codeReferenceServicePort *int - teamServicePort *int - insightsServicePort *int - notificationServicePort *int - prometheusURL *string - webGrpcGatewayPort *int - accountService *string - authService *string - batchService *string - environmentService *string - experimentService *string - featureService *string - autoOpsService *string - codeReferenceService *string - accessTokenTTL *time.Duration - refreshTokenTTL *time.Duration - emailFilter *string - oauthConfigPath *string - oauthPublicKeyPath *string - oauthPrivateKeyPath *string - webhookBaseURL *string - webhookKMSResourceName *string - cloudService *string - webConsoleEnvJSPath *string - pubSubType *string - pubSubRedisServerName *string - pubSubRedisAddr *string - pubSubRedisPoolSize *int - pubSubRedisMinIdle *int - pubSubRedisPartitionCount *int - pubSubRedisMode *string - dataWarehouseType *string - dataWarehouseConfigPath *string + port *int + project *string + isDemoSiteEnabled *bool + timezone *string + certPath *string + keyPath *string + serviceTokenPath *string + operationalDatabaseType *string + mysqlUser *string + mysqlPass *string + mysqlHost *string + mysqlPort *int + mysqlDBName *string + postgresUser *string + postgresPass *string + postgresHost *string + postgresPort *int + postgresDBName *string + postgresSSLMode *string + postgresSSLRootCert *string + postgresSSLCert *string + postgresSSLKey *string + persistentRedisServerName *string + persistentRedisAddr *string + persistentRedisPoolMaxIdle *int + persistentRedisPoolMaxActive *int + persistentRedisMode *string + persistentRedisTLSEnabled *bool + persistentRedisTLSCACert *string + persistentRedisTLSCert *string + persistentRedisTLSKey *string + persistentRedisTLSInsecureSkipVerify *bool + nonPersistentRedisServerName *string + nonPersistentRedisAddr *string + nonPersistentRedisPoolMaxIdle *int + nonPersistentRedisPoolMaxActive *int + nonPersistentRedisMode *string + nonPersistentRedisTLSEnabled *bool + nonPersistentRedisTLSCACert *string + nonPersistentRedisTLSCert *string + nonPersistentRedisTLSKey *string + nonPersistentRedisTLSInsecureSkipVerify *bool + bigQueryDataSet *string + bigQueryDataLocation *string + domainTopic *string + bulkSegmentUsersReceivedTopic *string + accountServicePort *int + authServicePort *int + auditLogServicePort *int + autoOpsServicePort *int + environmentServicePort *int + eventCounterServicePort *int + experimentServicePort *int + featureServicePort *int + subscriptionServicePort *int + pushServicePort *int + dashboardServicePort *int + tagServicePort *int + codeReferenceServicePort *int + teamServicePort *int + insightsServicePort *int + notificationServicePort *int + prometheusURL *string + webGrpcGatewayPort *int + accountService *string + authService *string + batchService *string + environmentService *string + experimentService *string + featureService *string + autoOpsService *string + codeReferenceService *string + accessTokenTTL *time.Duration + refreshTokenTTL *time.Duration + emailFilter *string + oauthConfigPath *string + oauthPublicKeyPath *string + oauthPrivateKeyPath *string + webhookBaseURL *string + webhookKMSResourceName *string + cloudService *string + webConsoleEnvJSPath *string + pubSubType *string + pubSubRedisServerName *string + pubSubRedisAddr *string + pubSubRedisPoolSize *int + pubSubRedisMinIdle *int + pubSubRedisPartitionCount *int + pubSubRedisMode *string + pubSubRedisTLSEnabled *bool + pubSubRedisTLSCACert *string + pubSubRedisTLSCert *string + pubSubRedisTLSKey *string + pubSubRedisTLSInsecureSkipVerify *bool + dataWarehouseType *string + dataWarehouseConfigPath *string // AI Chat configuration openAIAPIKey *string openAIBaseURL *string @@ -347,6 +362,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { persistentRedisMode: cmd.Flag("persistent-redis-mode", "Persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + persistentRedisTLSEnabled: cmd.Flag( + "persistent-redis-tls-enabled", + "Enable TLS when connecting to the persistent Redis server.", + ).Default("false").Bool(), + persistentRedisTLSCACert: cmd.Flag( + "persistent-redis-tls-ca-cert", + "Path to the persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + persistentRedisTLSCert: cmd.Flag( + "persistent-redis-tls-cert", + "Path to the persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + persistentRedisTLSKey: cmd.Flag( + "persistent-redis-tls-key", + "Path to the persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + persistentRedisTLSInsecureSkipVerify: cmd.Flag( + "persistent-redis-tls-insecure-skip-verify", + "Skip persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), nonPersistentRedisServerName: cmd.Flag( "non-persistent-redis-server-name", "Name of the non-persistent redis.", @@ -366,6 +401,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { nonPersistentRedisMode: cmd.Flag("non-persistent-redis-mode", "Non-persistent Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + nonPersistentRedisTLSEnabled: cmd.Flag( + "non-persistent-redis-tls-enabled", + "Enable TLS when connecting to the non-persistent Redis server.", + ).Default("false").Bool(), + nonPersistentRedisTLSCACert: cmd.Flag( + "non-persistent-redis-tls-ca-cert", + "Path to the non-persistent Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + nonPersistentRedisTLSCert: cmd.Flag( + "non-persistent-redis-tls-cert", + "Path to the non-persistent Redis TLS client certificate file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSKey: cmd.Flag( + "non-persistent-redis-tls-key", + "Path to the non-persistent Redis TLS client private key file (for mutual TLS).", + ).String(), + nonPersistentRedisTLSInsecureSkipVerify: cmd.Flag( + "non-persistent-redis-tls-insecure-skip-verify", + "Skip non-persistent Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), bigQueryDataSet: cmd.Flag("bigquery-data-set", "BigQuery DataSet Name").String(), bigQueryDataLocation: cmd.Flag("bigquery-data-location", "BigQuery DataSet Location").String(), domainTopic: cmd.Flag( @@ -539,6 +594,26 @@ func RegisterCommand(r cli.CommandRegistry, p cli.ParentCommand) cli.Command { pubSubRedisMode: cmd.Flag("pubsub-redis-mode", "PubSub Redis client mode: cluster, standalone, or auto.", ).Default("auto").String(), + pubSubRedisTLSEnabled: cmd.Flag( + "pubsub-redis-tls-enabled", + "Enable TLS when connecting to the PubSub Redis server.", + ).Default("false").Bool(), + pubSubRedisTLSCACert: cmd.Flag( + "pubsub-redis-tls-ca-cert", + "Path to the PubSub Redis TLS CA certificate file. Uses the system CA pool if unset.", + ).String(), + pubSubRedisTLSCert: cmd.Flag( + "pubsub-redis-tls-cert", + "Path to the PubSub Redis TLS client certificate file (for mutual TLS).", + ).String(), + pubSubRedisTLSKey: cmd.Flag( + "pubsub-redis-tls-key", + "Path to the PubSub Redis TLS client private key file (for mutual TLS).", + ).String(), + pubSubRedisTLSInsecureSkipVerify: cmd.Flag( + "pubsub-redis-tls-insecure-skip-verify", + "Skip PubSub Redis server certificate verification. Not recommended for production.", + ).Default("false").Bool(), // AI Chat configuration (optional — disabled when openai-api-key is empty) openAIAPIKey: cmd.Flag( "openai-api-key", @@ -722,6 +797,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.persistentRedisPoolMaxIdle), redisv3.WithServerName(*s.persistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.persistentRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.persistentRedisTLSEnabled, + CACert: *s.persistentRedisTLSCACert, + Cert: *s.persistentRedisTLSCert, + Key: *s.persistentRedisTLSKey, + InsecureSkipVerify: *s.persistentRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -736,6 +818,13 @@ func (s *server) Run(ctx context.Context, metrics metrics.Metrics, logger *zap.L redisv3.WithMinIdleConns(*s.nonPersistentRedisPoolMaxIdle), redisv3.WithServerName(*s.nonPersistentRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.nonPersistentRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.nonPersistentRedisTLSEnabled, + CACert: *s.nonPersistentRedisTLSCACert, + Cert: *s.nonPersistentRedisTLSCert, + Key: *s.nonPersistentRedisTLSKey, + InsecureSkipVerify: *s.nonPersistentRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), ) @@ -1460,6 +1549,13 @@ func (s *server) createPublisher( redisv3.WithMinIdleConns(*s.pubSubRedisMinIdle), redisv3.WithServerName(*s.pubSubRedisServerName), redisv3.WithRedisMode(redisv3.RedisMode(*s.pubSubRedisMode)), + redisv3.WithTLS(redisv3.TLSConfig{ + Enabled: *s.pubSubRedisTLSEnabled, + CACert: *s.pubSubRedisTLSCACert, + Cert: *s.pubSubRedisTLSCert, + Key: *s.pubSubRedisTLSKey, + InsecureSkipVerify: *s.pubSubRedisTLSInsecureSkipVerify, + }), redisv3.WithMetrics(registerer), redisv3.WithLogger(logger), )