From f10bc21a43bb68353905cd149ed4eca44dc59239 Mon Sep 17 00:00:00 2001 From: Lily Shen <115414357+lilyshen0722@users.noreply.github.com> Date: Wed, 7 Oct 2026 10:18:54 -0700 Subject: [PATCH] fix(helm): expose only LiteLLM's /v1 API publicly; keep the admin login internal litellm.commonly.me routed `/` to the proxy, so the LiteLLM dashboard and its login (username admin, password = LITELLM_MASTER_KEY) were reachable from the open internet, along with /openapi.json and the key/user/team management routes. Model routes already required a key (401 without one), but the master key is above every budget, and the login is a public guess target for it. Every client of the public host uses /v1: the pi adapter's baseUrl, the docs, and all 147 host-side references. The ingress now serves `paths: [/v1]` for the LiteLLM host. `paths` wins over a legacy single `path`, so an older values file cannot re-open `/`. The dashboard is reached by port-forward, documented in LITELLM.md. CI asserts the rendered rule is exactly /v1; it fails on [/] and on [/v1,/]. Co-Authored-By: Claude Opus 5.5 (1M context) Claude-Session: https://claude.ai/code/session_015NDyNbwmCco62PAAviSL1k --- .github/workflows/tests.yml | 2 ++ docs/development/LITELLM.md | 10 ++++++++-- k8s/helm/commonly/templates/ingress/ingress.yaml | 9 ++++++--- k8s/helm/commonly/values.yaml | 8 +++++++- 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 0a98ca0c0..d5ca08dea 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -290,6 +290,8 @@ jobs: test "$(yq 'select(.kind == "Deployment" and .metadata.name == "backend") | .spec.template.spec.terminationGracePeriodSeconds' /tmp/dev-rendered.yaml)" = "30" test "$(yq 'select(.kind == "Deployment" and .metadata.name == "backend") | .spec.template.spec.containers[] | select(.name == "backend") | .readinessProbe.httpGet.path' /tmp/dev-rendered.yaml)" = "/api/health/ready" test "$(yq 'select(.kind == "Deployment" and .metadata.name == "backend") | .spec.template.spec.containers[] | select(.name == "backend") | .lifecycle.preStop.exec.command[2]' /tmp/dev-rendered.yaml)" = "sleep 10" + # LiteLLM is public for its /v1 API only; `/` exposed the master-key admin login (2026-10-07). + test "$(yq 'select(.kind == "Ingress") | .spec.rules[] | select(.host == "litellm.commonly.me") | .http.paths | map(.path) | join(",")' /tmp/dev-rendered.yaml)" = "/v1" - name: Render Slack OAuth secret wiring run: | diff --git a/docs/development/LITELLM.md b/docs/development/LITELLM.md index 54301906f..6feb5b0ea 100644 --- a/docs/development/LITELLM.md +++ b/docs/development/LITELLM.md @@ -29,7 +29,7 @@ Backend services ──► LiteLLM :4000 ──► Gemini / OpenRouter / Ope ``` - **Service**: `litellm.commonly-dev.svc.cluster.local:4000` -- **Dashboard**: `https://litellm.commonly.me/ui` (login with `LITELLM_MASTER_KEY`) +- **Dashboard**: cluster-internal only. `kubectl port-forward -n commonly-dev svc/litellm 4000:4000`, then `http://localhost:4000/ui` (login with `LITELLM_MASTER_KEY`). The public host `litellm.commonly.me` serves `/v1` and nothing else; the ingress used to serve `/`, which put this master-key login on the open internet. - **Health probe**: `GET /health/readiness` (no auth required) - **Spend logs**: stored in Aiven PostgreSQL (`LiteLLM_SpendLogs` table) - **Image**: `ghcr.io/berriai/litellm:main-stable` @@ -422,7 +422,13 @@ req.write(body);req.end();" ### Using the LiteLLM Dashboard (UI) -URL: `https://litellm.commonly.me/ui` +Not public. Port-forward the service, then open the local URL: + +```bash +kubectl port-forward -n commonly-dev svc/litellm 4000:4000 +# http://localhost:4000/ui +``` + Login: username `admin`, password = value of `LITELLM_MASTER_KEY` ```bash diff --git a/k8s/helm/commonly/templates/ingress/ingress.yaml b/k8s/helm/commonly/templates/ingress/ingress.yaml index b0b3e1c90..bbc50512f 100644 --- a/k8s/helm/commonly/templates/ingress/ingress.yaml +++ b/k8s/helm/commonly/templates/ingress/ingress.yaml @@ -66,12 +66,15 @@ spec: - host: {{ .Values.ingress.hosts.litellm.host }} http: paths: - - path: {{ .Values.ingress.hosts.litellm.path }} - pathType: {{ .Values.ingress.hosts.litellm.pathType }} + {{- /* `paths` wins over a legacy single `path` so an older values file cannot re-open `/`. */}} + {{- range (.Values.ingress.hosts.litellm.paths | default (list .Values.ingress.hosts.litellm.path)) }} + - path: {{ . }} + pathType: {{ $.Values.ingress.hosts.litellm.pathType }} backend: service: name: litellm port: - number: {{ .Values.litellm.port }} + number: {{ $.Values.litellm.port }} + {{- end }} {{- end }} {{- end }} diff --git a/k8s/helm/commonly/values.yaml b/k8s/helm/commonly/values.yaml index 4665ef137..dbb44574e 100644 --- a/k8s/helm/commonly/values.yaml +++ b/k8s/helm/commonly/values.yaml @@ -370,7 +370,13 @@ ingress: aliases: [] litellm: host: litellm.commonly.me - path: / + # Only the OpenAI-compatible API is public. Clients (the pi adapter, any + # SDK) use the /v1 base URL; the admin dashboard, its login and the key / + # user / team management routes stay cluster-internal — reach them with + # `kubectl port-forward svc/litellm 4000:4000`. Serving `/` here put a + # master-key login page on the open internet. + paths: + - /v1 pathType: Prefix tls: enabled: false