Run code-executor unprivileged with seccomp on k8s >= 1.33#311
Run code-executor unprivileged with seccomp on k8s >= 1.33#311mertbozfakioglu wants to merge 5 commits into
Conversation
On Kubernetes 1.33+ (where the ProcMountType and UserNamespacesSupport feature gates are on by default), the code-executor now runs unprivileged using a localhost seccomp profile, NET_ADMIN, an unmasked /proc, and user namespaces - mirroring how the JS executor sandboxes itself. The nsjail seccomp profile is installed onto the node by an install-seccomp init container. On older clusters it falls back to the existing privileged mode, so the chart still installs without requiring 1.33+. Setting codeExecutor.securityContext explicitly continues to override this behavior for either mode. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
|
| Filename | Overview |
|---|---|
| charts/retool/templates/deployment_code_executor.yaml | Adds version-gated seccomp scaffolding (hostUsers, init container, volumes, security context); the init container write to /var/lib/kubelet/seccomp depends on idmap-mount kernel support when hostUsers: false is active, and allowPrivilegeEscalation is not explicitly disabled in the seccomp branch. |
| charts/retool/templates/configmap_code_executor.yaml | New ConfigMap rendering the nsjail-seccomp.json profile; condition mirrors the deployment's $useSecComp logic exactly and follows the existing jsExecutor ConfigMap pattern. |
| charts/retool/values.yaml | Comments out the default privileged securityContext and adds seccompLocalhostProfile with a sensible default; updated comments clearly document the auto-detection behavior and escape hatches. |
| charts/retool/Chart.yaml | Bumps chart version from 6.11.0 to 6.12.0, appropriate for a behavior-changing release. |
Sequence Diagram
sequenceDiagram
participant Helm as Helm Template Engine
participant CM as ConfigMap (nsjail-seccomp)
participant Init as install-seccomp (init)
participant Node as Node /var/lib/kubelet/seccomp
participant CE as code-executor container
Helm->>CM: "Render ConfigMap with nsjail-seccomp.json (k8s >=1.33 only)"
Helm->>Init: Render install-seccomp init container
Helm->>CE: Render securityContext (NET_ADMIN + procMount:Unmasked + Localhost seccomp)
Note over Helm,CE: At runtime (k8s >= 1.33, hostUsers:false)
Init->>CM: Mount seccomp-profile volume
Init->>Node: Copy nsjail-seccomp.json via host-seccomp hostPath
Note over Init,Node: Requires idmap mounts (kernel >= 5.12)
Init-->>CE: Init completes, main container starts
CE->>CE: nsjail reads seccomp profile from /proc (procMount:Unmasked)
CE-->>Node: Seccomp profile applied from Localhost path
Reviews (1): Last reviewed commit: "Drop codeExecutor securityContext commen..." | Re-trigger Greptile
| {{- if $useSecComp }} | ||
| hostUsers: false | ||
| {{- end }} | ||
| {{- if or $useSecComp .Values.initContainers }} | ||
| initContainers: | ||
| {{- if $useSecComp }} | ||
| - name: install-seccomp | ||
| image: busybox:1.37.0@sha256:b3255e7dfbcd10cb367af0d409747d511aeb66dfac98cf30e97e87e4207dd76f |
There was a problem hiding this comment.
hostUsers: false creates a write-permission dependency on idmap mounts
The pod-level hostUsers: false enables user namespaces for the entire pod, including the install-seccomp init container. With user namespaces active, UID 0 inside the container maps to a non-privileged UID on the host (e.g. 65536), so the /var/lib/kubelet/seccomp directory — typically owned by host root — is only writable if the kubelet is using idmap mounts (Linux kernel ≥ 5.12 for partial support, ≥ 6.3 for full hostPath support). The jsExecutor's identical init container works without issue because it does not set hostUsers: false.
If the node kernel is older (even on a k8s 1.33 cluster), the init container will fail with a permission-denied error and the pod will never reach the running state. The two live-cluster validation items in the test plan remain unchecked — this is the exact failure mode they would surface.
Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!
| securityContext: | ||
| {{ if .Values.codeExecutor.securityContext }} | ||
| {{- if .Values.codeExecutor.securityContext }} | ||
| {{ toYaml .Values.codeExecutor.securityContext | indent 10 }} | ||
| {{ else }} | ||
| {{- else if $useSecComp }} | ||
| capabilities: | ||
| add: ["NET_ADMIN"] | ||
| procMount: Unmasked | ||
| seccompProfile: | ||
| type: Localhost | ||
| localhostProfile: {{ .Values.codeExecutor.seccompLocalhostProfile }} | ||
| {{- else }} | ||
| privileged: true | ||
| {{ end }} | ||
| {{- end }} |
There was a problem hiding this comment.
allowPrivilegeEscalation not explicitly disabled in the seccomp branch
The seccomp security context adds NET_ADMIN but does not set allowPrivilegeEscalation: false. Without that field the kubelet leaves the no_new_privs bit unset, which means a setuid binary inside the container could still gain elevated privileges despite the seccomp filter. The jsExecutor security context has the same gap, but NET_ADMIN is a more powerful capability than what jsExecutor holds, making the omission slightly more impactful here. Consider adding allowPrivilegeEscalation: false to close this vector under the seccomp path.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Summary
Makes the code-executor (
ce) run unprivileged on Kubernetes >= 1.33, mirroring how the JS executor (jsExecutor) already sandboxes itself, while keeping the existing privileged behavior on older clusters. Addresses PLAT-1012.When rendered against k8s >= 1.33, the code-executor now uses:
nsjail-seccomp.json, the same onejsExecutoruses)capabilities.add: [NET_ADMIN]procMount: UnmaskedhostUsers: false(user namespaces, required forprocMount: Unmasked)install-seccompinit container that installs the profile onto the node (same pattern asdeployment_js_executor.yaml)On k8s < 1.33 it falls back to the previous
privileged: true, so the chart still installs without requiring 1.33+ (theProcMountTypeandUserNamespacesSupportfeature gates are only on-by-default at 1.33).Setting
codeExecutor.securityContextexplicitly continues to override the auto behavior in either direction.Why 1.33
procMount: Unmaskeddepends on theProcMountTypefeature gate, which (along withUserNamespacesSupportthat it relies on) is only enabled by default starting in Kubernetes 1.33.Behavior change
codeExecutor.securityContextpreviously defaulted toprivileged: true; it is now commented out so version auto-detection is the default. On upgrade, clusters on 1.33+ flip from privileged to the seccomp sandbox automatically; clusters < 1.33 are unchanged. Users can pincodeExecutor.securityContextto force a mode.Changes
charts/retool/templates/deployment_code_executor.yaml- version-gated securityContext,install-seccompinit container,hostUsers: false, seccomp checksum annotation, seccomp volumescharts/retool/templates/configmap_code_executor.yaml(new) - embeds the seccomp profile into a ConfigMap (rendered only on the seccomp path)charts/retool/values.yaml- comment out the defaultsecurityContext, addcodeExecutor.seccompLocalhostProfilecharts/retool/Chart.yaml- bump6.11.0->6.12.0Test plan
helm template --kube-version 1.34.3renders seccomp config + init container + ConfigMap +hostUsers: falsehelm template --kube-version 1.32.3rendersprivileged: trueand none of the seccomp scaffoldinghelm template --kube-version 1.34.3 --set codeExecutor.securityContext.privileged=falsehonors the explicit override and skips scaffoldinghelm lintpassesMade with Cursor