Skip to content

Commit e91fb74

Browse files
authored
feat(supervisor): configurable security context for run pods
Adds KUBERNETES_RUNNER_SECURITY_CONTEXT (off | baseline | restricted), selecting how constrained the run container is. baseline drops the capability bounding set and blocks privilege escalation. restricted additionally pins the container to a non-root uid, chosen by runtime so bun images get their own. Default is off, so this is inert on merge.
1 parent 444c221 commit e91fb74

4 files changed

Lines changed: 71 additions & 0 deletions

File tree

apps/supervisor/src/env.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,8 @@ export const Env = z
220220
KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z
221221
.enum(["none", "node-24-plus", "all"])
222222
.default("node-24-plus"),
223+
KUBERNETES_RUNNER_SECURITY_CONTEXT: z.enum(["off", "baseline", "restricted"]).default("off"),
224+
KUBERNETES_RUNNER_RUN_AS_USER: z.coerce.number().int().min(1).default(1000),
223225

224226
// Pod DNS config — override the cluster default ndots to `KUBERNETES_POD_DNS_NDOTS`.
225227
// Default k8s ndots is 5: any name with fewer than 5 dots (e.g. `api.example.com`, 2 dots) is first walked

apps/supervisor/src/workloadManager/kubernetes.test.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { describe, expect, it } from "vitest";
22
import {
33
nodetypeNodeSelector,
44
runPodTolerations,
5+
runnerSecurityContext,
56
withRunnerSeccompProfile,
67
withNodeSelector,
78
} from "./kubernetesPodSpec.js";
@@ -153,3 +154,35 @@ describe("withRunnerSeccompProfile", () => {
153154
}
154155
});
155156
});
157+
158+
describe("runnerSecurityContext", () => {
159+
it("sets nothing when off", () => {
160+
expect(runnerSecurityContext("off", 1000, "node-24")).toBeUndefined();
161+
});
162+
163+
it("drops all capabilities and blocks escalation at baseline", () => {
164+
expect(runnerSecurityContext("baseline", 1000, "node-24")).toEqual({
165+
allowPrivilegeEscalation: false,
166+
capabilities: { drop: ["ALL"] },
167+
});
168+
});
169+
170+
it("pins the configured uid when restricted", () => {
171+
expect(runnerSecurityContext("restricted", 1000, "node-24")).toEqual({
172+
allowPrivilegeEscalation: false,
173+
capabilities: { drop: ["ALL"] },
174+
runAsNonRoot: true,
175+
runAsUser: 1000,
176+
});
177+
});
178+
179+
it("pins bun's own uid, which differs from node's", () => {
180+
expect(runnerSecurityContext("restricted", 1000, "bun")?.runAsUser).toBe(1001);
181+
});
182+
183+
it("falls back to the configured uid when the runtime is unknown", () => {
184+
for (const runtime of [undefined, null, "", "node", "node-22", "node-26"]) {
185+
expect(runnerSecurityContext("restricted", 1000, runtime)?.runAsUser).toBe(1000);
186+
}
187+
});
188+
});

apps/supervisor/src/workloadManager/kubernetes.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { getRunnerId } from "../util.js";
1717
import {
1818
nodetypeNodeSelector,
1919
runPodTolerations,
20+
runnerSecurityContext,
2021
withRunnerSeccompProfile,
2122
withNodeSelector,
2223
} from "./kubernetesPodSpec.js";
@@ -175,6 +176,11 @@ export class KubernetesWorkloadManager implements WorkloadManager {
175176
},
176177
],
177178
resources: this.#getResourcesForMachine(opts.machine),
179+
securityContext: runnerSecurityContext(
180+
env.KUBERNETES_RUNNER_SECURITY_CONTEXT,
181+
env.KUBERNETES_RUNNER_RUN_AS_USER,
182+
opts.runtime
183+
),
178184
env: [
179185
{
180186
name: "TRIGGER_DEQUEUED_AT_MS",

apps/supervisor/src/workloadManager/kubernetesPodSpec.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,3 +96,33 @@ export function withRunnerSeccompProfile(
9696
},
9797
};
9898
}
99+
100+
const BUN_RUN_AS_USER = 1001;
101+
102+
/**
103+
* runnerSecurityContext maps a configured level onto the run container's security
104+
* context. "baseline" drops the capability bounding set and blocks setuid
105+
* escalation; "restricted" additionally pins the container to a non-root uid.
106+
*
107+
* The uid is set explicitly rather than read from the image: the kubelet cannot
108+
* verify `runAsNonRoot` against an image that declares a named user, and fails
109+
* the container instead. Bun images carry their user at a different uid to
110+
* node's, so the runtime selects which uid is pinned.
111+
*/
112+
export function runnerSecurityContext(
113+
level: "off" | "baseline" | "restricted",
114+
runAsUser: number,
115+
runtime: string | null | undefined
116+
): k8s.V1SecurityContext | undefined {
117+
if (level === "off") {
118+
return undefined;
119+
}
120+
121+
return {
122+
allowPrivilegeEscalation: false,
123+
capabilities: { drop: ["ALL"] },
124+
...(level === "restricted"
125+
? { runAsNonRoot: true, runAsUser: runtime === "bun" ? BUN_RUN_AS_USER : runAsUser }
126+
: {}),
127+
};
128+
}

0 commit comments

Comments
 (0)