From dce8c45f405c6d0958d6f10873ea8bd0555c725e Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Thu, 15 Oct 2026 15:33:51 -0700 Subject: [PATCH] feat(helm): add db.useIAMAuth and db.useAzureEntraAuth token auth to litellm-helm (#46534) Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- helm/litellm-helm/README.md | 21 ++ helm/litellm-helm/templates/_helpers.tpl | 96 ++++-- .../templates/migrations-job.yaml | 25 +- .../tests/database_auth_tests.yaml | 288 ++++++++++++++++++ helm/litellm-helm/values.yaml | 24 ++ 5 files changed, 408 insertions(+), 46 deletions(-) create mode 100644 helm/litellm-helm/tests/database_auth_tests.yaml diff --git a/helm/litellm-helm/README.md b/helm/litellm-helm/README.md index a76af037045..ff432c451e5 100644 --- a/helm/litellm-helm/README.md +++ b/helm/litellm-helm/README.md @@ -126,6 +126,11 @@ Set `billingMetrics.caSecretName` only when the collector is a private or test o | `db.secret.name` | If `db.useExisting` is `true`, the name of the Kubernetes Secret that contains credentials. | `postgres` | | `db.secret.usernameKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the username for authenticating with the Postgres instance. | `username` | | `db.secret.passwordKey` | If `db.useExisting` is `true`, the name of the key within the Kubernetes Secret that holds the password associates with the above user. | `password` | +| `db.username` | If `db.useExisting` is `true`, the database user as a plain value. When set, `db.secret.usernameKey` is not read. | `""` | +| `db.port` | If `db.useExisting` is `true` with token auth, the Postgres port used to build the connection URL. Password auth takes the port from `db.url`. | `5432` | +| `db.schema` | With token auth, the Postgres schema holding the LiteLLM tables, rendered as `DATABASE_SCHEMA` when the tables are not in `public`. Ignored with password auth, where the schema stays in `db.url` as `?schema=`. | `""` | +| `db.useIAMAuth` | If `db.useExisting` is `true` and `db.deployStandalone` is `false`, authenticate with an AWS RDS IAM token instead of a password. See [Token auth](#token-auth-to-an-existing-postgres-rds-iam-or-microsoft-entra-id). | `false` | +| `db.useAzureEntraAuth` | If `db.useExisting` is `true` and `db.deployStandalone` is `false`, authenticate with a Microsoft Entra ID token instead of a password. Mutually exclusive with `db.useIAMAuth`. | `false` | | `db.useStackgresOperator` | Not yet implemented. | `false` | | `db.deployStandalone` | Deploy a standalone, single instance deployment of Postgres, using the Bitnami postgresql chart. This is useful for getting started but doesn't provide HA or (by default) data backups. | `true` | | `postgresql.*` | If `db.deployStandalone` is `true`, configuration passed to the Bitnami postgresql chart. See the [Bitnami Documentation](https://github.com/bitnami/charts/tree/main/bitnami/postgresql) for full configuration details. See [values.yaml](./values.yaml) for the default configuration. | See [values.yaml](./values.yaml) | @@ -156,6 +161,22 @@ data: type: Opaque ``` +#### Token auth to an existing Postgres (RDS IAM or Microsoft Entra ID) + +When the database accepts short-lived tokens instead of a password (AWS RDS or Aurora with IAM database authentication, or Azure Database for PostgreSQL with Microsoft Entra ID), there is no password to put in a Secret. Turn on `db.useIAMAuth` or `db.useAzureEntraAuth` and the chart stops referencing `db.secret.passwordKey` and `db.url`. It sets `IAM_TOKEN_DB_AUTH` (or `AZURE_POSTGRESQL_AUTH`) and emits `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USERNAME` and `DATABASE_NAME` for the proxy, the migrations job and the collector sidecar. Each of them mints its own token at startup and builds `DATABASE_URL` from those parts, and the in-container pgbouncer (`db.connectionPool`) refreshes the token before it expires. + +```yaml +db: + useExisting: true + deployStandalone: false + useIAMAuth: true + endpoint: litellm.cluster-abc123.us-west-1.rds.amazonaws.com + database: litellm + username: litellm_iam # the role granted rds_iam; no db.secret needed +``` + +The pod needs cloud credentials for the token: on EKS use IRSA or Pod Identity on the chart's `serviceAccount` (and `migrationJob.serviceAccountName` when the Helm hook runs before the ServiceAccount exists), on AKS use workload identity. Set `AWS_RDS_REGION` under `envVars` when the RDS hostname is not a standard regional endpoint (the token must be signed for the region the database lives in). TLS options that would otherwise go in `db.url` are set as `DATABASE_SSLMODE` and `DATABASE_SSLROOTCERT` under `envVars`. Leaving `db.username` empty reads the user from `db.secret.usernameKey` as before. + #### Examples for `environmentSecrets` and `environemntConfigMaps` ```yaml diff --git a/helm/litellm-helm/templates/_helpers.tpl b/helm/litellm-helm/templates/_helpers.tpl index cde4bce7f47..e4e6cdd4435 100644 --- a/helm/litellm-helm/templates/_helpers.tpl +++ b/helm/litellm-helm/templates/_helpers.tpl @@ -162,6 +162,77 @@ taken before the change, which by that point no longer exists. {{- end -}} {{- end -}} +{{/* +Whether the existing Postgres is reached with a short-lived token (AWS RDS IAM +or Microsoft Entra ID) instead of a password. +*/}} +{{- define "litellm.db.tokenAuth" -}} +{{- if or .Values.db.useIAMAuth .Values.db.useAzureEntraAuth -}}true{{- end -}} +{{- end -}} + +{{- define "litellm.validateDbTokenAuth" -}} +{{- if and .Values.db.useIAMAuth .Values.db.useAzureEntraAuth -}} +{{- fail "db.useIAMAuth and db.useAzureEntraAuth are mutually exclusive: the database password can only come from one token source" -}} +{{- end -}} +{{- if and (include "litellm.db.tokenAuth" .) (or (not .Values.db.useExisting) .Values.db.deployStandalone) -}} +{{- fail "db.useIAMAuth and db.useAzureEntraAuth need db.useExisting: true and db.deployStandalone: false (token auth targets an existing RDS or Azure Database for PostgreSQL; the bundled postgresql subchart only supports password auth)" -}} +{{- end -}} +{{- end -}} + +{{/* +Database env for an existing Postgres, shared by the proxy container, the +collector sidecar and the migrations Job. Under token auth every entrypoint +mints its own token and assembles DATABASE_URL itself, so db.url is left out. +*/}} +{{- define "litellm.existingDbEnv" -}} +{{- $tokenAuth := include "litellm.db.tokenAuth" . -}} +- name: DATABASE_USERNAME + {{- if .Values.db.username }} + value: {{ .Values.db.username | quote }} + {{- else }} + valueFrom: + secretKeyRef: + name: {{ .Values.db.secret.name }} + key: {{ .Values.db.secret.usernameKey }} + {{- end }} +{{- if .Values.db.useIAMAuth }} +- name: IAM_TOKEN_DB_AUTH + value: "true" +{{- else if .Values.db.useAzureEntraAuth }} +- name: AZURE_POSTGRESQL_AUTH + value: "true" +{{- else }} +- name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: {{ .Values.db.secret.name }} + key: {{ .Values.db.secret.passwordKey }} +{{- end }} +- name: DATABASE_HOST + {{- if .Values.db.secret.endpointKey }} + valueFrom: + secretKeyRef: + name: {{ .Values.db.secret.name }} + key: {{ .Values.db.secret.endpointKey }} + {{- else }} + value: {{ .Values.db.endpoint }} + {{- end }} +{{- if $tokenAuth }} +- name: DATABASE_PORT + value: {{ .Values.db.port | quote }} +{{- end }} +- name: DATABASE_NAME + value: {{ .Values.db.database }} +{{- if and $tokenAuth .Values.db.schema }} +- name: DATABASE_SCHEMA + value: {{ .Values.db.schema | quote }} +{{- end }} +{{- if not $tokenAuth }} +- name: DATABASE_URL + value: {{ .Values.db.url | quote }} +{{- end }} +{{- end -}} + {{/* Environment shared by the proxy container and the opt-in collector sidecar: database, pgbouncer, master key, redis, user envVars. Both containers must see @@ -169,6 +240,7 @@ the same DATABASE_URL and REDIS_* so the sidecar reaches the pod's pgbouncer and the same spend transaction buffer. */}} {{- define "litellm.proxyEnv" -}} +{{- include "litellm.validateDbTokenAuth" . -}} - name: HOST value: "{{ .Values.listen | default "0.0.0.0" }}" - name: PORT @@ -189,29 +261,7 @@ and the same spend transaction buffer. - name: DATABASE_NAME value: litellm {{- else if .Values.db.useExisting }} -- name: DATABASE_USERNAME - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.usernameKey }} -- name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.passwordKey }} -- name: DATABASE_HOST - {{- if .Values.db.secret.endpointKey }} - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.endpointKey }} - {{- else }} - value: {{ .Values.db.endpoint }} - {{- end }} -- name: DATABASE_NAME - value: {{ .Values.db.database }} -- name: DATABASE_URL - value: {{ .Values.db.url | quote }} +{{ include "litellm.existingDbEnv" . }} {{- end }} {{- if and .Values.db.useExisting .Values.db.readReplicaUrl .Values.db.secret.readReplicaEndpointKey (not .Values.db.secret.readReplicaUrlKey) }} - name: DATABASE_READER_HOST diff --git a/helm/litellm-helm/templates/migrations-job.yaml b/helm/litellm-helm/templates/migrations-job.yaml index b199ad2d8f3..7aa96a59102 100644 --- a/helm/litellm-helm/templates/migrations-job.yaml +++ b/helm/litellm-helm/templates/migrations-job.yaml @@ -68,30 +68,9 @@ spec: {{- end }} workingDir: "/app" env: + {{- include "litellm.validateDbTokenAuth" . -}} {{- if .Values.db.useExisting }} - - name: DATABASE_USERNAME - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.usernameKey }} - - name: DATABASE_PASSWORD - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.passwordKey }} - - name: DATABASE_HOST - {{- if .Values.db.secret.endpointKey }} - valueFrom: - secretKeyRef: - name: {{ .Values.db.secret.name }} - key: {{ .Values.db.secret.endpointKey }} - {{- else }} - value: {{ .Values.db.endpoint }} - {{- end }} - - name: DATABASE_NAME - value: {{ .Values.db.database }} - - name: DATABASE_URL - value: {{ .Values.db.url | quote }} + {{- include "litellm.existingDbEnv" . | nindent 12 }} {{- else if .Values.db.deployStandalone }} - name: DATABASE_URL value: postgresql://{{ .Values.postgresql.auth.username }}:{{ .Values.postgresql.auth.password }}@{{ .Release.Name }}-postgresql/{{ .Values.postgresql.auth.database }} diff --git a/helm/litellm-helm/tests/database_auth_tests.yaml b/helm/litellm-helm/tests/database_auth_tests.yaml new file mode 100644 index 00000000000..2c480196dfb --- /dev/null +++ b/helm/litellm-helm/tests/database_auth_tests.yaml @@ -0,0 +1,288 @@ +suite: test existing database token auth +templates: + - deployment.yaml + - migrations-job.yaml + - configmap-litellm.yaml +tests: + - it: should keep password auth from the secret when no token auth is set + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_USERNAME + valueFrom: + secretKeyRef: + name: postgres + key: username + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PASSWORD + valueFrom: + secretKeyRef: + name: postgres + key: password + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_URL + value: postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME) + - notContains: + path: spec.template.spec.containers[0].env + content: + name: IAM_TOKEN_DB_AUTH + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: AZURE_POSTGRESQL_AUTH + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PORT + any: true + + - it: should render RDS IAM auth without a password secret on the proxy and the collector sidecar + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + db.username: litellm_iam + db.endpoint: writer.cluster-abc.us-west-1.rds.amazonaws.com + db.port: 5433 + collector.enabled: true + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: IAM_TOKEN_DB_AUTH + value: "true" + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_USERNAME + value: litellm_iam + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_HOST + value: writer.cluster-abc.us-west-1.rds.amazonaws.com + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PORT + value: "5433" + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_NAME + value: litellm + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PASSWORD + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_URL + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: AZURE_POSTGRESQL_AUTH + any: true + - contains: + path: spec.template.spec.containers[1].env + content: + name: IAM_TOKEN_DB_AUTH + value: "true" + - notContains: + path: spec.template.spec.containers[1].env + content: + name: DATABASE_PASSWORD + any: true + + - it: should give the migrations job the same token auth env as the proxy + template: migrations-job.yaml + set: + migrationJob.enabled: true + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + db.username: litellm_iam + db.endpoint: writer.cluster-abc.us-west-1.rds.amazonaws.com + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: IAM_TOKEN_DB_AUTH + value: "true" + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_USERNAME + value: litellm_iam + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_HOST + value: writer.cluster-abc.us-west-1.rds.amazonaws.com + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PORT + value: "5432" + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PASSWORD + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_URL + any: true + + - it: should carry db.schema as DATABASE_SCHEMA under token auth and omit it when empty + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + db.username: litellm_iam + db.schema: tenant1 + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_SCHEMA + value: tenant1 + + - it: should leave the schema to db.url under password auth even when db.schema is set + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.schema: tenant1 + db.url: postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME)?schema=tenant1 + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_SCHEMA + any: true + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_URL + value: postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME)?schema=tenant1 + + - it: should not emit DATABASE_SCHEMA when db.schema is empty + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + asserts: + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_SCHEMA + any: true + + - it: should read the user from the secret when db.username is empty under token auth + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + db.secret.name: rds-identity + db.secret.usernameKey: user + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_USERNAME + valueFrom: + secretKeyRef: + name: rds-identity + key: user + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PASSWORD + any: true + + - it: should render Entra auth with AZURE_POSTGRESQL_AUTH and no password + template: deployment.yaml + set: + db.useExisting: true + db.deployStandalone: false + db.useAzureEntraAuth: true + db.username: litellm@example.onmicrosoft.com + db.endpoint: litellm.postgres.database.azure.com + asserts: + - contains: + path: spec.template.spec.containers[0].env + content: + name: AZURE_POSTGRESQL_AUTH + value: "true" + - contains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_USERNAME + value: litellm@example.onmicrosoft.com + - notContains: + path: spec.template.spec.containers[0].env + content: + name: IAM_TOKEN_DB_AUTH + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_PASSWORD + any: true + - notContains: + path: spec.template.spec.containers[0].env + content: + name: DATABASE_URL + any: true + + - it: should fail when both token sources are enabled + template: deployment.yaml + set: + migrationJob.enabled: false + db.useExisting: true + db.deployStandalone: false + db.useIAMAuth: true + db.useAzureEntraAuth: true + asserts: + - failedTemplate: + errorMessage: "db.useIAMAuth and db.useAzureEntraAuth are mutually exclusive: the database password can only come from one token source" + + - it: should fail when token auth is set while the bundled postgres is still deployed + template: deployment.yaml + set: + migrationJob.enabled: false + db.useExisting: true + db.useIAMAuth: true + asserts: + - failedTemplate: + errorPattern: "need db.useExisting: true and db.deployStandalone: false" + + - it: should fail when token auth is set without useExisting + template: migrations-job.yaml + set: + migrationJob.enabled: true + db.deployStandalone: false + db.useAzureEntraAuth: true + asserts: + - failedTemplate: + errorPattern: "need db.useExisting: true and db.deployStandalone: false" diff --git a/helm/litellm-helm/values.yaml b/helm/litellm-helm/values.yaml index 11954d121a5..199c4595149 100644 --- a/helm/litellm-helm/values.yaml +++ b/helm/litellm-helm/values.yaml @@ -376,6 +376,30 @@ db: endpoint: localhost database: litellm url: postgresql://$(DATABASE_USERNAME):$(DATABASE_PASSWORD)@$(DATABASE_HOST)/$(DATABASE_NAME) + # Authenticate with a short-lived token instead of a password. useIAMAuth is + # AWS RDS / Aurora IAM database authentication (the pod needs AWS credentials, + # e.g. IRSA or EKS Pod Identity, and the database user needs GRANT rds_iam); + # useAzureEntraAuth is Azure Database for PostgreSQL with a Microsoft Entra ID + # identity (workload identity). Mutually exclusive, and both need + # useExisting: true with deployStandalone: false. The chart then sets + # IAM_TOKEN_DB_AUTH or AZURE_POSTGRESQL_AUTH, emits DATABASE_HOST, DATABASE_PORT, + # DATABASE_USERNAME and DATABASE_NAME and skips DATABASE_PASSWORD and db.url: + # the proxy, the migrations job and the collector sidecar each mint their own + # token and build DATABASE_URL from those parts. Set DATABASE_SSLMODE / + # DATABASE_SSLROOTCERT under envVars for TLS options you would otherwise put + # in db.url. + useIAMAuth: false + useAzureEntraAuth: false + # Postgres port, used to build the token-auth URL (password auth takes the + # port from db.url) + port: 5432 + # Postgres schema for the LiteLLM tables, rendered as DATABASE_SCHEMA under + # token auth only (password auth carries it as ?schema= in db.url) + schema: "" + # Database user as a plain value. Empty means read it from + # db.secret.usernameKey. With token auth the user name is the only credential + # the chart needs, so setting it here removes the need for db.secret entirely. + username: "" secret: name: postgres usernameKey: username