From 872d05e8d206adbfb0dbbebfe94d31fdb8346db8 Mon Sep 17 00:00:00 2001 From: Max Xing Date: Thu, 23 Jul 2026 20:02:47 -0700 Subject: [PATCH 1/2] fix(icms-translate): run LLM sidecars as a shared UID so pylon can read the worker-token The llm-credential-manager writes the worker auth token to the shared "llm" emptyDir with mode 0600, but pylon (llm-router-client) runs as the distroless non-root user nvs (1000) while the credential-manager runs as root, so pylon cannot read it. Registration fails at token read before pylon dials the router, leaving the model unregistered and gateway invokes returning no_eligible_candidates. Render both sidecars with runAsUser/runAsGroup 1000 so they share the token owner while keeping 0600; the customer workload container runs as its own user and still cannot read it. Regenerate the container/llm and helmchart/llm goldens and sync the NVCA vendored copy. NO-REF --- .../icms-translate/translate/function/llm.go | 17 +++++++++++ .../icms-translate/translate/function/llm.go | 17 +++++++++++ .../translate/function/llm_test.go | 28 +++++++++++++++++++ .../function/container/llm/exp.yaml | 6 ++++ .../function/helmchart/llm/exp.yaml | 6 ++++ 5 files changed, 74 insertions(+) diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go index 21ec72965..2f0890249 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go @@ -40,8 +40,23 @@ const ( llmDirMountPath = "/var/run/llm" llmWorkerTokenPath = llmDirMountPath + "/worker-token" + + // pylon's image user (nvs). Both LLM sidecars run as this UID so pylon can + // read the 0600 worker-token the credential-manager writes to the shared + // emptyDir; the customer container runs as its own user and cannot. + llmSidecarRunAsUser int64 = 1000 ) +// newLLMSidecarSecurityContext runs both LLM sidecars as the same UID so they +// can exchange the worker-token on the shared emptyDir. +func newLLMSidecarSecurityContext() *corev1.SecurityContext { + uid := llmSidecarRunAsUser + return &corev1.SecurityContext{ + RunAsUser: &uid, + RunAsGroup: &uid, + } +} + func normalizeLLMRequestRouterAddressEnvAliases(envSet map[string]string) { llmRequestRouterAddress := envSet[llmRequestRouterAddressEnv] if llmRequestRouterAddress == "" { @@ -143,6 +158,7 @@ func newLLMRouterClientContainer( Name: LLMWorkerContainerName, Image: llmRouterClientImage, ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: newLLMSidecarSecurityContext(), Args: args, Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ @@ -217,6 +233,7 @@ func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateCo Name: "llm-credential-manager", Image: llmCredentialManagerImage, ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: newLLMSidecarSecurityContext(), Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go index 21ec72965..2f0890249 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go @@ -40,8 +40,23 @@ const ( llmDirMountPath = "/var/run/llm" llmWorkerTokenPath = llmDirMountPath + "/worker-token" + + // pylon's image user (nvs). Both LLM sidecars run as this UID so pylon can + // read the 0600 worker-token the credential-manager writes to the shared + // emptyDir; the customer container runs as its own user and cannot. + llmSidecarRunAsUser int64 = 1000 ) +// newLLMSidecarSecurityContext runs both LLM sidecars as the same UID so they +// can exchange the worker-token on the shared emptyDir. +func newLLMSidecarSecurityContext() *corev1.SecurityContext { + uid := llmSidecarRunAsUser + return &corev1.SecurityContext{ + RunAsUser: &uid, + RunAsGroup: &uid, + } +} + func normalizeLLMRequestRouterAddressEnvAliases(envSet map[string]string) { llmRequestRouterAddress := envSet[llmRequestRouterAddressEnv] if llmRequestRouterAddress == "" { @@ -143,6 +158,7 @@ func newLLMRouterClientContainer( Name: LLMWorkerContainerName, Image: llmRouterClientImage, ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: newLLMSidecarSecurityContext(), Args: args, Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ @@ -217,6 +233,7 @@ func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateCo Name: "llm-credential-manager", Image: llmCredentialManagerImage, ImagePullPolicy: corev1.PullIfNotPresent, + SecurityContext: newLLMSidecarSecurityContext(), Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go index 557e701e7..f6aa21b97 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go @@ -431,3 +431,31 @@ func envSliceToMap(envs []corev1.EnvVar) map[string]string { } return m } + +// TestLLMSidecarsShareRunAsUser guards the fix for the worker-token permission +// bug: the credential-manager writes the token 0600, so it and pylon must run +// as the same non-root UID for pylon to read it (and to keep it from the +// customer workload container). +func TestLLMSidecarsShareRunAsUser(t *testing.T) { + allEnvSet := map[string]string{ + "LLM_REQUEST_ROUTER_ADDRESS": "llm-router.example.com:443", + "INFERENCE_PORT": "8080", + } + + pylon, err := newLLMRouterClientContainer(&LaunchSpecification{}, allEnvSet, TranslateConfig{}, "inst-123", false) + require.NoError(t, err) + cred, err := newLLMCredentialManagerContainer(allEnvSet, TranslateConfig{}) + require.NoError(t, err) + + for name, sc := range map[string]*corev1.SecurityContext{"pylon": pylon.SecurityContext, "credential-manager": cred.SecurityContext} { + require.NotNilf(t, sc, "%s must set a security context", name) + require.NotNilf(t, sc.RunAsUser, "%s must set runAsUser", name) + assert.Equalf(t, llmSidecarRunAsUser, *sc.RunAsUser, "%s runAsUser", name) + require.NotNilf(t, sc.RunAsGroup, "%s must set runAsGroup", name) + assert.Equalf(t, llmSidecarRunAsUser, *sc.RunAsGroup, "%s runAsGroup", name) + assert.NotZerof(t, *sc.RunAsUser, "%s must be non-root", name) + } + + assert.Equal(t, *pylon.SecurityContext.RunAsUser, *cred.SecurityContext.RunAsUser, + "both sidecars must share a UID so pylon can read the credential-manager's 0600 token") +} diff --git a/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml b/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml index 9c416a60f..3d4ff7c3f 100644 --- a/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml +++ b/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml @@ -276,6 +276,9 @@ spec: requests: cpu: 500m memory: 512Mi + securityContext: + runAsGroup: 1000 + runAsUser: 1000 volumeMounts: - mountPath: /var/run/llm name: llm @@ -342,6 +345,9 @@ spec: requests: cpu: 125m memory: 64Mi + securityContext: + runAsGroup: 1000 + runAsUser: 1000 volumeMounts: - mountPath: /var/run/llm name: llm diff --git a/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml b/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml index 41484e5c6..5c8104814 100644 --- a/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml +++ b/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml @@ -244,6 +244,9 @@ spec: requests: cpu: 500m memory: 512Mi + securityContext: + runAsGroup: 1000 + runAsUser: 1000 volumeMounts: - mountPath: /var/run/llm name: llm @@ -310,6 +313,9 @@ spec: requests: cpu: 125m memory: 64Mi + securityContext: + runAsGroup: 1000 + runAsUser: 1000 volumeMounts: - mountPath: /var/run/llm name: llm From 67f2cf2bc4d3fd6300858f8394c7582e84535603 Mon Sep 17 00:00:00 2001 From: Max Xing Date: Fri, 24 Jul 2026 10:12:12 -0700 Subject: [PATCH 2/2] fix(icms-translate): make LLM sidecar UID configurable and set pod fsGroup Addressing review. Hardcoding runAsUser 1000 conflicts with OpenShift SCC UID-range admission, and the non-root sidecars need the shared emptyDir writable. - Add TranslateConfig.LLMSidecarRunAsUser and the NVCA WorkloadConfig field. Defaults to 1000; a negative value leaves runAsUser unset so the platform (e.g. OpenShift) can assign a UID from its allowed range. - Set the pod fsGroup to the same UID so the credential-manager can write the worker-token to the shared emptyDir. NO-REF --- .../miniservice/translate_workload.go | 1 + .../nvca/pkg/nvca/k8scomputebackend.go | 1 + .../nvca/pkg/storage/translate_workload.go | 1 + .../icms-translate/translate/function/llm.go | 52 +++++++++++--- .../translate/function/translate.go | 4 ++ .../translate/function/translate_container.go | 1 + .../translate/function/translate_helm.go | 1 + .../go/lib/pkg/types/nvca/config/types.go | 4 ++ .../nvca/config/zz_generated.deepcopy.go | 5 ++ .../icms-translate/translate/function/llm.go | 52 +++++++++++--- .../translate/function/llm_test.go | 68 ++++++++++++++----- .../translate/function/translate.go | 4 ++ .../translate/function/translate_container.go | 1 + .../translate/function/translate_helm.go | 1 + .../go/lib/pkg/types/nvca/config/types.go | 4 ++ .../nvca/config/zz_generated.deepcopy.go | 5 ++ .../function/container/llm/exp.yaml | 2 + .../function/helmchart/llm/exp.yaml | 2 + 18 files changed, 171 insertions(+), 38 deletions(-) diff --git a/src/compute-plane-services/nvca/internal/miniservice/translate_workload.go b/src/compute-plane-services/nvca/internal/miniservice/translate_workload.go index 703ab3dbf..6bafd232a 100644 --- a/src/compute-plane-services/nvca/internal/miniservice/translate_workload.go +++ b/src/compute-plane-services/nvca/internal/miniservice/translate_workload.go @@ -89,6 +89,7 @@ func (r *Reconciler) translateFunctionWorkload( TranslateConfig: cmnTCfg, DefaultStargateAddress: r.cfg.Workload.DefaultStargateAddress, StargateQUICInsecure: r.cfg.Workload.StargateQUICInsecure, + LLMSidecarRunAsUser: r.cfg.Workload.LLMSidecarRunAsUser, } msg := function.CreationQueueMessage{ diff --git a/src/compute-plane-services/nvca/pkg/nvca/k8scomputebackend.go b/src/compute-plane-services/nvca/pkg/nvca/k8scomputebackend.go index f0d18f43a..fa1a29dd6 100644 --- a/src/compute-plane-services/nvca/pkg/nvca/k8scomputebackend.go +++ b/src/compute-plane-services/nvca/pkg/nvca/k8scomputebackend.go @@ -285,6 +285,7 @@ func (c K8sComputeBackend) translateFunctionLaunchSpecification( }, DefaultStargateAddress: c.bk8s.cfg.Workload.DefaultStargateAddress, StargateQUICInsecure: c.bk8s.cfg.Workload.StargateQUICInsecure, + LLMSidecarRunAsUser: c.bk8s.cfg.Workload.LLMSidecarRunAsUser, } if reqType == ftContainer { diff --git a/src/compute-plane-services/nvca/pkg/storage/translate_workload.go b/src/compute-plane-services/nvca/pkg/storage/translate_workload.go index 5d39e3079..85037892a 100644 --- a/src/compute-plane-services/nvca/pkg/storage/translate_workload.go +++ b/src/compute-plane-services/nvca/pkg/storage/translate_workload.go @@ -85,6 +85,7 @@ func (r *Reconciler) translateFunctionWorkload( TranslateConfig: cmnTCfg, DefaultStargateAddress: r.cfg.Workload.DefaultStargateAddress, StargateQUICInsecure: r.cfg.Workload.StargateQUICInsecure, + LLMSidecarRunAsUser: r.cfg.Workload.LLMSidecarRunAsUser, } msg := function.CreationQueueMessage{ diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go index 2f0890249..844eb72b0 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go @@ -41,20 +41,50 @@ const ( llmDirMountPath = "/var/run/llm" llmWorkerTokenPath = llmDirMountPath + "/worker-token" - // pylon's image user (nvs). Both LLM sidecars run as this UID so pylon can - // read the 0600 worker-token the credential-manager writes to the shared - // emptyDir; the customer container runs as its own user and cannot. - llmSidecarRunAsUser int64 = 1000 + // pylon's image user (nvs); both LLM sidecars share it so pylon can read + // the credential-manager's 0600 worker-token. Override via + // TranslateConfig.LLMSidecarRunAsUser (negative = unset for OpenShift SCC). + defaultLLMSidecarRunAsUser int64 = 1000 ) +// resolveLLMSidecarRunAsUser returns the shared sidecar UID, or nil to leave +// it unset for the platform to assign. +func resolveLLMSidecarRunAsUser(tcfg TranslateConfig) *int64 { + if tcfg.LLMSidecarRunAsUser == nil { + uid := defaultLLMSidecarRunAsUser + return &uid + } + if *tcfg.LLMSidecarRunAsUser < 0 { + return nil + } + return tcfg.LLMSidecarRunAsUser +} + // newLLMSidecarSecurityContext runs both LLM sidecars as the same UID so they // can exchange the worker-token on the shared emptyDir. -func newLLMSidecarSecurityContext() *corev1.SecurityContext { - uid := llmSidecarRunAsUser +func newLLMSidecarSecurityContext(tcfg TranslateConfig) *corev1.SecurityContext { + uid := resolveLLMSidecarRunAsUser(tcfg) + if uid == nil { + return nil + } return &corev1.SecurityContext{ - RunAsUser: &uid, - RunAsGroup: &uid, + RunAsUser: uid, + RunAsGroup: uid, + } +} + +// applyLLMPodSecurityContext sets the pod fsGroup to the sidecar UID so the +// non-root sidecars can write the shared worker-token emptyDir. No-op when the +// UID is platform-managed. +func applyLLMPodSecurityContext(pod *corev1.Pod, tcfg TranslateConfig) { + uid := resolveLLMSidecarRunAsUser(tcfg) + if uid == nil { + return + } + if pod.Spec.SecurityContext == nil { + pod.Spec.SecurityContext = &corev1.PodSecurityContext{} } + pod.Spec.SecurityContext.FSGroup = uid } func normalizeLLMRequestRouterAddressEnvAliases(envSet map[string]string) { @@ -158,7 +188,7 @@ func newLLMRouterClientContainer( Name: LLMWorkerContainerName, Image: llmRouterClientImage, ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: newLLMSidecarSecurityContext(), + SecurityContext: newLLMSidecarSecurityContext(tcfg), Args: args, Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ @@ -190,7 +220,7 @@ func newLLMRouterClientContainer( return c, nil } -func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateConfig) (corev1.Container, error) { +func newLLMCredentialManagerContainer(allEnvSet map[string]string, tcfg TranslateConfig) (corev1.Container, error) { llmCredentialManagerImage := allEnvSet[llmCredentialManagerImageEnv] if llmCredentialManagerImage == "" { llmCredentialManagerImage = llmCredentialManagerImageDefault @@ -233,7 +263,7 @@ func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateCo Name: "llm-credential-manager", Image: llmCredentialManagerImage, ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: newLLMSidecarSecurityContext(), + SecurityContext: newLLMSidecarSecurityContext(tcfg), Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go index 4042a38bf..ec6f26b01 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go @@ -46,6 +46,10 @@ type TranslateConfig struct { // Whether to use QUIC insecure mode for stargate connections // If not set, "--quick-insecure" is omitted StargateQUICInsecure bool `json:"stargateQUICInsecure,omitempty"` + + // LLMSidecarRunAsUser is the UID both LLM sidecars run as (default 1000). + // Negative leaves runAsUser unset so the platform (e.g. OpenShift) assigns. + LLMSidecarRunAsUser *int64 `json:"llmSidecarRunAsUser,omitempty"` } func Translate(t CreationQueueMessage, tcfg TranslateConfig) (objs []metav1.Object, err error) { diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go index d00614633..d92ed1040 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go @@ -446,6 +446,7 @@ func translateContainer(t CreationQueueMessage, tcfg TranslateConfig) (objs []me return nil, err } pod.Spec.Containers = append(pod.Spec.Containers, llmCredentialManagerContainer) + applyLLMPodSecurityContext(pod, tcfg) } // Setup telemetry for the pod diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go index ef526fef5..4c3ff0dc7 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go @@ -366,6 +366,7 @@ func translateHelmChart(t CreationQueueMessage, tcfg TranslateConfig) (objs []me return nil, err } utilsPod.Spec.Containers = append(utilsPod.Spec.Containers, llmCredentialManagerContainer) + applyLLMPodSecurityContext(utilsPod, tcfg) } if hasTelemetries { diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/types.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/types.go index 50d6d9456..ed9f2b8e8 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/types.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/types.go @@ -704,6 +704,10 @@ type WorkloadConfig struct { DefaultStargateAddress string `yaml:",omitempty"` StargateQUICInsecure bool `yaml:",omitempty"` + // LLMSidecarRunAsUser is the UID both LLM sidecars run as (default 1000). + // Negative leaves runAsUser unset so the platform (e.g. OpenShift) assigns. + LLMSidecarRunAsUser *int64 `yaml:",omitempty"` + // TransportTLS configures trust material used by workload transport clients. TransportTLS *TransportTLSConfig `yaml:",omitempty"` } diff --git a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go index 2924ec332..8e39a8db8 100644 --- a/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go +++ b/src/compute-plane-services/nvca/vendor/github.com/NVIDIA/nvcf/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go @@ -695,6 +695,11 @@ func (in *WorkloadConfig) DeepCopyInto(out *WorkloadConfig) { (*out)[key] = val } } + if in.LLMSidecarRunAsUser != nil { + in, out := &in.LLMSidecarRunAsUser, &out.LLMSidecarRunAsUser + *out = new(int64) + **out = **in + } if in.TransportTLS != nil { in, out := &in.TransportTLS, &out.TransportTLS *out = new(TransportTLSConfig) diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go index 2f0890249..844eb72b0 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm.go @@ -41,20 +41,50 @@ const ( llmDirMountPath = "/var/run/llm" llmWorkerTokenPath = llmDirMountPath + "/worker-token" - // pylon's image user (nvs). Both LLM sidecars run as this UID so pylon can - // read the 0600 worker-token the credential-manager writes to the shared - // emptyDir; the customer container runs as its own user and cannot. - llmSidecarRunAsUser int64 = 1000 + // pylon's image user (nvs); both LLM sidecars share it so pylon can read + // the credential-manager's 0600 worker-token. Override via + // TranslateConfig.LLMSidecarRunAsUser (negative = unset for OpenShift SCC). + defaultLLMSidecarRunAsUser int64 = 1000 ) +// resolveLLMSidecarRunAsUser returns the shared sidecar UID, or nil to leave +// it unset for the platform to assign. +func resolveLLMSidecarRunAsUser(tcfg TranslateConfig) *int64 { + if tcfg.LLMSidecarRunAsUser == nil { + uid := defaultLLMSidecarRunAsUser + return &uid + } + if *tcfg.LLMSidecarRunAsUser < 0 { + return nil + } + return tcfg.LLMSidecarRunAsUser +} + // newLLMSidecarSecurityContext runs both LLM sidecars as the same UID so they // can exchange the worker-token on the shared emptyDir. -func newLLMSidecarSecurityContext() *corev1.SecurityContext { - uid := llmSidecarRunAsUser +func newLLMSidecarSecurityContext(tcfg TranslateConfig) *corev1.SecurityContext { + uid := resolveLLMSidecarRunAsUser(tcfg) + if uid == nil { + return nil + } return &corev1.SecurityContext{ - RunAsUser: &uid, - RunAsGroup: &uid, + RunAsUser: uid, + RunAsGroup: uid, + } +} + +// applyLLMPodSecurityContext sets the pod fsGroup to the sidecar UID so the +// non-root sidecars can write the shared worker-token emptyDir. No-op when the +// UID is platform-managed. +func applyLLMPodSecurityContext(pod *corev1.Pod, tcfg TranslateConfig) { + uid := resolveLLMSidecarRunAsUser(tcfg) + if uid == nil { + return + } + if pod.Spec.SecurityContext == nil { + pod.Spec.SecurityContext = &corev1.PodSecurityContext{} } + pod.Spec.SecurityContext.FSGroup = uid } func normalizeLLMRequestRouterAddressEnvAliases(envSet map[string]string) { @@ -158,7 +188,7 @@ func newLLMRouterClientContainer( Name: LLMWorkerContainerName, Image: llmRouterClientImage, ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: newLLMSidecarSecurityContext(), + SecurityContext: newLLMSidecarSecurityContext(tcfg), Args: args, Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ @@ -190,7 +220,7 @@ func newLLMRouterClientContainer( return c, nil } -func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateConfig) (corev1.Container, error) { +func newLLMCredentialManagerContainer(allEnvSet map[string]string, tcfg TranslateConfig) (corev1.Container, error) { llmCredentialManagerImage := allEnvSet[llmCredentialManagerImageEnv] if llmCredentialManagerImage == "" { llmCredentialManagerImage = llmCredentialManagerImageDefault @@ -233,7 +263,7 @@ func newLLMCredentialManagerContainer(allEnvSet map[string]string, _ TranslateCo Name: "llm-credential-manager", Image: llmCredentialManagerImage, ImagePullPolicy: corev1.PullIfNotPresent, - SecurityContext: newLLMSidecarSecurityContext(), + SecurityContext: newLLMSidecarSecurityContext(tcfg), Env: common.SortEnvs(envs), Resources: corev1.ResourceRequirements{ Requests: corev1.ResourceList{ diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go index f6aa21b97..276b4b854 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/llm_test.go @@ -434,28 +434,64 @@ func envSliceToMap(envs []corev1.EnvVar) map[string]string { // TestLLMSidecarsShareRunAsUser guards the fix for the worker-token permission // bug: the credential-manager writes the token 0600, so it and pylon must run -// as the same non-root UID for pylon to read it (and to keep it from the -// customer workload container). +// as the same UID for pylon to read it. The UID is configurable for platforms +// (e.g. OpenShift) whose SCC stipulates the allowed range. func TestLLMSidecarsShareRunAsUser(t *testing.T) { allEnvSet := map[string]string{ "LLM_REQUEST_ROUTER_ADDRESS": "llm-router.example.com:443", "INFERENCE_PORT": "8080", } + ptr := func(v int64) *int64 { return &v } + build := func(t *testing.T, tcfg TranslateConfig) (corev1.Container, corev1.Container) { + t.Helper() + pylon, err := newLLMRouterClientContainer(&LaunchSpecification{}, allEnvSet, tcfg, "inst-123", false) + require.NoError(t, err) + cred, err := newLLMCredentialManagerContainer(allEnvSet, tcfg) + require.NoError(t, err) + return pylon, cred + } + + t.Run("default is the shared non-root pylon UID", func(t *testing.T) { + pylon, cred := build(t, TranslateConfig{}) + for name, sc := range map[string]*corev1.SecurityContext{"pylon": pylon.SecurityContext, "credential-manager": cred.SecurityContext} { + require.NotNilf(t, sc, "%s security context", name) + require.NotNilf(t, sc.RunAsUser, "%s runAsUser", name) + assert.Equalf(t, defaultLLMSidecarRunAsUser, *sc.RunAsUser, "%s runAsUser", name) + require.NotNilf(t, sc.RunAsGroup, "%s runAsGroup", name) + assert.Equalf(t, defaultLLMSidecarRunAsUser, *sc.RunAsGroup, "%s runAsGroup", name) + assert.NotZerof(t, *sc.RunAsUser, "%s must be non-root", name) + } + }) - pylon, err := newLLMRouterClientContainer(&LaunchSpecification{}, allEnvSet, TranslateConfig{}, "inst-123", false) - require.NoError(t, err) - cred, err := newLLMCredentialManagerContainer(allEnvSet, TranslateConfig{}) - require.NoError(t, err) + t.Run("configured UID overrides the default on both sidecars", func(t *testing.T) { + pylon, cred := build(t, TranslateConfig{LLMSidecarRunAsUser: ptr(2000)}) + require.NotNil(t, pylon.SecurityContext.RunAsUser) + require.NotNil(t, cred.SecurityContext.RunAsUser) + assert.Equal(t, int64(2000), *pylon.SecurityContext.RunAsUser) + assert.Equal(t, *pylon.SecurityContext.RunAsUser, *cred.SecurityContext.RunAsUser) + }) - for name, sc := range map[string]*corev1.SecurityContext{"pylon": pylon.SecurityContext, "credential-manager": cred.SecurityContext} { - require.NotNilf(t, sc, "%s must set a security context", name) - require.NotNilf(t, sc.RunAsUser, "%s must set runAsUser", name) - assert.Equalf(t, llmSidecarRunAsUser, *sc.RunAsUser, "%s runAsUser", name) - require.NotNilf(t, sc.RunAsGroup, "%s must set runAsGroup", name) - assert.Equalf(t, llmSidecarRunAsUser, *sc.RunAsGroup, "%s runAsGroup", name) - assert.NotZerof(t, *sc.RunAsUser, "%s must be non-root", name) - } + t.Run("negative UID leaves runAsUser unset for the platform", func(t *testing.T) { + pylon, cred := build(t, TranslateConfig{LLMSidecarRunAsUser: ptr(-1)}) + assert.Nil(t, pylon.SecurityContext) + assert.Nil(t, cred.SecurityContext) + }) +} + +func TestApplyLLMPodSecurityContext(t *testing.T) { + ptr := func(v int64) *int64 { return &v } + + t.Run("sets fsGroup to the sidecar UID", func(t *testing.T) { + pod := &corev1.Pod{} + applyLLMPodSecurityContext(pod, TranslateConfig{}) + require.NotNil(t, pod.Spec.SecurityContext) + require.NotNil(t, pod.Spec.SecurityContext.FSGroup) + assert.Equal(t, defaultLLMSidecarRunAsUser, *pod.Spec.SecurityContext.FSGroup) + }) - assert.Equal(t, *pylon.SecurityContext.RunAsUser, *cred.SecurityContext.RunAsUser, - "both sidecars must share a UID so pylon can read the credential-manager's 0600 token") + t.Run("negative UID leaves the pod security context unset", func(t *testing.T) { + pod := &corev1.Pod{} + applyLLMPodSecurityContext(pod, TranslateConfig{LLMSidecarRunAsUser: ptr(-1)}) + assert.Nil(t, pod.Spec.SecurityContext) + }) } diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go index 4042a38bf..ec6f26b01 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate.go @@ -46,6 +46,10 @@ type TranslateConfig struct { // Whether to use QUIC insecure mode for stargate connections // If not set, "--quick-insecure" is omitted StargateQUICInsecure bool `json:"stargateQUICInsecure,omitempty"` + + // LLMSidecarRunAsUser is the UID both LLM sidecars run as (default 1000). + // Negative leaves runAsUser unset so the platform (e.g. OpenShift) assigns. + LLMSidecarRunAsUser *int64 `json:"llmSidecarRunAsUser,omitempty"` } func Translate(t CreationQueueMessage, tcfg TranslateConfig) (objs []metav1.Object, err error) { diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go index d00614633..d92ed1040 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_container.go @@ -446,6 +446,7 @@ func translateContainer(t CreationQueueMessage, tcfg TranslateConfig) (objs []me return nil, err } pod.Spec.Containers = append(pod.Spec.Containers, llmCredentialManagerContainer) + applyLLMPodSecurityContext(pod, tcfg) } // Setup telemetry for the pod diff --git a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go index ef526fef5..4c3ff0dc7 100644 --- a/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go +++ b/src/libraries/go/lib/pkg/icms-translate/translate/function/translate_helm.go @@ -366,6 +366,7 @@ func translateHelmChart(t CreationQueueMessage, tcfg TranslateConfig) (objs []me return nil, err } utilsPod.Spec.Containers = append(utilsPod.Spec.Containers, llmCredentialManagerContainer) + applyLLMPodSecurityContext(utilsPod, tcfg) } if hasTelemetries { diff --git a/src/libraries/go/lib/pkg/types/nvca/config/types.go b/src/libraries/go/lib/pkg/types/nvca/config/types.go index 50d6d9456..ed9f2b8e8 100644 --- a/src/libraries/go/lib/pkg/types/nvca/config/types.go +++ b/src/libraries/go/lib/pkg/types/nvca/config/types.go @@ -704,6 +704,10 @@ type WorkloadConfig struct { DefaultStargateAddress string `yaml:",omitempty"` StargateQUICInsecure bool `yaml:",omitempty"` + // LLMSidecarRunAsUser is the UID both LLM sidecars run as (default 1000). + // Negative leaves runAsUser unset so the platform (e.g. OpenShift) assigns. + LLMSidecarRunAsUser *int64 `yaml:",omitempty"` + // TransportTLS configures trust material used by workload transport clients. TransportTLS *TransportTLSConfig `yaml:",omitempty"` } diff --git a/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go b/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go index 2924ec332..8e39a8db8 100644 --- a/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go +++ b/src/libraries/go/lib/pkg/types/nvca/config/zz_generated.deepcopy.go @@ -695,6 +695,11 @@ func (in *WorkloadConfig) DeepCopyInto(out *WorkloadConfig) { (*out)[key] = val } } + if in.LLMSidecarRunAsUser != nil { + in, out := &in.LLMSidecarRunAsUser, &out.LLMSidecarRunAsUser + *out = new(int64) + **out = **in + } if in.TransportTLS != nil { in, out := &in.TransportTLS, &out.TransportTLS *out = new(TransportTLSConfig) diff --git a/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml b/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml index 3d4ff7c3f..ee870e55f 100644 --- a/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml +++ b/src/libraries/go/lib/testdata/icms-translate/function/container/llm/exp.yaml @@ -447,6 +447,8 @@ spec: - mountPath: /var/run/nvcf/info name: instance-data restartPolicy: Always + securityContext: + fsGroup: 1000 serviceAccountName: my-sa terminationGracePeriodSeconds: 120 tolerations: diff --git a/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml b/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml index 5c8104814..42b13e219 100644 --- a/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml +++ b/src/libraries/go/lib/testdata/icms-translate/function/helmchart/llm/exp.yaml @@ -419,6 +419,8 @@ spec: - mountPath: /var/run/nvcf/info name: instance-data restartPolicy: Always + securityContext: + fsGroup: 1000 serviceAccountName: my-sa terminationGracePeriodSeconds: 120 tolerations: