From fdbb393ec2daf77baa326abf5c1c3fd2f58522af Mon Sep 17 00:00:00 2001 From: Jeffrey Rohlman Date: Wed, 9 Sep 2026 13:23:28 -0500 Subject: [PATCH 1/4] fix(redis): decouple TLS from auth so on-prem can require a password On-prem Redis is plaintext but still requires a credential. The Redis helpers treated the presence of REDIS_AUTH_TOKEN as proof of in-transit encryption, so any attempt to force auth on-prem produced a rediss:// handshake against a plaintext server. Meanwhile the on-prem config paths never applied the token at all, and cache_redis_host_port only stripped credentials from rediss:// URLs. core/celery/app.py: - Add redis_tls_enabled(), driven by REDIS_ENABLE_TLS. When unset it falls back to bool(REDIS_AUTH_TOKEN), preserving today's ElastiCache behaviour. - Extract build_redis_url() so scheme and credential are chosen independently, and route get_redis_endpoint() through it. - get_redis_instance() applies password and ssl=True separately. common/config.py: - Add _apply_redis_auth_token(), applied to all three on-prem branches of cache_redis_url including the early cache_redis_onprem_url return. This keeps the password out of Helm values: the chart names host and db index, the app supplies the credential from its secret-backed env var. URLs that already carry userinfo are left alone. - Make cache_redis_host_port scheme-agnostic. It feeds the KEDA scaler's address metadata, so a password surviving in it would both break the address and leak into the ScaledObject. Passwords are now percent-encoded. kombu and redis-py both unquote userinfo, so the wire-level password is unchanged for AWS, while tokens containing @, / or # stop corrupting URL parsing: unencoded, a '#' truncates the URL at the fragment and both parsers then raise ValueError on the mangled port. Beyond the minimum fix: - get_async_redis_instance() and RedisBroker._init_client() built passwordless URLs on every cloud, so both would fail NOAUTH against an authenticated broker. Both now use build_redis_url(). Chart: - Extract the Redis credential env into a shared modelEngine.redisAuthEnv helper and include it from both the gateway/builder/cacher env and the celery autoscaler StatefulSet. The autoscaler had no REDIS_AUTH_TOKEN at all, so fixing RedisBroker._init_client() alone would have left it connecting anonymously. - Emit REDIS_ENABLE_TLS from the same redis.enableTLS value that feeds the KEDA scaler, so chart and app cannot drift. The gate forwards any set value, including a stringified bool from a desired-state value override; gating on a real bool would have silently dropped "false" and left the app inferring TLS while the scaler ran plaintext. - redis.enableTLS now defaults to unset rather than false. Emitting it unconditionally would have sent REDIS_ENABLE_TLS=false to every install, silently downgrading an ElastiCache deployment with an auth token from TLS to plaintext. The scaler gets `| default false` so its rendered output is byte-identical to before when the value is unset. Verified: ruff 0.6.8, black 24.8.0 and helm lint clean; app env and scaler metadata agree for bool, stringified and unset enableTLS, and the unset render is byte-identical to before. Note that tests/unit/conftest.py needs fastapi, which is not installed locally, so the new tests were exercised against the committed function bodies via an import shim (22/22 pass, and the 4 assertions covering cache_redis_host_port and get_redis_instance fail against the pre-fix bodies) rather than in-tree -- they still need one CI run to confirm the import path. Co-Authored-By: Claude Opus 5 (1M context) --- charts/model-engine/templates/_helpers.tpl | 33 ++-- .../celery_autoscaler_stateful_set.yaml | 1 + .../service_template_config_map.yaml | 2 +- charts/model-engine/values.yaml | 5 +- .../model_engine_server/common/config.py | 35 +++- .../core/celery/__init__.py | 2 + .../model_engine_server/core/celery/app.py | 53 ++++-- .../core/celery/celery_autoscaler.py | 3 +- .../tests/unit/common/test_redis_auth.py | 152 ++++++++++++++++++ 9 files changed, 251 insertions(+), 35 deletions(-) create mode 100644 model-engine/tests/unit/common/test_redis_auth.py diff --git a/charts/model-engine/templates/_helpers.tpl b/charts/model-engine/templates/_helpers.tpl index 18935711c..d45453807 100644 --- a/charts/model-engine/templates/_helpers.tpl +++ b/charts/model-engine/templates/_helpers.tpl @@ -290,6 +290,28 @@ env: {{- end }} {{- end }} +{{- /* Every workload that opens a Redis connection must include this, or it + reaches an authenticated broker with no credential and fails NOAUTH. */}} +{{- define "modelEngine.redisAuthEnv" }} + {{- if .Values.redis.authSecretName }} + - name: REDIS_AUTH_TOKEN + valueFrom: + secretKeyRef: + name: {{ .Values.redis.authSecretName }} + key: {{ .Values.redis.authSecretKey | default "auth_token" }} + {{- else if .Values.redis.auth }} + - name: REDIS_AUTH_TOKEN + value: {{ .Values.redis.auth }} + {{- end }} + {{- /* Same value drives the KEDA scaler's enableTLS so chart and app cannot + drift. Any set value is forwarded, including a stringified bool from a + value override; only an unset value leaves the app's inference intact. */}} + {{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) }} + - name: REDIS_ENABLE_TLS + value: {{ .Values.redis.enableTLS | quote }} + {{- end }} +{{- end }} + {{- define "modelEngine.serviceEnvBase" }} env: - name: DD_TRACE_ENABLED @@ -379,16 +401,7 @@ env: - name: CELERY_RESULT_BACKEND value: {{ .Values.celeryResultBackend | quote }} {{- end }} - {{- if .Values.redis.authSecretName }} - - name: REDIS_AUTH_TOKEN - valueFrom: - secretKeyRef: - name: {{ .Values.redis.authSecretName }} - key: {{ .Values.redis.authSecretKey | default "auth_token" }} - {{- else if .Values.redis.auth }} - - name: REDIS_AUTH_TOKEN - value: {{ .Values.redis.auth }} - {{- end }} + {{- include "modelEngine.redisAuthEnv" . }} {{- if .Values.azure}} - name: AZURE_IDENTITY_NAME value: {{ .Values.azure.identity_name }} diff --git a/charts/model-engine/templates/celery_autoscaler_stateful_set.yaml b/charts/model-engine/templates/celery_autoscaler_stateful_set.yaml index 3c7a0e95c..540b3beda 100644 --- a/charts/model-engine/templates/celery_autoscaler_stateful_set.yaml +++ b/charts/model-engine/templates/celery_autoscaler_stateful_set.yaml @@ -66,6 +66,7 @@ spec: value: {{ $broker_name }} - name: CELERY_ELASTICACHE_ENABLED value: {{ (eq $message_broker "elasticache") | squote }} + {{- include "modelEngine.redisAuthEnv" . | indent 6 }} - name: POD_NAME valueFrom: fieldRef: diff --git a/charts/model-engine/templates/service_template_config_map.yaml b/charts/model-engine/templates/service_template_config_map.yaml index 57432a06f..08c3e8f0a 100644 --- a/charts/model-engine/templates/service_template_config_map.yaml +++ b/charts/model-engine/templates/service_template_config_map.yaml @@ -513,7 +513,7 @@ data: listName: "launch-endpoint-autoscaling:${ENDPOINT_ID}" listLength: "100" # something absurdly high so we don't scale past 1 pod activationListLength: "0" - enableTLS: "{{ .Values.redis.enableTLS }}" + enableTLS: "{{ .Values.redis.enableTLS | default false }}" unsafeSsl: "{{ .Values.redis.unsafeSsl }}" databaseIndex: "${REDIS_DB_INDEX}" {{- if .Values.redis.enableAuth }} diff --git a/charts/model-engine/values.yaml b/charts/model-engine/values.yaml index 664a9766f..bdd84a034 100644 --- a/charts/model-engine/values.yaml +++ b/charts/model-engine/values.yaml @@ -80,7 +80,10 @@ redis: auth: authSecretName: "" authSecretKey: "" - enableTLS: false + # Unset means the app infers TLS from the presence of a Redis credential + # (what ElastiCache in-transit encryption needs). Set it explicitly to pin + # the scheme; the same value drives the KEDA scaler. + enableTLS: enableAuth: false kedaSecretName: "" unsafeSsl: false diff --git a/model-engine/model_engine_server/common/config.py b/model-engine/model_engine_server/common/config.py index d85573f19..05e421bbf 100644 --- a/model-engine/model_engine_server/common/config.py +++ b/model-engine/model_engine_server/common/config.py @@ -6,6 +6,7 @@ from dataclasses import dataclass from pathlib import Path from typing import Optional, Sequence +from urllib.parse import quote import yaml from azure.identity import DefaultAzureCredential @@ -47,6 +48,24 @@ def get_model_cache_directory_name(model_name: str): return name +def _apply_redis_auth_token(url: str) -> str: + """Add REDIS_AUTH_TOKEN to a Redis URL that carries no credential of its own. + + Keeps the password out of Helm values: the chart names the host and db + index, the app supplies the credential from its secret-backed env var. A + URL that already has userinfo is left alone so an explicit override wins. + """ + auth_token = os.getenv("REDIS_AUTH_TOKEN") + if not auth_token: + return url + scheme, sep, remainder = url.partition("://") + if not sep or "@" in remainder.split("/")[0]: + return url + # redis-py unquotes the userinfo, so percent-encoding here is what lets a + # password containing @, / or # survive URL parsing. + return f"{scheme}://:{quote(auth_token, safe='')}@{remainder}" + + @dataclass class HostedModelInferenceServiceConfig: gateway_namespace: str @@ -104,7 +123,7 @@ def from_yaml(cls, yaml_path): def cache_redis_url(self) -> str: # On-prem Redis support (explicit URL, no cloud provider dependency) if self.cache_redis_onprem_url: - return self.cache_redis_onprem_url + return _apply_redis_auth_token(self.cache_redis_onprem_url) cloud_provider = infra_config().cloud_provider @@ -112,10 +131,10 @@ def cache_redis_url(self) -> str: if cloud_provider == "onprem": if self.cache_redis_aws_url: logger.info("On-prem deployment using cache_redis_aws_url") - return self.cache_redis_aws_url + return _apply_redis_auth_token(self.cache_redis_aws_url) redis_host = os.getenv("REDIS_HOST", "redis") redis_port = getattr(infra_config(), "redis_port", 6379) - return f"redis://{redis_host}:{redis_port}/0" + return _apply_redis_auth_token(f"redis://{redis_host}:{redis_port}/0") if cloud_provider == "gcp": assert self.cache_redis_gcp_url, "cache_redis_gcp_url required for GCP" @@ -148,11 +167,13 @@ def cache_redis_url_expiration_timestamp(self) -> Optional[int]: @property def cache_redis_host_port(self) -> str: - # redis://redis.url:6379/ + # redis://:password@redis.url:6379/ # -> redis.url:6379 - if "rediss://" in self.cache_redis_url: - return self.cache_redis_url.split("rediss://")[1].split("@")[-1].split("/")[0] - return self.cache_redis_url.split("redis://")[1].split("/")[0] + # Credentials must be stripped for every scheme: this value is rendered + # into the KEDA scaler's `address` metadata, so a password left in here + # both breaks the address and leaks into the ScaledObject. + authority = self.cache_redis_url.split("://", 1)[-1] + return authority.split("@")[-1].split("/")[0] @property def cache_redis_db_index(self) -> int: diff --git a/model-engine/model_engine_server/core/celery/__init__.py b/model-engine/model_engine_server/core/celery/__init__.py index 3368bc698..0617d0011 100644 --- a/model-engine/model_engine_server/core/celery/__init__.py +++ b/model-engine/model_engine_server/core/celery/__init__.py @@ -3,6 +3,7 @@ from .app import ( DEFAULT_TASK_VISIBILITY_SECONDS, TaskVisibility, + build_redis_url, celery_app, get_all_db_indexes, get_redis_host_port, @@ -10,6 +11,7 @@ ) __all__: Sequence[str] = ( + "build_redis_url", "celery_app", "get_all_db_indexes", "get_redis_host_port", diff --git a/model-engine/model_engine_server/core/celery/app.py b/model-engine/model_engine_server/core/celery/app.py index 6ec4711ae..7ef233b57 100644 --- a/model-engine/model_engine_server/core/celery/app.py +++ b/model-engine/model_engine_server/core/celery/app.py @@ -2,6 +2,7 @@ import os from enum import IntEnum, unique from typing import Any, Dict, Iterable, List, Optional, Tuple, Union +from urllib.parse import quote import celery import redis.asyncio as aioredis @@ -209,31 +210,53 @@ def get_redis_endpoint(db_index: int = 0) -> str: return f"{scheme}:{auth_token}@{host}:{port}/{db_index}{query_params}" return f"{scheme}{host}:{port}/{db_index}{query_params}" host, port = get_redis_host_port() + return build_redis_url(host, port, db_index) + + +def redis_tls_enabled() -> bool: + """Whether to speak TLS to Redis. + + Transport security and authentication are independent: on-prem Redis is + reached over plaintext while still requiring a password. REDIS_ENABLE_TLS + decides the scheme; when it is unset the presence of a credential implies + TLS, which is what ElastiCache with in-transit encryption needs. + """ + explicit = os.getenv("REDIS_ENABLE_TLS") + if explicit: + return explicit.lower() == "true" + return bool(os.getenv("REDIS_AUTH_TOKEN")) + + +def build_redis_url(host: str, port: Union[str, int], db_index: int = 0) -> str: auth_token = os.getenv("REDIS_AUTH_TOKEN") - if auth_token: - return f"rediss://:{auth_token}@{host}:{port}/{db_index}?ssl_cert_reqs=none" - return f"redis://{host}:{port}/{db_index}" + # kombu and redis-py both unquote the userinfo, so percent-encoding here is + # what lets a password containing @, / or # survive URL parsing. + credential = f":{quote(auth_token, safe='')}@" if auth_token else "" + if redis_tls_enabled(): + return f"rediss://{credential}{host}:{port}/{db_index}?ssl_cert_reqs=none" + return f"redis://{credential}{host}:{port}/{db_index}" def get_redis_instance(db_index: int = 0) -> Union[Redis, StrictRedis]: host, port = get_redis_host_port() auth_token = os.getenv("REDIS_AUTH_TOKEN") - - if auth_token: - return StrictRedis( - host=host, - port=port, - db=db_index, - password=auth_token, - ssl=True, - ssl_cert_reqs="none", - ) - return Redis(host=host, port=port, db=db_index) + use_tls = redis_tls_enabled() + + if not auth_token and not use_tls: + return Redis(host=host, port=port, db=db_index) + ssl_kwargs: Dict[str, Any] = {"ssl": True, "ssl_cert_reqs": "none"} if use_tls else {} + return StrictRedis( + host=host, + port=port, + db=db_index, + password=auth_token, + **ssl_kwargs, + ) def get_async_redis_instance(db_index: int = 0) -> aioredis.Redis: host, port = get_redis_host_port() - return build_aioredis_client(f"redis://{host}:{port}/{db_index}") + return build_aioredis_client(build_redis_url(host, port, db_index)) def celery_app( diff --git a/model-engine/model_engine_server/core/celery/celery_autoscaler.py b/model-engine/model_engine_server/core/celery/celery_autoscaler.py index 9d9e1b920..3799b4070 100644 --- a/model-engine/model_engine_server/core/celery/celery_autoscaler.py +++ b/model-engine/model_engine_server/core/celery/celery_autoscaler.py @@ -25,6 +25,7 @@ from model_engine_server.core.aws.roles import session from model_engine_server.core.celery import ( TaskVisibility, + build_redis_url, celery_app, get_all_db_indexes, get_redis_host_port, @@ -362,7 +363,7 @@ async def _init_client(self): get_redis_host_port() ) # Switches the redis instance based on CELERY_ELASTICACHE_ENABLED's value self.redis = { - db_index: build_aioredis_client(f"redis://{host}:{port}/{db_index}") + db_index: build_aioredis_client(build_redis_url(host, port, db_index)) for db_index in get_all_db_indexes() } self.initialized = True diff --git a/model-engine/tests/unit/common/test_redis_auth.py b/model-engine/tests/unit/common/test_redis_auth.py new file mode 100644 index 000000000..7c0d2a7f3 --- /dev/null +++ b/model-engine/tests/unit/common/test_redis_auth.py @@ -0,0 +1,152 @@ +import pytest +from model_engine_server.common.config import ( + HostedModelInferenceServiceConfig, + _apply_redis_auth_token, +) +from model_engine_server.core.celery.app import ( + build_redis_url, + get_redis_instance, + redis_tls_enabled, +) +from redis.connection import SSLConnection + +TOKEN_WITH_RESERVED_CHARS = "p@ss/w#rd" +ENCODED_RESERVED_CHARS = "p%40ss%2Fw%23rd" + + +@pytest.fixture +def redis_env(monkeypatch): + def _set(auth_token=None, enable_tls=None): + for name, value in ( + ("REDIS_AUTH_TOKEN", auth_token), + ("REDIS_ENABLE_TLS", enable_tls), + ): + if value is None: + monkeypatch.delenv(name, raising=False) + else: + monkeypatch.setenv(name, value) + + return _set + + +def test_tls_inferred_from_credential_when_unset(redis_env): + redis_env(auth_token="tok") + assert redis_tls_enabled() is True + redis_env() + assert redis_tls_enabled() is False + + +@pytest.mark.parametrize("value,expected", [("true", True), ("True", True), ("false", False)]) +def test_explicit_tls_setting_overrides_credential(redis_env, value, expected): + redis_env(auth_token="tok", enable_tls=value) + assert redis_tls_enabled() is expected + + +def test_credential_without_tls_yields_plaintext_url(redis_env): + redis_env(auth_token=TOKEN_WITH_RESERVED_CHARS, enable_tls="false") + assert build_redis_url("redis", 6379, 2) == f"redis://:{ENCODED_RESERVED_CHARS}@redis:6379/2" + + +def test_tls_without_credential_yields_rediss_url(redis_env): + redis_env(enable_tls="true") + assert build_redis_url("host", 6379, 0) == "rediss://host:6379/0?ssl_cert_reqs=none" + + +def test_credential_defaults_to_tls_url(redis_env): + redis_env(auth_token="tok") + assert build_redis_url("host", 6379, 1) == "rediss://:tok@host:6379/1?ssl_cert_reqs=none" + + +def test_no_credential_no_tls_yields_plain_url(redis_env): + redis_env() + assert build_redis_url("host", 6379, 0) == "redis://host:6379/0" + + +def test_auth_token_injected_into_credential_free_url(redis_env): + redis_env(auth_token=TOKEN_WITH_RESERVED_CHARS) + assert ( + _apply_redis_auth_token("redis://redis:6379/0") + == f"redis://:{ENCODED_RESERVED_CHARS}@redis:6379/0" + ) + + +def test_existing_credential_is_preserved(redis_env): + redis_env(auth_token="tok") + assert _apply_redis_auth_token("redis://:mine@h:6379/0") == "redis://:mine@h:6379/0" + + +def test_url_unchanged_without_auth_token(redis_env): + redis_env() + assert _apply_redis_auth_token("redis://redis:6379/0") == "redis://redis:6379/0" + + +def _host_port(url: str) -> str: + """Evaluate the cache_redis_host_port property against a stubbed URL.""" + + class _Stub: + cache_redis_url = url + + return HostedModelInferenceServiceConfig.cache_redis_host_port.fget(_Stub()) + + +@pytest.mark.parametrize( + "url,expected", + [ + ("redis://redis.url:6379/0", "redis.url:6379"), + ("rediss://redis.url:6379/0", "redis.url:6379"), + ("redis://:p%40ss@redis.url:6379/2", "redis.url:6379"), + ("rediss://:tok@redis.url:6379/0", "redis.url:6379"), + ("rediss://user:tok@cache.redis.azure.com", "cache.redis.azure.com"), + ("redis://redis:6379", "redis:6379"), + ], +) +def test_host_port_strips_credentials_for_every_scheme(url, expected): + assert _host_port(url) == expected + + +def test_host_port_never_leaks_password_into_scaler_address(redis_env): + redis_env(auth_token=TOKEN_WITH_RESERVED_CHARS) + url = _apply_redis_auth_token("redis://redis:6379/0") + host_port = _host_port(url) + assert "@" not in host_port + assert ENCODED_RESERVED_CHARS not in host_port + assert host_port == "redis:6379" + + +@pytest.fixture +def fixed_redis_host(monkeypatch): + monkeypatch.setattr( + "model_engine_server.core.celery.app.get_redis_host_port", lambda: ("h", 6379) + ) + + +def _uses_tls(client) -> bool: + return client.connection_pool.connection_class is SSLConnection + + +def test_instance_credential_without_tls(redis_env, fixed_redis_host): + redis_env(auth_token="tok", enable_tls="false") + client = get_redis_instance(1) + assert client.connection_pool.connection_kwargs["password"] == "tok" + assert not _uses_tls(client) + + +def test_instance_tls_without_credential(redis_env, fixed_redis_host): + redis_env(enable_tls="true") + client = get_redis_instance() + assert client.connection_pool.connection_kwargs.get("password") is None + assert _uses_tls(client) + + +def test_instance_credential_implies_tls_when_unset(redis_env, fixed_redis_host): + redis_env(auth_token="tok") + client = get_redis_instance() + assert client.connection_pool.connection_kwargs["password"] == "tok" + assert _uses_tls(client) + + +def test_instance_plain_when_neither_set(redis_env, fixed_redis_host): + redis_env() + client = get_redis_instance() + assert client.connection_pool.connection_kwargs.get("password") is None + assert not _uses_tls(client) From 089c0e0773c57453e1aaa2ffb066e62cc67617a0 Mon Sep 17 00:00:00 2001 From: Jeffrey Rohlman Date: Wed, 9 Sep 2026 14:39:08 -0500 Subject: [PATCH 2/4] fix(chart): derive Redis TLS once so the scaler cannot disagree REDIS_ENABLE_TLS was only emitted when redis.enableTLS was explicitly set. An install that configures a credential and leaves the value alone therefore left the app inferring TLS from the credential while the KEDA scaler fell back to plaintext, so the scaler talked cleartext to a TLS endpoint and collected no queue metrics. Resolve the scheme once in modelEngine.redisEnableTLS and feed both the app env and the scaler from it. The unset case mirrors the app's own fallback -- a configured credential implies TLS -- so the two cannot disagree. The app's resolved scheme is unchanged in every configuration. Only the scaler moves, and only where a credential is set without an explicit enableTLS, which is exactly the case where it was misconfigured. Co-Authored-By: Claude Opus 5 (1M context) --- charts/model-engine/templates/_helpers.tpl | 21 +++++++++++++------ .../service_template_config_map.yaml | 2 +- charts/model-engine/values.yaml | 5 ++--- 3 files changed, 18 insertions(+), 10 deletions(-) diff --git a/charts/model-engine/templates/_helpers.tpl b/charts/model-engine/templates/_helpers.tpl index d45453807..3f6858900 100644 --- a/charts/model-engine/templates/_helpers.tpl +++ b/charts/model-engine/templates/_helpers.tpl @@ -290,6 +290,20 @@ env: {{- end }} {{- end }} +{{- /* Resolves the Redis scheme for both the app and the KEDA scaler. An unset + value mirrors the app's own fallback -- a configured credential implies + TLS, which is what ElastiCache in-transit encryption needs -- so the two + cannot disagree about the transport. */}} +{{- define "modelEngine.redisEnableTLS" -}} +{{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) -}} +{{- .Values.redis.enableTLS }} +{{- else if or .Values.redis.authSecretName .Values.redis.auth -}} +true +{{- else -}} +false +{{- end -}} +{{- end }} + {{- /* Every workload that opens a Redis connection must include this, or it reaches an authenticated broker with no credential and fails NOAUTH. */}} {{- define "modelEngine.redisAuthEnv" }} @@ -303,13 +317,8 @@ env: - name: REDIS_AUTH_TOKEN value: {{ .Values.redis.auth }} {{- end }} - {{- /* Same value drives the KEDA scaler's enableTLS so chart and app cannot - drift. Any set value is forwarded, including a stringified bool from a - value override; only an unset value leaves the app's inference intact. */}} - {{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) }} - name: REDIS_ENABLE_TLS - value: {{ .Values.redis.enableTLS | quote }} - {{- end }} + value: {{ include "modelEngine.redisEnableTLS" . | quote }} {{- end }} {{- define "modelEngine.serviceEnvBase" }} diff --git a/charts/model-engine/templates/service_template_config_map.yaml b/charts/model-engine/templates/service_template_config_map.yaml index 08c3e8f0a..a49fd5bdd 100644 --- a/charts/model-engine/templates/service_template_config_map.yaml +++ b/charts/model-engine/templates/service_template_config_map.yaml @@ -513,7 +513,7 @@ data: listName: "launch-endpoint-autoscaling:${ENDPOINT_ID}" listLength: "100" # something absurdly high so we don't scale past 1 pod activationListLength: "0" - enableTLS: "{{ .Values.redis.enableTLS | default false }}" + enableTLS: "{{ include "modelEngine.redisEnableTLS" . }}" unsafeSsl: "{{ .Values.redis.unsafeSsl }}" databaseIndex: "${REDIS_DB_INDEX}" {{- if .Values.redis.enableAuth }} diff --git a/charts/model-engine/values.yaml b/charts/model-engine/values.yaml index bdd84a034..318c74329 100644 --- a/charts/model-engine/values.yaml +++ b/charts/model-engine/values.yaml @@ -80,9 +80,8 @@ redis: auth: authSecretName: "" authSecretKey: "" - # Unset means the app infers TLS from the presence of a Redis credential - # (what ElastiCache in-transit encryption needs). Set it explicitly to pin - # the scheme; the same value drives the KEDA scaler. + # Unset infers TLS from the presence of a Redis credential, which is what + # ElastiCache in-transit encryption needs. Set explicitly to pin the scheme. enableTLS: enableAuth: false kedaSecretName: "" From f4010fb412e9d86af6e66e74ac33487ff4ea5b70 Mon Sep 17 00:00:00 2001 From: Jeffrey Rohlman Date: Wed, 9 Sep 2026 14:46:27 -0500 Subject: [PATCH 3/4] fix(chart): withhold REDIS_ENABLE_TLS when the value is unknowable A token can reach the pod through the extraEnvVars hook, which the chart cannot see. Emitting REDIS_ENABLE_TLS unconditionally sent an explicit false in that case, overriding the app's token-based inference and forcing plaintext against a TLS-only Redis. Emit only when the chart can actually determine the scheme -- enableTLS set explicitly, or a credential configured through redis.auth/authSecretName -- and otherwise leave the app's inference intact. The KEDA scaler still resolves a concrete value from the same helper, since it has no inference of its own. All six auth/enableTLS combinations still agree. Co-Authored-By: Claude Opus 5 (1M context) --- charts/model-engine/templates/_helpers.tpl | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/charts/model-engine/templates/_helpers.tpl b/charts/model-engine/templates/_helpers.tpl index 3f6858900..7d03dd658 100644 --- a/charts/model-engine/templates/_helpers.tpl +++ b/charts/model-engine/templates/_helpers.tpl @@ -294,8 +294,12 @@ env: value mirrors the app's own fallback -- a configured credential implies TLS, which is what ElastiCache in-transit encryption needs -- so the two cannot disagree about the transport. */}} +{{- define "modelEngine.redisEnableTLSIsSet" -}} +{{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) -}}true{{- end -}} +{{- end }} + {{- define "modelEngine.redisEnableTLS" -}} -{{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) -}} +{{- if include "modelEngine.redisEnableTLSIsSet" . -}} {{- .Values.redis.enableTLS }} {{- else if or .Values.redis.authSecretName .Values.redis.auth -}} true @@ -317,8 +321,13 @@ false - name: REDIS_AUTH_TOKEN value: {{ .Values.redis.auth }} {{- end }} + {{- /* Withheld when the chart cannot determine it: a token reaching the pod + through extraEnvVars is invisible here, and an explicit false would + override the app's own inference and force plaintext. */}} + {{- if or (include "modelEngine.redisEnableTLSIsSet" .) .Values.redis.authSecretName .Values.redis.auth }} - name: REDIS_ENABLE_TLS value: {{ include "modelEngine.redisEnableTLS" . | quote }} + {{- end }} {{- end }} {{- define "modelEngine.serviceEnvBase" }} From 738c85ea8ce68379a2a3ce2b3aff086a359f6deb Mon Sep 17 00:00:00 2001 From: Jeffrey Rohlman Date: Wed, 9 Sep 2026 15:23:40 -0500 Subject: [PATCH 4/4] chore(chart): bump version to 0.2.10 Co-Authored-By: Claude Opus 5 (1M context) --- charts/model-engine/Chart.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/charts/model-engine/Chart.yaml b/charts/model-engine/Chart.yaml index 26a322215..ed2f7a2ad 100644 --- a/charts/model-engine/Chart.yaml +++ b/charts/model-engine/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 0.2.9 +version: 0.2.10 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to