From 46f8fabb07c90c0f0d49277c6a3c7654edebf3db Mon Sep 17 00:00:00 2001 From: Yassin Kortam Date: Sat, 1 Aug 2026 14:10:55 -0700 Subject: [PATCH] feat(helm): add pod-hardening and migration-Job knobs to the componentized chart (#35489) The componentized chart exposed no pod-hardening surface, so values that operators of hardened clusters routinely set (podSecurityContext, securityContext, extraContainers, podLabels, lifecycle, terminationGracePeriodSeconds) rendered nothing at all. Helm does not error on unknown values, so the deploy went green with none of the hardening applied. Adds those six knobs to gateway, backend, and ui, plus volumes, volumeMounts, podLabels, podSecurityContext, and securityContext on the migrations Job. Also fixes a first-install failure: the migrations Job is a pre-install/pre-upgrade hook, so borrowing the backend ServiceAccount name while the chart creates that account references an account that does not exist yet, and the Job pod is rejected as forbidden. The Job now resolves its own name through migrationJob.serviceAccountName, falling back to the namespace default account when the chart creates the backend one and keeping today's shared name otherwise. --- helm/litellm/templates/_helpers.tpl | 53 ++++ .../litellm/templates/backend/deployment.yaml | 22 ++ .../litellm/templates/gateway/deployment.yaml | 22 ++ helm/litellm/templates/migrations-job.yaml | 27 +- helm/litellm/templates/ui/deployment.yaml | 22 ++ helm/litellm/tests/migration_job_tests.yaml | 169 ++++++++++ helm/litellm/tests/pod_hardening_tests.yaml | 298 ++++++++++++++++++ helm/litellm/values.yaml | 84 +++++ 8 files changed, 694 insertions(+), 3 deletions(-) create mode 100644 helm/litellm/tests/migration_job_tests.yaml create mode 100644 helm/litellm/tests/pod_hardening_tests.yaml diff --git a/helm/litellm/templates/_helpers.tpl b/helm/litellm/templates/_helpers.tpl index a0205c0a3a2..bffd627393a 100644 --- a/helm/litellm/templates/_helpers.tpl +++ b/helm/litellm/templates/_helpers.tpl @@ -138,6 +138,59 @@ is false the chart uses the provided name, or the namespace `default` SA. {{- end -}} {{- end -}} +{{/* +ServiceAccount name for the migrations Job. + +The Job is a pre-install / pre-upgrade hook, so it is created before the +chart's ordinary resources. A ServiceAccount the chart creates is one of +those ordinary resources, which makes borrowing the backend name a cycle: +the hook pod is rejected because the account does not exist yet. So when +`serviceAccounts.backend.create` is true the Job falls back to the namespace +`default` account unless the operator names one that already exists. With +`create` false the backend name is either an operator-supplied existing +account or `default`, both of which are safe for the hook, so the Job keeps +sharing it. + +`migrationJob.serviceAccountName` always wins when set, which is how a Job +that needs credentials of its own (IRSA / Workload Identity for IAM database +auth) gets them. +*/}} +{{- define "litellm.migrations.serviceAccountName" -}} +{{- if .Values.migrationJob.serviceAccountName -}} +{{ .Values.migrationJob.serviceAccountName }} +{{- else if .Values.serviceAccounts.backend.create -}} +default +{{- else -}} +{{ include "litellm.backend.serviceAccountName" . }} +{{- end -}} +{{- end -}} + +{{/* +Extra pod labels for a component's Deployment, validated against its selector. + +Invoke with a dict: + (dict "podLabels" .Values.gateway.podLabels "componentName" "gateway") + +The three selector keys are also emitted on the pod template, so a podLabels +entry reusing one renders a duplicate YAML key whose later value wins. That +leaves the pod template no longer matching the (immutable) selector and the +apiserver rejects the Deployment. Fail at template time naming the key +instead, so the operator gets the reason here rather than an opaque +`selector does not match template labels` from the apiserver. + +The migrations Job takes podLabels unvalidated: a Job's selector is generated +by the controller rather than declared, so nothing there can collide. +*/}} +{{- define "litellm.podLabels" -}} +{{- $componentName := .componentName -}} +{{- range $key, $value := .podLabels }} +{{- if has $key (list "app.kubernetes.io/name" "app.kubernetes.io/instance" "app.kubernetes.io/component") }} +{{- fail (printf "%s.podLabels cannot set %s: it is part of the Deployment's immutable selector" $componentName $key) }} +{{- end }} +{{- end }} +{{- toYaml .podLabels }} +{{- end -}} + {{/* Master-key + database + redis env block — shared by gateway, backend, and the migrations Job. diff --git a/helm/litellm/templates/backend/deployment.yaml b/helm/litellm/templates/backend/deployment.yaml index 892b84ff7d5..c5d799a0faf 100644 --- a/helm/litellm/templates/backend/deployment.yaml +++ b/helm/litellm/templates/backend/deployment.yaml @@ -23,9 +23,16 @@ spec: {{- end }} labels: {{- include "litellm.backend.selectorLabels" . | nindent 8 }} + {{- with .Values.backend.podLabels }} + {{- include "litellm.podLabels" (dict "podLabels" . "componentName" "backend") | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "litellm.backend.serviceAccountName" . }} automountServiceAccountToken: {{ .Values.serviceAccounts.backend.automount }} + {{- with .Values.backend.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -34,6 +41,10 @@ spec: - name: backend image: "{{ .Values.backend.image.repository }}:{{ .Values.backend.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.backend.image.pullPolicy }} + {{- with .Values.backend.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} ports: - name: http containerPort: 4001 @@ -70,8 +81,15 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.backend.lifecycle }} + lifecycle: + {{- toYaml . | nindent 12 }} + {{- end }} resources: {{- toYaml .Values.backend.resources | nindent 12 }} + {{- with .Values.backend.extraContainers }} + {{- tpl (toYaml .) $ | nindent 8 }} + {{- end }} {{- if or .Values.gateway.config.create .Values.backend.volumes .Values.billingMetrics.enabled }} volumes: {{- if .Values.gateway.config.create }} @@ -102,4 +120,8 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} + {{- $gracePeriod := .Values.backend.terminationGracePeriodSeconds }} + {{- if not (or (kindIs "invalid" $gracePeriod) (eq (printf "%v" $gracePeriod) "")) }} + terminationGracePeriodSeconds: {{ $gracePeriod }} + {{- end }} {{- end }} diff --git a/helm/litellm/templates/gateway/deployment.yaml b/helm/litellm/templates/gateway/deployment.yaml index b2e22612905..7d16134a53d 100644 --- a/helm/litellm/templates/gateway/deployment.yaml +++ b/helm/litellm/templates/gateway/deployment.yaml @@ -21,9 +21,16 @@ spec: {{- end }} labels: {{- include "litellm.gateway.selectorLabels" . | nindent 8 }} + {{- with .Values.gateway.podLabels }} + {{- include "litellm.podLabels" (dict "podLabels" . "componentName" "gateway") | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "litellm.gateway.serviceAccountName" . }} automountServiceAccountToken: {{ .Values.serviceAccounts.gateway.automount }} + {{- with .Values.gateway.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -32,6 +39,10 @@ spec: - name: gateway image: "{{ .Values.gateway.image.repository }}:{{ .Values.gateway.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.gateway.image.pullPolicy }} + {{- with .Values.gateway.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} ports: - name: http containerPort: 4000 @@ -72,8 +83,15 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.gateway.lifecycle }} + lifecycle: + {{- toYaml . | nindent 12 }} + {{- end }} resources: {{- toYaml .Values.gateway.resources | nindent 12 }} + {{- with .Values.gateway.extraContainers }} + {{- tpl (toYaml .) $ | nindent 8 }} + {{- end }} {{- if or .Values.gateway.config.create .Values.gateway.volumes .Values.billingMetrics.enabled }} volumes: {{- if .Values.gateway.config.create }} @@ -104,4 +122,8 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} + {{- $gracePeriod := .Values.gateway.terminationGracePeriodSeconds }} + {{- if not (or (kindIs "invalid" $gracePeriod) (eq (printf "%v" $gracePeriod) "")) }} + terminationGracePeriodSeconds: {{ $gracePeriod }} + {{- end }} {{- end }} diff --git a/helm/litellm/templates/migrations-job.yaml b/helm/litellm/templates/migrations-job.yaml index 92671388546..2debe8a1e10 100644 --- a/helm/litellm/templates/migrations-job.yaml +++ b/helm/litellm/templates/migrations-job.yaml @@ -23,12 +23,21 @@ spec: ttlSecondsAfterFinished: {{ .Values.migrationJob.ttlSecondsAfterFinished }} template: metadata: + {{- /* The Job's selector is generated by the controller rather than + declared, so podLabels may override a chart label here. Merge + instead of appending so an override replaces the key rather than + rendering it twice. */}} + {{- $chartLabels := merge (dict "app.kubernetes.io/component" "migrations") (fromYaml (include "litellm.commonLabels" .)) }} labels: - {{- include "litellm.commonLabels" . | nindent 8 }} - app.kubernetes.io/component: migrations + {{- toYaml (merge (deepCopy .Values.migrationJob.podLabels) $chartLabels) | nindent 8 }} spec: restartPolicy: Never - serviceAccountName: {{ include "litellm.backend.serviceAccountName" . }} + serviceAccountName: {{ include "litellm.migrations.serviceAccountName" . }} + automountServiceAccountToken: {{ .Values.migrationJob.automountServiceAccountToken }} + {{- with .Values.migrationJob.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -37,10 +46,22 @@ spec: - name: prisma-migrations image: "{{ .Values.migrationJob.image.repository }}:{{ .Values.migrationJob.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.migrationJob.image.pullPolicy }} + {{- with .Values.migrationJob.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} env: {{- include "litellm.serverEnv" (dict "root" $ "component" .Values.migrationJob) | nindent 12 }} + {{- with .Values.migrationJob.volumeMounts }} + volumeMounts: + {{- toYaml . | nindent 12 }} + {{- end }} {{- with .Values.migrationJob.resources }} resources: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.migrationJob.volumes }} + volumes: + {{- toYaml . | nindent 8 }} + {{- end }} {{- end }} diff --git a/helm/litellm/templates/ui/deployment.yaml b/helm/litellm/templates/ui/deployment.yaml index cd1f8c08fd4..b4129dbc8ac 100644 --- a/helm/litellm/templates/ui/deployment.yaml +++ b/helm/litellm/templates/ui/deployment.yaml @@ -18,9 +18,16 @@ spec: {{- end }} labels: {{- include "litellm.ui.selectorLabels" . | nindent 8 }} + {{- with .Values.ui.podLabels }} + {{- include "litellm.podLabels" (dict "podLabels" . "componentName" "ui") | nindent 8 }} + {{- end }} spec: serviceAccountName: {{ include "litellm.ui.serviceAccountName" . }} automountServiceAccountToken: {{ .Values.serviceAccounts.ui.automount }} + {{- with .Values.ui.podSecurityContext }} + securityContext: + {{- toYaml . | nindent 8 }} + {{- end }} {{- with .Values.imagePullSecrets }} imagePullSecrets: {{- toYaml . | nindent 8 }} @@ -29,6 +36,10 @@ spec: - name: ui image: "{{ .Values.ui.image.repository }}:{{ .Values.ui.image.tag | default .Chart.AppVersion }}" imagePullPolicy: {{ .Values.ui.image.pullPolicy }} + {{- with .Values.ui.securityContext }} + securityContext: + {{- toYaml . | nindent 12 }} + {{- end }} ports: - name: http containerPort: 3000 @@ -58,8 +69,15 @@ spec: readinessProbe: {{- toYaml . | nindent 12 }} {{- end }} + {{- with .Values.ui.lifecycle }} + lifecycle: + {{- toYaml . | nindent 12 }} + {{- end }} resources: {{- toYaml .Values.ui.resources | nindent 12 }} + {{- with .Values.ui.extraContainers }} + {{- tpl (toYaml .) $ | nindent 8 }} + {{- end }} {{- with .Values.ui.volumes }} volumes: {{- toYaml . | nindent 8 }} @@ -80,4 +98,8 @@ spec: topologySpreadConstraints: {{- toYaml . | nindent 8 }} {{- end }} + {{- $gracePeriod := .Values.ui.terminationGracePeriodSeconds }} + {{- if not (or (kindIs "invalid" $gracePeriod) (eq (printf "%v" $gracePeriod) "")) }} + terminationGracePeriodSeconds: {{ $gracePeriod }} + {{- end }} {{- end }} diff --git a/helm/litellm/tests/migration_job_tests.yaml b/helm/litellm/tests/migration_job_tests.yaml new file mode 100644 index 00000000000..12e525c5a8c --- /dev/null +++ b/helm/litellm/tests/migration_job_tests.yaml @@ -0,0 +1,169 @@ +suite: test migrations Job ServiceAccount resolution and pod hardening +templates: + - migrations-job.yaml +values: + - ./values/required.yaml +tests: + - it: borrows the namespace default account when no ServiceAccount is configured + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + + - it: falls back to the namespace default account when the chart creates the backend ServiceAccount + set: + serviceAccounts.backend.create: true + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + - notEqual: + path: spec.template.spec.serviceAccountName + value: RELEASE-NAME-litellm-backend + + - it: keeps sharing an existing backend ServiceAccount the chart does not create + set: + serviceAccounts.backend.create: false + serviceAccounts.backend.name: existing-backend-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: existing-backend-sa + + - it: prefers an explicit migration ServiceAccount over the created backend one + set: + serviceAccounts.backend.create: true + migrationJob.serviceAccountName: migrations-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: migrations-sa + + - it: prefers an explicit migration ServiceAccount over an existing backend one + set: + serviceAccounts.backend.create: false + serviceAccounts.backend.name: existing-backend-sa + migrationJob.serviceAccountName: migrations-sa + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: migrations-sa + + - it: mounts no ServiceAccount token by default + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: false + + - it: mounts a ServiceAccount token when the operator asks for one + set: + migrationJob.automountServiceAccountToken: true + asserts: + - equal: + path: spec.template.spec.automountServiceAccountToken + value: true + + - it: keeps the token off the Job when the backend disables automounting + set: + serviceAccounts.backend.create: true + serviceAccounts.backend.automount: false + asserts: + - equal: + path: spec.template.spec.serviceAccountName + value: default + - equal: + path: spec.template.spec.automountServiceAccountToken + value: false + + - it: renders no hardening fields by default + asserts: + - isNull: + path: spec.template.spec.securityContext + - isNull: + path: spec.template.spec.containers[0].securityContext + - isNull: + path: spec.template.spec.volumes + - isNull: + path: spec.template.spec.containers[0].volumeMounts + - equal: + path: spec.template.metadata.labels + value: + app.kubernetes.io/name: litellm + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/managed-by: Helm + helm.sh/chart: litellm-0.1.0 + app.kubernetes.io/component: migrations + + - it: renders pod-level and container-level securityContext in their own scopes + set: + migrationJob.podSecurityContext: + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + migrationJob.securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + asserts: + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + runAsUser: 65532 + seccompProfile: + type: RuntimeDefault + - equal: + path: spec.template.spec.containers[0].securityContext + value: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + + - it: renders volumes on the pod and volumeMounts on the migration container + set: + migrationJob.volumes: + - name: tmp + emptyDir: + sizeLimit: 64Mi + migrationJob.volumeMounts: + - name: tmp + mountPath: /tmp + asserts: + - equal: + path: spec.template.spec.volumes + value: + - name: tmp + emptyDir: + sizeLimit: 64Mi + - equal: + path: spec.template.spec.containers[0].volumeMounts + value: + - name: tmp + mountPath: /tmp + + - it: merges podLabels with the chart labels on the Job pod + set: + migrationJob.podLabels: + egress-policy: restricted + asserts: + - equal: + path: spec.template.metadata.labels['egress-policy'] + value: restricted + - equal: + path: spec.template.metadata.labels['app.kubernetes.io/component'] + value: migrations + + - it: accepts a podLabel that reuses a chart label, since the Job selector is controller-generated + set: + migrationJob.podLabels: + app.kubernetes.io/component: batch-migrations + asserts: + - notFailedTemplate: {} + - equal: + path: spec.template.metadata.labels['app.kubernetes.io/component'] + value: batch-migrations diff --git a/helm/litellm/tests/pod_hardening_tests.yaml b/helm/litellm/tests/pod_hardening_tests.yaml new file mode 100644 index 00000000000..18e836670c0 --- /dev/null +++ b/helm/litellm/tests/pod_hardening_tests.yaml @@ -0,0 +1,298 @@ +suite: test pod hardening knobs on the component deployments +templates: + - gateway/deployment.yaml + - gateway/configmap.yaml + - backend/deployment.yaml + - ui/deployment.yaml +values: + - ./values/required.yaml +tests: + - it: gateway renders no hardening fields by default + template: gateway/deployment.yaml + asserts: + - isNull: + path: spec.template.spec.securityContext + - isNull: + path: spec.template.spec.containers[0].securityContext + - isNull: + path: spec.template.spec.containers[0].lifecycle + - isNull: + path: spec.template.spec.terminationGracePeriodSeconds + - lengthEqual: + path: spec.template.spec.containers + count: 1 + - equal: + path: spec.template.metadata.labels + value: + app.kubernetes.io/name: litellm + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: gateway + + - it: gateway renders pod-level and container-level securityContext in their own scopes + template: gateway/deployment.yaml + set: + gateway.podSecurityContext: + runAsNonRoot: true + runAsUser: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + gateway.securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + asserts: + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + runAsUser: 65532 + fsGroup: 65532 + seccompProfile: + type: RuntimeDefault + - equal: + path: spec.template.spec.containers[0].securityContext + value: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: true + capabilities: + drop: + - ALL + + - it: gateway merges podLabels with the selector labels + template: gateway/deployment.yaml + set: + gateway.podLabels: + egress-policy: restricted + team: platform + asserts: + - equal: + path: spec.template.metadata.labels + value: + app.kubernetes.io/name: litellm + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: gateway + egress-policy: restricted + team: platform + - equal: + path: spec.selector.matchLabels + value: + app.kubernetes.io/name: litellm + app.kubernetes.io/instance: RELEASE-NAME + app.kubernetes.io/component: gateway + + - it: gateway rejects a podLabel that collides with the selector + template: gateway/deployment.yaml + set: + gateway.podLabels: + app.kubernetes.io/component: not-gateway + asserts: + - failedTemplate: + errorMessage: "gateway.podLabels cannot set app.kubernetes.io/component: it is part of the Deployment's immutable selector" + + - it: backend rejects a podLabel that collides with the selector + template: backend/deployment.yaml + set: + backend.podLabels: + app.kubernetes.io/name: not-litellm + asserts: + - failedTemplate: + errorMessage: "backend.podLabels cannot set app.kubernetes.io/name: it is part of the Deployment's immutable selector" + + - it: ui rejects a podLabel that collides with the selector + template: ui/deployment.yaml + set: + ui.podLabels: + app.kubernetes.io/instance: not-the-release + asserts: + - failedTemplate: + errorMessage: "ui.podLabels cannot set app.kubernetes.io/instance: it is part of the Deployment's immutable selector" + + - it: gateway renders lifecycle hooks on the container + template: gateway/deployment.yaml + set: + gateway.lifecycle: + preStop: + httpGet: + path: /health/drain + port: 4000 + asserts: + - equal: + path: spec.template.spec.containers[0].lifecycle + value: + preStop: + httpGet: + path: /health/drain + port: 4000 + + - it: gateway renders terminationGracePeriodSeconds on the pod spec + template: gateway/deployment.yaml + set: + gateway.terminationGracePeriodSeconds: 90 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 90 + + - it: gateway honors an explicit terminationGracePeriodSeconds of zero + template: gateway/deployment.yaml + set: + gateway.terminationGracePeriodSeconds: 0 + asserts: + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 0 + + - it: gateway appends extraContainers after the gateway container + template: gateway/deployment.yaml + set: + gateway.extraContainers: + - name: auth-sidecar + image: registry.example.com/auth-proxy:1.2.3 + args: + - --upstream + - http://127.0.0.1:4000 + asserts: + - lengthEqual: + path: spec.template.spec.containers + count: 2 + - equal: + path: spec.template.spec.containers[0].name + value: gateway + - equal: + path: spec.template.spec.containers[1] + value: + name: auth-sidecar + image: registry.example.com/auth-proxy:1.2.3 + args: + - --upstream + - http://127.0.0.1:4000 + + - it: gateway templates chart context inside extraContainers + template: gateway/deployment.yaml + set: + gateway.extraContainers: + - name: auth-sidecar + image: registry.example.com/auth-proxy:1.2.3 + env: + - name: RELEASE + value: "{{ .Release.Name }}" + asserts: + - equal: + path: spec.template.spec.containers[1].env[0].value + value: RELEASE-NAME + + - it: backend renders every hardening knob in the right scope + template: backend/deployment.yaml + set: + backend.podLabels: + egress-policy: restricted + backend.podSecurityContext: + runAsNonRoot: true + backend.securityContext: + readOnlyRootFilesystem: true + backend.lifecycle: + preStop: + exec: + command: + - sleep + - "5" + backend.terminationGracePeriodSeconds: 60 + backend.extraContainers: + - name: auth-sidecar + image: registry.example.com/auth-proxy:1.2.3 + asserts: + - equal: + path: spec.template.metadata.labels['egress-policy'] + value: restricted + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + - equal: + path: spec.template.spec.containers[0].securityContext + value: + readOnlyRootFilesystem: true + - equal: + path: spec.template.spec.containers[0].lifecycle + value: + preStop: + exec: + command: + - sleep + - "5" + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 60 + - equal: + path: spec.template.spec.containers[1].name + value: auth-sidecar + + - it: ui renders every hardening knob in the right scope + template: ui/deployment.yaml + set: + ui.podLabels: + egress-policy: restricted + ui.podSecurityContext: + runAsNonRoot: true + fsGroup: 101 + ui.securityContext: + readOnlyRootFilesystem: true + ui.lifecycle: + preStop: + exec: + command: + - /bin/sh + - -c + - nginx -s quit + ui.terminationGracePeriodSeconds: 30 + ui.extraContainers: + - name: auth-sidecar + image: registry.example.com/auth-proxy:1.2.3 + asserts: + - equal: + path: spec.template.metadata.labels['egress-policy'] + value: restricted + - equal: + path: spec.template.spec.securityContext + value: + runAsNonRoot: true + fsGroup: 101 + - equal: + path: spec.template.spec.containers[0].securityContext + value: + readOnlyRootFilesystem: true + - equal: + path: spec.template.spec.containers[0].lifecycle + value: + preStop: + exec: + command: + - /bin/sh + - -c + - nginx -s quit + - equal: + path: spec.template.spec.terminationGracePeriodSeconds + value: 30 + - equal: + path: spec.template.spec.containers[1].name + value: auth-sidecar + + - it: backend and ui render no hardening fields by default + templates: + - backend/deployment.yaml + - ui/deployment.yaml + asserts: + - isNull: + path: spec.template.spec.securityContext + - isNull: + path: spec.template.spec.containers[0].securityContext + - isNull: + path: spec.template.spec.containers[0].lifecycle + - isNull: + path: spec.template.spec.terminationGracePeriodSeconds + - lengthEqual: + path: spec.template.spec.containers + count: 1 diff --git a/helm/litellm/values.yaml b/helm/litellm/values.yaml index 461935b2f50..48e55a6805b 100644 --- a/helm/litellm/values.yaml +++ b/helm/litellm/values.yaml @@ -57,6 +57,42 @@ migrationJob: backoffLimit: 4 ttlSecondsAfterFinished: 120 resources: {} + # ServiceAccount for the Job pod only. + # + # The Job is a pre-install / pre-upgrade hook, so it runs before the chart's + # ordinary resources exist. With `serviceAccounts.backend.create: true` the + # backend ServiceAccount is one of those ordinary resources, so a Job that + # borrowed its name would reference an account that does not exist yet and + # the first install would fail with a forbidden pod creation. The name set + # here always wins; when it is empty the Job falls back to `default` if the + # chart creates the backend ServiceAccount, and to the backend + # ServiceAccount name otherwise (that name is either an existing account you + # supplied or `default`). + # + # Point this at a pre-existing ServiceAccount when the Job needs credentials + # of its own, e.g. the IRSA / Workload Identity annotations that + # `database.writer.useIAMAuth` relies on. That is also the upgrade path to + # watch: a release already running with `serviceAccounts.backend.create: + # true` used to hand the Job the created backend account on every upgrade, + # and now hands it `default` unless you name an account here. + serviceAccountName: "" + # The Job runs `prisma migrate deploy` against Postgres and never calls the + # K8s API, so it defaults to no projected ServiceAccount token, the same + # reasoning the ui SA above uses. Flip to true if your Job genuinely needs + # one; IAM database auth does not, since EKS Pod Identity injects its own + # projected token volume and GKE Workload Identity goes through the + # metadata server, neither of which is the default token mount. + automountServiceAccountToken: false + # Standard k8s pod-level and container-level securityContext for the Job + # pod. Same shape as gateway.podSecurityContext / gateway.securityContext. + podSecurityContext: {} + securityContext: {} + # Extra pod labels on the Job pod, merged into the chart's common labels. + podLabels: {} + # Additional volumes on the Job pod and volumeMounts on its container, e.g. + # the writable scratch space a read-only root filesystem needs. + volumes: [] + volumeMounts: [] image: repository: ghcr.io/berriai/litellm-migrations tag: "" # defaults to .Chart.AppVersion @@ -200,6 +236,37 @@ gateway: minAvailable: "" maxUnavailable: "" podAnnotations: {} + # Extra pod labels, merged into the chart's selector labels. Do not + # re-declare `app.kubernetes.io/name` / `instance` / `component` here: they + # form the Deployment's immutable selector. + podLabels: {} + # Pod-level securityContext, applied to every container in the pod + # (runAsNonRoot, runAsUser, fsGroup, seccompProfile, ...). Empty by default + # so the cluster's own defaults keep applying to existing installs; clusters + # enforcing a restricted Pod Security Standard usually want at least + # `runAsNonRoot: true` and `seccompProfile.type: RuntimeDefault`. + podSecurityContext: {} + # Container-level securityContext for the gateway container. Empty by + # default for the same reason. Example: + # allowPrivilegeEscalation: false + # readOnlyRootFilesystem: true + # capabilities: + # drop: + # - ALL + # `readOnlyRootFilesystem: true` needs writable scratch space; supply it + # through `volumes` / `volumeMounts` above rather than expecting the chart + # to guess the paths your workload writes to. + securityContext: {} + # Extra sidecar containers appended to the gateway pod, e.g. an auth or + # egress proxy. Rendered through `tpl`, so entries may reference chart + # values and release metadata. + extraContainers: [] + # Container lifecycle hooks (postStart / preStop) for the gateway container. + lifecycle: {} + # Grace period the kubelet allows between SIGTERM and SIGKILL. Leave empty + # to inherit the Kubernetes default of 30s. Set it a few seconds above the + # proxy's GRACEFUL_SHUTDOWN_TIMEOUT when you use a draining preStop hook. + terminationGracePeriodSeconds: "" nodeSelector: {} tolerations: [] affinity: {} @@ -257,6 +324,13 @@ backend: minAvailable: "" maxUnavailable: "" podAnnotations: {} + # Same shape as the gateway blocks of the same name. + podLabels: {} + podSecurityContext: {} + securityContext: {} + extraContainers: [] + lifecycle: {} + terminationGracePeriodSeconds: "" nodeSelector: {} tolerations: [] affinity: {} @@ -310,6 +384,16 @@ ui: minAvailable: "" maxUnavailable: "" podAnnotations: {} + # Same shape as the gateway blocks of the same name. The nginx runtime + # writes its pid, cache, and proxy temp files under the image's root + # filesystem, so `securityContext.readOnlyRootFilesystem: true` here needs + # emptyDir volumes mounted over those paths. + podLabels: {} + podSecurityContext: {} + securityContext: {} + extraContainers: [] + lifecycle: {} + terminationGracePeriodSeconds: "" nodeSelector: {} tolerations: [] affinity: {}