From 1b943b20bfa21f1fbb25ff02d19c849a26208ccc Mon Sep 17 00:00:00 2001 From: Hongwei Date: Fri, 17 Jul 2026 10:45:23 +0200 Subject: [PATCH 1/2] fix(cache): route the memoize cache through the authenticated Redis pool The memoize-backed cache (memoizeWithRedis / memoizeSyncWithRedis, the backing store for Caching.memoize*WithProvider) was built with RedisCache(url, port), which constructs its own internal JedisPool that ignores cache.redis.password and redis.use.ssl. Every other Redis path already leases from the configured jedisPool, which passes both. With requirepass enabled on Redis, the memoize path failed with NOAUTH while the startup health check (which uses jedisPool) still reported PASSED, so the main caching layer went dead with clean-looking logs. Reuse the already authenticated, optionally SSL-configured jedisPool for the memoize store too; the RedisCache(JedisPool, Option[ClassLoader]) overload resolves with its default second argument. Also document cache.redis.password and redis.use.ssl in the Redis block of sample.props.template, and note that the 127.0.0.1 default is a deliberate security default (Redis must not be bound to a public interface). --- obp-api/src/main/resources/props/sample.props.template | 4 ++++ obp-api/src/main/scala/code/api/cache/Redis.scala | 6 +++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/obp-api/src/main/resources/props/sample.props.template b/obp-api/src/main/resources/props/sample.props.template index 12356d2cb6..c5ac75d4c4 100644 --- a/obp-api/src/main/resources/props/sample.props.template +++ b/obp-api/src/main/resources/props/sample.props.template @@ -1085,10 +1085,14 @@ featured_apis=elasticSearchWarehouseV300 ## Note: You do NOT need to include anything here for this to work. # -- Redis cache ------------------------------------- +# The 127.0.0.1 default is a deliberate security default: Redis must not be bound to a +# public interface. If you enable authentication (requirepass) or TLS on Redis, set the +# properties below - they are honoured by both the direct and the memoize-backed cache paths. # cache.redis.url=127.0.0.1 # cache.redis.port=6379 # Default value is empty or omitted props # cache.redis.password = +# redis.use.ssl=false # --------------------------------------------------------- # -- New Style Endpoints ----------------------- diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 74c37e69bd..2f85490eee 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -290,7 +290,11 @@ object Redis extends MdcLoggable { } } - implicit val scalaCache = ScalaCache(RedisCache(url, port)) + // Reuse the pool built above so the memoize-backed cache shares the same authenticated, + // optionally SSL-configured connection. The RedisCache(url, port) overload builds its own + // JedisPool internally with no password and no SSL, so with `requirepass` enabled it fails + // with NOAUTH while the jedisPool-based paths keep working. + implicit val scalaCache = ScalaCache(RedisCache(jedisPool)) implicit val flags = Flags(readsEnabled = true, writesEnabled = true) implicit def anyToByte[T](implicit m: Manifest[T]) = new Codec[T, Array[Byte]] { From 791abd81e1d1cffb48bc741dc622800932cd3a94 Mon Sep 17 00:00:00 2001 From: Hongwei Date: Fri, 17 Jul 2026 11:06:21 +0200 Subject: [PATCH 2/2] fix(cache): honour redis.use.ssl on pub/sub subscriber connections The chat, log-cache and metrics event buses each built their subscriber connection with the plain Jedis(url, port, timeout) constructor, which is unencrypted. They authenticated explicitly, so cache.redis.password was applied, but redis.use.ssl was silently ignored: with TLS enabled the pool and the memoize cache spoke TLS while these three streaming buses tried to connect in plaintext, so they either failed to connect or would have sent data unencrypted on a link the operator configured as TLS-only. Subscribers genuinely cannot lease from jedisPool, since subscribe/psubscribe occupy their connection for the life of the subscription. Rather than let each call site reassemble the connection settings, add Redis.newSubscriberConnection() which applies the same password and TLS configuration the pool uses, and have the three buses call it. The SSLContext becomes a lazy val so the keystore and truststore are read once and only when redis.use.ssl is on. Verified against a TLS-only, requirepass-protected local Redis: the pre-fix plaintext construction fails with a connection reset, while a subscriber from newSubscriberConnection() authenticates and completes a pub/sub round trip. The no-SSL path is unchanged and still exercised by the existing cache tests. --- .../src/main/scala/code/api/cache/Redis.scala | 21 ++++++++++++++++++- .../main/scala/code/chat/ChatEventBus.scala | 3 +-- .../code/logcache/LogCacheEventBus.scala | 3 +-- .../code/metricsstream/MetricsEventBus.scala | 3 +-- 4 files changed, 23 insertions(+), 7 deletions(-) diff --git a/obp-api/src/main/scala/code/api/cache/Redis.scala b/obp-api/src/main/scala/code/api/cache/Redis.scala index 2f85490eee..99ef445cce 100644 --- a/obp-api/src/main/scala/code/api/cache/Redis.scala +++ b/obp-api/src/main/scala/code/api/cache/Redis.scala @@ -46,16 +46,35 @@ object Redis extends MdcLoggable { poolConfig.setNumTestsPerEvictionRun(3) poolConfig.setBlockWhenExhausted(true) + // Lazy so the keystore/truststore files are only read when redis.use.ssl is on, and only once + // even though both jedisPool and every subscriber connection need the socket factory. + private lazy val sslContext: SSLContext = configureSslContext() + val jedisPool = if (useSsl) { // SSL connection: Use SSLContext with JedisPool - val sslContext = configureSslContext() new JedisPool(poolConfig, url, port, timeout, password, true, sslContext.getSocketFactory, null, null) } else { // Non-SSL connection new JedisPool(poolConfig, url, port, timeout, password) } + /** + * Build a dedicated, non-pooled connection for a pub/sub subscriber. + * + * `subscribe`/`psubscribe` occupy their connection for the whole life of the subscription, + * so subscribers cannot lease from jedisPool. They must still apply the same password and + * TLS settings the pool uses: the plain `Jedis(url, port, timeout)` constructor is + * unencrypted, so building one directly silently ignores redis.use.ssl. + */ + def newSubscriberConnection(): Jedis = { + val jedis = + if (useSsl) new Jedis(url, port, timeout, true, sslContext.getSocketFactory, null, null) + else new Jedis(url, port, timeout) + if (password != null) jedis.auth(password) + jedis + } + // Redis startup health check private def performStartupHealthCheck(): Unit = { try { diff --git a/obp-api/src/main/scala/code/chat/ChatEventBus.scala b/obp-api/src/main/scala/code/chat/ChatEventBus.scala index ab799f3a7f..cbac73654d 100644 --- a/obp-api/src/main/scala/code/chat/ChatEventBus.scala +++ b/obp-api/src/main/scala/code/chat/ChatEventBus.scala @@ -123,8 +123,7 @@ object ChatEventBus extends MdcLoggable { subscriberThread = new Thread(() => { try { // Dedicated connection for the subscriber (not from the pool) - subscriberJedis = new Jedis(Redis.url, Redis.port, Redis.timeout) - if (Redis.password != null) subscriberJedis.auth(Redis.password) + subscriberJedis = Redis.newSubscriberConnection() logger.info(s"ChatEventBus says: Redis subscriber started, pattern-subscribing to ${CHANNEL_PREFIX}*") subscriberJedis.psubscribe(pubSub, s"${CHANNEL_PREFIX}*") } catch { diff --git a/obp-api/src/main/scala/code/logcache/LogCacheEventBus.scala b/obp-api/src/main/scala/code/logcache/LogCacheEventBus.scala index 8e5ff1844c..de12f9a35f 100644 --- a/obp-api/src/main/scala/code/logcache/LogCacheEventBus.scala +++ b/obp-api/src/main/scala/code/logcache/LogCacheEventBus.scala @@ -107,8 +107,7 @@ object LogCacheEventBus extends MdcLoggable { subscriberThread = new Thread(() => { try { - subscriberJedis = new Jedis(Redis.url, Redis.port, Redis.timeout) - if (Redis.password != null) subscriberJedis.auth(Redis.password) + subscriberJedis = Redis.newSubscriberConnection() logger.info(s"LogCacheEventBus says: Redis subscriber started, pattern-subscribing to ${CHANNEL_PREFIX}*") subscriberJedis.psubscribe(pubSub, s"${CHANNEL_PREFIX}*") } catch { diff --git a/obp-api/src/main/scala/code/metricsstream/MetricsEventBus.scala b/obp-api/src/main/scala/code/metricsstream/MetricsEventBus.scala index 42255ccd32..41a52df5a0 100644 --- a/obp-api/src/main/scala/code/metricsstream/MetricsEventBus.scala +++ b/obp-api/src/main/scala/code/metricsstream/MetricsEventBus.scala @@ -102,8 +102,7 @@ object MetricsEventBus extends MdcLoggable { subscriberThread = new Thread(() => { try { - subscriberJedis = new Jedis(Redis.url, Redis.port, Redis.timeout) - if (Redis.password != null) subscriberJedis.auth(Redis.password) + subscriberJedis = Redis.newSubscriberConnection() logger.info(s"MetricsEventBus says: Redis subscriber started, subscribing to $ALL_CHANNEL") subscriberJedis.subscribe(pubSub, ALL_CHANNEL) } catch {