Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion charts/model-engine/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
51 changes: 41 additions & 10 deletions charts/model-engine/templates/_helpers.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,46 @@ 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.redisEnableTLSIsSet" -}}
{{- if not (or (kindIs "invalid" .Values.redis.enableTLS) (eq (toString .Values.redis.enableTLS) "")) -}}true{{- end -}}
{{- end }}

{{- define "modelEngine.redisEnableTLS" -}}
{{- if include "modelEngine.redisEnableTLSIsSet" . -}}
{{- .Values.redis.enableTLS }}
{{- else if or .Values.redis.authSecretName .Values.redis.auth -}}
true
{{- else -}}
false
Comment thread
greptile-apps[bot] marked this conversation as resolved.
{{- 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 }}
{{- /* 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" }}
env:
- name: DD_TRACE_ENABLED
Expand Down Expand Up @@ -379,16 +419,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 }}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: "{{ include "modelEngine.redisEnableTLS" . }}"

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you double check whether this regresses AWS? Specifically enableTLS: "{{ include "modelEngine.redisEnableTLS" . }}".

The concern is that the scaler and the app connect to two different Redis instances. This trigger's address is ${REDIS_HOST_PORT} = hmi_config.cache_redis_host_port (k8s_resource_types.py:1368), which derives from cache_redis_url → on AWS that's cache_redis_aws_url verbatim, untouched by this PR (config.py:150). But redisEnableTLS infers TLS from redis.auth/authSecretName, which is the message broker's credential — a different endpoint.

The app connects to the cache using whatever scheme cache_redis_aws_url names, and that path never gets a credential injected on AWS. So if that URL is redis://, the cache accepts plaintext, and telling the scaler TLS=true is wrong for the same endpoint. Both shipped examples are plaintext, on a host distinct from the broker:

  • values_sample.yaml:263redis://llm-engine-prod-cache.use1.cache.amazonaws.com:6379/15
  • values_circleci.yaml:172redis://redis-message-broker-master.default/15

Rendered with values_circleci.yaml --set redis.auth=… and enableTLS unset: main gives enableTLS: "false", this branch gives "true". If that's the shape of a real install, KEDA's handshake against a plaintext cache fails, the redis trigger reports no queue length, and async endpoints stop scaling off the activation threshold. AWS does take this branch — the servicebus alternative is gated on $azure_cloud_provider (:497).

unsafeSsl: "{{ .Values.redis.unsafeSsl }}"
databaseIndex: "${REDIS_DB_INDEX}"
{{- if .Values.redis.enableAuth }}
Expand Down
4 changes: 3 additions & 1 deletion charts/model-engine/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ redis:
auth:
authSecretName: ""
authSecretKey: ""
enableTLS: false
# 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: ""
unsafeSsl: false
Expand Down
35 changes: 28 additions & 7 deletions model-engine/model_engine_server/common/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -104,18 +123,18 @@ 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)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Can you check if Injecting the credential into cache_redis_url leaks it into logs?

entrypoints/k8s_cache.py:108 does logger.info(f"Using cache redis url {redis_url}") with no debug_mode guard, and the cacher Deployment runs exactly that entrypoint (cacher_deployment.yaml:61-64) with REDIS_AUTH_TOKEN in its env. On main the on-prem URL carried no credential, so this is new exposure — on every cacher startup, into Datadog. service_builder/tasks_v1.py:158 repeats it behind debug_mode.


cloud_provider = infra_config().cloud_provider

# On-prem: support REDIS_HOST env var fallback
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"
Expand Down Expand Up @@ -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/<db_index>
# redis://:password@redis.url:6379/<db_index>
# -> 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:
Expand Down
2 changes: 2 additions & 0 deletions model-engine/model_engine_server/core/celery/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
from .app import (
DEFAULT_TASK_VISIBILITY_SECONDS,
TaskVisibility,
build_redis_url,
celery_app,
get_all_db_indexes,
get_redis_host_port,
inspect_app,
)

__all__: Sequence[str] = (
"build_redis_url",
"celery_app",
"get_all_db_indexes",
"get_redis_host_port",
Expand Down
53 changes: 38 additions & 15 deletions model-engine/model_engine_server/core/celery/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Comment thread
greptile-apps[bot] marked this conversation as resolved.


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"
Comment thread
greptile-apps[bot] marked this conversation as resolved.
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
Expand Down
Loading