diff --git a/apps/supervisor/src/env.ts b/apps/supervisor/src/env.ts index 3f2cc8b46e9..e066ae31348 100644 --- a/apps/supervisor/src/env.ts +++ b/apps/supervisor/src/env.ts @@ -220,6 +220,8 @@ export const Env = z KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z .enum(["none", "node-24-plus", "all"]) .default("node-24-plus"), + KUBERNETES_RUNNER_SECURITY_CONTEXT: z.enum(["off", "baseline", "restricted"]).default("off"), + KUBERNETES_RUNNER_RUN_AS_USER: z.coerce.number().int().min(1).default(1000), // Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`. // Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked diff --git a/apps/supervisor/src/workloadManager/kubernetes.test.ts b/apps/supervisor/src/workloadManager/kubernetes.test.ts index e3023bc5d84..e99e292aca1 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.test.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest"; import { nodetypeNodeSelector, runPodTolerations, + runnerSecurityContext, withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -153,3 +154,35 @@ describe("withRunnerSeccompProfile", () => { } }); }); + +describe("runnerSecurityContext", () => { + it("sets nothing when off", () => { + expect(runnerSecurityContext("off", 1000, "node-24")).toBeUndefined(); + }); + + it("drops all capabilities and blocks escalation at baseline", () => { + expect(runnerSecurityContext("baseline", 1000, "node-24")).toEqual({ + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + }); + }); + + it("pins the configured uid when restricted", () => { + expect(runnerSecurityContext("restricted", 1000, "node-24")).toEqual({ + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + runAsNonRoot: true, + runAsUser: 1000, + }); + }); + + it("pins bun's own uid, which differs from node's", () => { + expect(runnerSecurityContext("restricted", 1000, "bun")?.runAsUser).toBe(1001); + }); + + it("falls back to the configured uid when the runtime is unknown", () => { + for (const runtime of [undefined, null, "", "node", "node-22", "node-26"]) { + expect(runnerSecurityContext("restricted", 1000, runtime)?.runAsUser).toBe(1000); + } + }); +}); diff --git a/apps/supervisor/src/workloadManager/kubernetes.ts b/apps/supervisor/src/workloadManager/kubernetes.ts index 54a9428efeb..b7a2f752196 100644 --- a/apps/supervisor/src/workloadManager/kubernetes.ts +++ b/apps/supervisor/src/workloadManager/kubernetes.ts @@ -17,6 +17,7 @@ import { getRunnerId } from "../util.js"; import { nodetypeNodeSelector, runPodTolerations, + runnerSecurityContext, withRunnerSeccompProfile, withNodeSelector, } from "./kubernetesPodSpec.js"; @@ -175,6 +176,11 @@ export class KubernetesWorkloadManager implements WorkloadManager { }, ], resources: this.#getResourcesForMachine(opts.machine), + securityContext: runnerSecurityContext( + env.KUBERNETES_RUNNER_SECURITY_CONTEXT, + env.KUBERNETES_RUNNER_RUN_AS_USER, + opts.runtime + ), env: [ { name: "TRIGGER_DEQUEUED_AT_MS", diff --git a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts index 6c6ddfc09b0..ddd336d2f2e 100644 --- a/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts +++ b/apps/supervisor/src/workloadManager/kubernetesPodSpec.ts @@ -96,3 +96,33 @@ export function withRunnerSeccompProfile( }, }; } + +const BUN_RUN_AS_USER = 1001; + +/** + * runnerSecurityContext maps a configured level onto the run container's security + * context. "baseline" drops the capability bounding set and blocks setuid + * escalation; "restricted" additionally pins the container to a non-root uid. + * + * The uid is set explicitly rather than read from the image: the kubelet cannot + * verify `runAsNonRoot` against an image that declares a named user, and fails + * the container instead. Bun images carry their user at a different uid to + * node's, so the runtime selects which uid is pinned. + */ +export function runnerSecurityContext( + level: "off" | "baseline" | "restricted", + runAsUser: number, + runtime: string | null | undefined +): k8s.V1SecurityContext | undefined { + if (level === "off") { + return undefined; + } + + return { + allowPrivilegeEscalation: false, + capabilities: { drop: ["ALL"] }, + ...(level === "restricted" + ? { runAsNonRoot: true, runAsUser: runtime === "bun" ? BUN_RUN_AS_USER : runAsUser } + : {}), + }; +}