Skip to content

UN-4123 [FEAT] Support TLS to Redis, and keep pooled/idle connections healthy - #2287

Open
muhammad-ali-e wants to merge 1 commit into
mainfrom
UN-4123-redis-tls
Open

muhammad-ali-e wants to merge 1 commit into
mainfrom
UN-4123-redis-tls

Conversation

@muhammad-ali-e

Copy link
Copy Markdown
Contributor

What

Makes an encrypted connection to Redis possible, so the platform can run against a managed endpoint (Memorystore, ElastiCache, Azure Cache — the last of which disables its non-TLS port by default).

The scheme is the switch. {prefix}URL (falling back to REDIS_URL) goes to redis.Redis.from_url, and rediss:// selects TLS on its own — there is no separate "use TLS" flag to forget, and redis:// behaves exactly as today. Discrete host/port vars stay the primary path: they need no percent-encoding, and they are what the Helm chart, every sample.env, and the non-Python services (api-hub, llm-whisperer) read.

Also in scope, because they are the same class of silent failure:

  • {prefix}SSL now falls back to REDIS_SSL, plus a CA-certificate option.
  • The Django cache and the Socket.IO/kombu manager can finally use TLS (both hardcoded redis://).
  • The tool-sidecar and tool-container env allowlists carry TLS settings and REDIS_DB.
  • health_check_interval defaults to 30s.

Why

Password-only auth to an external Redis already worked, so TLS was the missing half. Chart-side support for pointing at an external endpoint is UN-4122 (cloud repo); this is the OSS half.

Everything is additive and inert by default — with nothing configured, the local/in-cluster path builds exactly the client it built before.

Two redis-py behaviours bite silently, and both are now handled and pinned by tests:

  1. The URL path beats a db= kwarg. from_url('rediss://h:6380/5', db=1) yields db 5. sdk1 metrics asks for db=1 explicitly, so a URL carrying a path would have moved its keys into another service's keyspace with nothing to indicate it. The path is stripped when an override is given.
  2. ssl=True into a ConnectionPool does not fail at construction. The pool defers its kwargs to the connection class, so platform-service (max_connections=10) started healthy, kept the plain Connection class, and raised TypeError: AbstractConnection.__init__() got an unexpected keyword argument 'ssl' on its first command. Pooled TLS now selects SSLConnection.

Three more silent failures fixed:

  • A forgotten per-prefix SSL flag is a plaintext client dialling a TLS port. CACHE_REDIS_SSL and MANUAL_REVIEW_REDIS_SSL had to be set separately; they now inherit REDIS_SSL and can still override it.
  • django-redis 5.4.0 ignores DB and USERNAME from OPTIONS. Verified against the installed version — ConnectionFactory.make_connection_params reads only PASSWORD and the two timeouts; DB: 3 yields db=None, while redis://h:6379/3 yields db=3. The db now travels in the LOCATION URL, so the backend cache stops sitting on db 0 while every other service honours REDIS_DB: with REDIS_DB=N, workers RPUSH log_history_queue to db N and the backend LPOPs an empty db 0. USERNAME is deliberately not restored — auth stays password-only as the built-in default user, which is what a managed AUTH string is; named ACL users cannot work platform-wide while django-redis discards the username.
  • kombu reads TLS off the scheme but defaults ssl_cert_reqs to CERT_NONE — encrypted while accepting any certificate. The Socket.IO manager URL now carries an explicit ssl_cert_reqs, since KombuManager takes a URL rather than connection kwargs.

How

# either
REDIS_SSL=true
REDIS_SSL_CERT_REQS=required
REDIS_SSL_CA_CERTS=/etc/ssl/redis-ca.pem   # only where the CA isn't publicly trusted (Memorystore)

# or
REDIS_URL=rediss://:<password>@<host>:6380/0?ssl_cert_reqs=required

health_check_interval defaults to 30s ({prefix}HEALTH_CHECK_INTERVAL, 0 disables). Only the two worker caches set it before, so a connection killed while parked — managed failover, or Azure Cache's 10-minute idle reaper — was discovered by a real command failing on it. This applies with or without TLS.

Retries are deliberately not enabled globally. retry_on_timeout would re-issue blocking BLPOP/BLMOVE calls whose reply was lost, risking consuming a second message rather than recovering the first — the trap already documented at pg_queue/result_backend.py:152.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why.

No. Every new setting is opt-in and the default path is unchanged — the first test in the new suite pins plaintext/localhost/db0 with no SSL kwargs.

Two intentional behaviour changes on the existing path, both called out for review:

  1. health_check_interval now defaults to 30s instead of 0. Effect: redis-py sends a PING before reusing a connection idle longer than that. Set REDIS_HEALTH_CHECK_INTERVAL=0 to restore the old behaviour.
  2. The Django cache LOCATION now carries the db path. For REDIS_DB unset or 0 — every shipped config — the connection is identical. Where REDIS_DB=N, the backend cache moves from db 0 to db N, which is the fix described above; that deployment is currently split-brained with its own workers.

Sidecar/tool env keys are forwarded only when set, so an unconfigured deployment sees no new variables.

Database Migrations

None.

Env Config

All optional, all defaulting to current behaviour: REDIS_URL / {prefix}URL, REDIS_SSL, REDIS_SSL_CERT_REQS, REDIS_SSL_CA_CERTS, REDIS_HEALTH_CHECK_INTERVAL. Documented in backend/, runner/, platform-service/ and workers/ sample.env.

Relevant Docs

Module docstring in unstract/core/.../cache/redis_client.py covers URL-vs-discrete precedence and why discrete stays primary.

Related Issues or PRs

UN-4123. Pairs with UN-4122 (cloud chart: external/managed Redis endpoint).

Not covered here, tracked separately: api-hub builds a credential-free redis:// URL and needs a one-line fix before AUTH is enabled anywhere, and llm-whisperer supports a password but has no TLS support. Both live in their own repos.

Dependencies Versions

No changes. Behaviour verified against the pinned redis-py 5.2.1, kombu 5.5.4, django-redis 5.4.0.

Notes on Testing

  • 21 new tests in unstract/core/tests/test_redis_client_config.py — defaults, discrete TLS (including the pooled regression), URL mode (scheme, percent-decoded password, db precedence, per-prefix URLs), and password-only auth.
  • 5 added to runner/tests/test_sidecar_log_transport.py, beside the existing LOG_TRANSPORT ones.
  • Suites green: core 136, runner 10, Redis-related workers 51.
  • Django settings verified by rendering both modes: plaintext → redis://localhost:6379/0 with no pool kwargs; TLS → rediss://cache.example:6380/3 with ssl_cert_reqs/ssl_ca_certs, and the Socket.IO URL gaining ?ssl_cert_reqs=required.
  • Not done: a live run against a TLS-enabled Redis. Every assertion here is about the client that gets constructed, which is where the bugs were — but the handshake itself is unproven. Worth one integration run with REDIS_SSL=true before relying on it in production, including a container-based tool so the sidecar path is exercised.

🤖 Generated with Claude Code

… healthy

Makes an encrypted connection to Redis possible so the platform can run against a
managed endpoint (Memorystore / ElastiCache / Azure Cache, which disables its
non-TLS port by default). Chart-side support for pointing at an external Redis is
UN-4122; password-only auth already worked, so what was missing was TLS.

Everything here is additive and inert by default: with nothing configured, the
local/in-cluster path builds exactly the client it built before.

**The scheme is the switch.** `{prefix}URL` (falling back to REDIS_URL) is handed
to redis.Redis.from_url, and `rediss://` selects TLS on its own — no separate
"use TLS" flag to forget, and `redis://` behaves as today. Discrete host/port vars
remain the primary path: they need no percent-encoding, and they are what the Helm
chart, every sample.env and the non-Python services (api-hub, llm-whisperer) read.

Two redis-py behaviours that bite silently, both handled and pinned by tests:

  * The URL path beats a `db=` kwarg. sdk1 metrics asks for db=1 explicitly, so a
    URL ending in /5 would have moved its keys into another service's keyspace
    with nothing to show for it. The path is stripped when an override is given.
  * `ssl=True` into a ConnectionPool does NOT fail at construction — the pool
    defers kwargs to the connection class, so platform-service (max_connections=10)
    started healthy, kept the PLAIN Connection class, and raised
    `TypeError: AbstractConnection.__init__() got an unexpected keyword argument
    'ssl'` on its first command. Pooled TLS now selects SSLConnection instead.

Also fixed, because they are the same class of silent failure:

  * `{prefix}SSL` falls back to REDIS_SSL. Enabling TLS platform-wide previously
    meant remembering CACHE_REDIS_SSL and MANUAL_REVIEW_REDIS_SSL too, and a
    forgotten one is a plaintext client dialling a TLS port.
  * django-redis 5.4.0 ignores both DB and USERNAME from OPTIONS (verified against
    the installed 5.4.0: make_connection_params reads only PASSWORD and timeouts).
    The db now travels in the LOCATION URL, so the backend cache stops sitting on
    db 0 while every other service honours REDIS_DB — with REDIS_DB=N the workers
    RPUSH log_history_queue to db N and the backend LPOPs an empty db 0. USERNAME
    is deliberately NOT restored: auth stays password-only as the built-in
    `default` user, which is what a managed AUTH string is.
  * kombu reads TLS off the scheme but defaults ssl_cert_reqs to CERT_NONE —
    encrypted while accepting any certificate. Socket.IO's manager URL carries an
    explicit ssl_cert_reqs, since KombuManager takes a URL, not kwargs.
  * The sidecar and tool-container environments are hand-picked allowlists (the
    trap that made the LOG_TRANSPORT fix necessary). TLS settings and REDIS_DB now
    reach both, and only when actually set — an empty string reads as "configured"
    to os.getenv and would suppress the fallback.

health_check_interval now defaults to 30s, configurable via
{prefix}HEALTH_CHECK_INTERVAL. Only the two worker caches set it before, so a
connection killed while parked — managed failover, or Azure Cache's 10-minute idle
reaper — was discovered by a real command failing. This is the one intentional
behaviour change on the existing path, and it applies with or without TLS.

Retries are deliberately NOT enabled globally: retry_on_timeout would re-issue
blocking BLPOP/BLMOVE calls whose reply was lost, which risks consuming a second
message rather than recovering the first.

Tests: 21 new in unstract/core/tests/test_redis_client_config.py, 5 added to the
runner sidecar suite. Core 136, runner 10, Redis-related worker tests 51 — green.
Django settings verified by rendering both modes: plaintext yields
redis://host:6379/0 with no pool kwargs; TLS yields rediss://…/3 plus
ssl_cert_reqs/ssl_ca_certs, and the Socket.IO URL gains ?ssl_cert_reqs=required.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@greptile-apps

greptile-apps Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

via Greptile

RetriggerConfidence Score: 1/5

The PR is not safe to merge because documented Redis URL and TLS configurations leave key backend and worker consumers connected to the wrong endpoint or transport.

Fix All in Claude CodeFindings

  1. P1 Redis URL Is Ignored
  2. P1 URL TLS Drops Custom CA
  3. P1 Worker Publisher Remains Plaintext
  4. P1 Socket.IO Omits Custom CA
Fix with agent prompt
### Issue 1
backend/backend/settings/base.py:574-584
When an operator configures only `REDIS_URL` as documented, this branch still builds the Django cache and Socket.IO connections from `REDIS_HOST`, `REDIS_PORT`, and `REDIS_SSL`. Shared clients created through `create_redis_client` do honor `REDIS_URL`, so the backend can connect its Redis consumers to different endpoints. This breaks cache, session, and WebSocket behavior or splits queues across Redis instances.

### Issue 2
unstract/core/src/unstract/core/cache/redis_client.py:181-190
If a deployment uses the documented `REDIS_URL=rediss://...` mode with `REDIS_SSL_CA_CERTS`, but does not also set the redundant `REDIS_SSL=true` flag, this block never resolves the CA certificate. The CA is therefore not passed to `Redis.from_url`, and certificate verification fails for private-CA endpoints such as Memorystore.

### Issue 3
backend/backend/settings/base.py:574-584
Enabling `REDIS_SSL` changes the backend Socket.IO manager to `rediss://`, but `workers/log_consumer/tasks.py` still builds the write-only Kombu manager with a hardcoded `redis://` URL. Against a TLS-only Redis endpoint, the worker cannot publish to the channel used by the backend, so execution-log events no longer reach connected clients.

### Issue 4
backend/backend/settings/base.py:581-584
If the Redis server uses a CA outside the system trust store, this URL forwards `ssl_cert_reqs` but omits `REDIS_SSL_CA_CERTS`. The Django Redis pool immediately below receives the custom CA, so cache access can succeed while the Kombu manager cannot verify the same server, disabling WebSocket event delivery.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Summary

This PR adds URL- and flag-driven Redis TLS configuration, custom-CA support, connection health checks, database propagation, and Redis settings for spawned tool and sidecar containers. The shared client and container forwarding are broadly structured around the intended configuration model, but several backend and URL-mode paths do not apply that model consistently.

  • Adds rediss:// support and TLS connection classes to the shared Redis client factory.
  • Defaults pooled connection health checks to 30 seconds.
  • Moves the Django cache database into its URL and configures TLS pool parameters.
  • Forwards Redis TLS, URL, CA, and database variables into tool-sidecar and tool-container environments.
  • Leaves backend URL mode, custom-CA Socket.IO configuration, and the worker-side Kombu publisher inconsistent with the new TLS behavior.
Diagram
%%{init: {'theme': 'neutral'}}%%
flowchart TD
    ENV[Redis environment configuration]
    ENV --> SHARED[Shared create_redis_client]
    ENV --> DJANGO[Django cache settings]
    ENV --> BACKENDSO[Backend Socket.IO Kombu manager]
    ENV --> WORKERSO[Log-consumer Kombu manager]
    SHARED --> URLCLIENT[redis-py URL or discrete client]
    DJANGO --> CACHE[(Redis cache)]
    BACKENDSO --> CHANNEL[(Socket.IO Redis channel)]
    WORKERSO --> CHANNEL
    URLCLIENT --> REDIS[(Configured Redis)]
    CACHE --> REDIS
    CHANNEL --> REDIS
    ENV -. REDIS_URL ignored .-> DJANGO
    ENV -. REDIS_URL and custom CA incomplete .-> BACKENDSO
    ENV -. REDIS_SSL ignored .-> WORKERSO
Loading

Reviews (1) · Last reviewed commit: "UN-4123 [FEAT] Support TLS to Redis, and..."

Comment on lines +574 to +584
_scheme = "rediss" if REDIS_SSL else "redis"
_cache_db = int(REDIS_DB) if REDIS_DB else 0

# kombu reads TLS off the scheme, but defaults ssl_cert_reqs to CERT_NONE —
# encrypted while accepting ANY certificate, which is not what "TLS" is meant
# to buy. The query parameter is the only way to say otherwise here, since
# KombuManager takes a URL rather than connection kwargs.
_socketio_tls_query = f"?ssl_cert_reqs={REDIS_SSL_CERT_REQS}" if REDIS_SSL else ""
SOCKET_IO_MANAGER_URL = (
f"{_scheme}://{_cred_prefix}{REDIS_HOST}:{REDIS_PORT}{_socketio_tls_query}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Redis URL Is Ignored

When an operator configures only REDIS_URL as documented, this branch still builds the Django cache and Socket.IO connections from REDIS_HOST, REDIS_PORT, and REDIS_SSL. Shared clients created through create_redis_client do honor REDIS_URL, so the backend can connect its Redis consumers to different endpoints. This breaks cache, session, and WebSocket behavior or splits queues across Redis instances.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/backend/settings/base.py
Line: 574-584

Comment:
**Redis URL Is Ignored**

When an operator configures only `REDIS_URL` as documented, this branch still builds the Django cache and Socket.IO connections from `REDIS_HOST`, `REDIS_PORT`, and `REDIS_SSL`. Shared clients created through `create_redis_client` do honor `REDIS_URL`, so the backend can connect its Redis consumers to different endpoints. This breaks cache, session, and WebSocket behavior or splits queues across Redis instances.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines 181 to +190
if ssl:
result["ssl_cert_reqs"] = os.getenv(f"{env_prefix}SSL_CERT_REQS", "required")
# Needed where the server's CA is not in the system trust store — notably
# Memorystore, whose CA is Google-managed. ElastiCache and Azure chain to
# public CAs and need nothing here.
ca_certs = os.getenv(
f"{env_prefix}SSL_CA_CERTS", os.getenv("REDIS_SSL_CA_CERTS", "")
).strip()
if ca_certs:
result["ssl_ca_certs"] = ca_certs

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 URL TLS Drops Custom CA

If a deployment uses the documented REDIS_URL=rediss://... mode with REDIS_SSL_CA_CERTS, but does not also set the redundant REDIS_SSL=true flag, this block never resolves the CA certificate. The CA is therefore not passed to Redis.from_url, and certificate verification fails for private-CA endpoints such as Memorystore.

Prompt To Fix With AI
This is a comment left during a code review.
Path: unstract/core/src/unstract/core/cache/redis_client.py
Line: 181-190

Comment:
**URL TLS Drops Custom CA**

If a deployment uses the documented `REDIS_URL=rediss://...` mode with `REDIS_SSL_CA_CERTS`, but does not also set the redundant `REDIS_SSL=true` flag, this block never resolves the CA certificate. The CA is therefore not passed to `Redis.from_url`, and certificate verification fails for private-CA endpoints such as Memorystore.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines +574 to +584
_scheme = "rediss" if REDIS_SSL else "redis"
_cache_db = int(REDIS_DB) if REDIS_DB else 0

# kombu reads TLS off the scheme, but defaults ssl_cert_reqs to CERT_NONE —
# encrypted while accepting ANY certificate, which is not what "TLS" is meant
# to buy. The query parameter is the only way to say otherwise here, since
# KombuManager takes a URL rather than connection kwargs.
_socketio_tls_query = f"?ssl_cert_reqs={REDIS_SSL_CERT_REQS}" if REDIS_SSL else ""
SOCKET_IO_MANAGER_URL = (
f"{_scheme}://{_cred_prefix}{REDIS_HOST}:{REDIS_PORT}{_socketio_tls_query}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Worker Publisher Remains Plaintext

Enabling REDIS_SSL changes the backend Socket.IO manager to rediss://, but workers/log_consumer/tasks.py still builds the write-only Kombu manager with a hardcoded redis:// URL. Against a TLS-only Redis endpoint, the worker cannot publish to the channel used by the backend, so execution-log events no longer reach connected clients.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/backend/settings/base.py
Line: 574-584

Comment:
**Worker Publisher Remains Plaintext**

Enabling `REDIS_SSL` changes the backend Socket.IO manager to `rediss://`, but `workers/log_consumer/tasks.py` still builds the write-only Kombu manager with a hardcoded `redis://` URL. Against a TLS-only Redis endpoint, the worker cannot publish to the channel used by the backend, so execution-log events no longer reach connected clients.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

Comment on lines +581 to +584
_socketio_tls_query = f"?ssl_cert_reqs={REDIS_SSL_CERT_REQS}" if REDIS_SSL else ""
SOCKET_IO_MANAGER_URL = (
f"{_scheme}://{_cred_prefix}{REDIS_HOST}:{REDIS_PORT}{_socketio_tls_query}"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Socket.IO Omits Custom CA

If the Redis server uses a CA outside the system trust store, this URL forwards ssl_cert_reqs but omits REDIS_SSL_CA_CERTS. The Django Redis pool immediately below receives the custom CA, so cache access can succeed while the Kombu manager cannot verify the same server, disabling WebSocket event delivery.

Prompt To Fix With AI
This is a comment left during a code review.
Path: backend/backend/settings/base.py
Line: 581-584

Comment:
**Socket.IO Omits Custom CA**

If the Redis server uses a CA outside the system trust store, this URL forwards `ssl_cert_reqs` but omits `REDIS_SSL_CA_CERTS`. The Django Redis pool immediately below receives the custom CA, so cache access can succeed while the Kombu manager cannot verify the same server, disabling WebSocket event delivery.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code

@github-actions

Copy link
Copy Markdown
Contributor

Unstract test results

Per-group results

Status Group Tier Passed Failed Errors Skipped Duration (s)
e2e-api-deployment e2e 3 0 0 0 22.3
e2e-coowners e2e 1 0 0 0 1.3
e2e-etl e2e 1 0 0 0 14.5
e2e-login e2e 2 0 0 0 1.3
e2e-prompt-studio e2e 1 0 0 0 8.8
e2e-smoke e2e 2 0 0 0 2.4
e2e-workflow e2e 1 0 0 0 18.3
frontend unit 0 1 0 0 0.0
integration-backend integration 598 0 0 26 59.6
integration-connectors integration 1 0 0 7 8.4
integration-workers integration 157 0 0 1 52.8
ui e2e 0 1 0 0 0.0
unit-backend unit 1276 0 0 1 45.9
unit-connectors unit 63 0 0 0 10.0
unit-core unit 136 0 0 0 2.5
unit-platform-service unit 15 0 0 0 2.6
unit-rig unit 120 0 0 0 4.7
unit-runner unit 10 0 0 0 3.0
unit-sdk1 unit 563 0 0 0 29.6
unit-workers unit 1425 0 0 1 124.9
TOTAL 4375 2 0 36 412.9

Critical paths

⚠️ Critical paths not yet covered

  • workflow-execution-fan-out — Multi-file workflow execution fans out to file-processing workers and rejoins. (declared coverage: no groups declared)
✅ Covered critical paths
  • auth-login — covered by e2e-login
  • adapter-register-llm — covered by integration-backend
  • workflow-author — covered by integration-backend
  • co-owner-manage — covered by integration-backend, e2e-coowners
  • workflow-create-execute — covered by e2e-workflow
  • api-deployment-provision — covered by integration-backend
  • api-deployment-auth — covered by integration-backend
  • api-deployment-run — covered by e2e-api-deployment
  • mcp-server-auth — covered by integration-backend
  • mcp-platform-auth — covered by integration-backend
  • platform-key-whoami — covered by integration-backend
  • prompt-studio-author — covered by integration-backend
  • prompt-studio-fetch-response — covered by e2e-prompt-studio
  • connector-register-test — covered by integration-backend
  • pipeline-etl-execute — covered by e2e-etl
  • usage-aggregate-read — covered by integration-backend
  • usage-token-tracking — covered by e2e-api-deployment
  • callback-result-delivery — covered by e2e-api-deployment

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant