feat(supervisor): gate the runner seccomp profile by runtime scope

This commit is contained in:
nicktrn
2026-08-18 13:57:18 +01:00
parent 84c4a91553
commit 1b71ae70b9
6 changed files with 88 additions and 25 deletions
+8 -1
View File
@@ -210,7 +210,14 @@ export const Env = z
KUBERNETES_MEMORY_OVERHEAD_GB: z.coerce.number().min(0).optional(), // Optional memory overhead to add to the limit in GB
KUBERNETES_SCHEDULER_NAME: z.string().optional(), // Custom scheduler name for pods
KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z.string().default("profiles/block-io-uring.json"),
KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH: z
.string()
.trim()
.min(1)
.default("profiles/block-io-uring.json"),
KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES: z
.enum(["none", "node-24-plus", "all"])
.default("node-24-plus"),
// 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
+1
View File
@@ -653,6 +653,7 @@ class ManagedSupervisor {
projectId: message.project.id,
deploymentFriendlyId: message.deployment.friendlyId,
deploymentVersion: message.backgroundWorker.version,
runtime: message.backgroundWorker.runtime,
deploymentToken,
runId: message.run.id,
runFriendlyId: message.run.friendlyId,
@@ -100,24 +100,56 @@ describe("withNodeSelector", () => {
});
describe("withRunnerSeccompProfile", () => {
it("applies the profile for every runtime, preserving pod security defaults", () => {
const podSpec = withRunnerSeccompProfile(basePodSpec, "profiles/example.json");
const base = {
profilePath: "profiles/example.json",
runtimes: "node-24-plus" as const,
runtime: "node-24",
checkpointsEnabled: true,
};
expect(podSpec).toMatchObject({
...basePodSpec,
securityContext: {
...basePodSpec.securityContext,
seccompProfile: {
type: "Localhost",
localhostProfile: "profiles/example.json",
},
},
});
const withProfile = {
...basePodSpec,
securityContext: {
...basePodSpec.securityContext,
seccompProfile: { type: "Localhost", localhostProfile: "profiles/example.json" },
},
};
it("applies the profile to node-24 and above under the default scope", () => {
for (const runtime of ["node-24", "node-26", "node-30", "experimental-node-24"]) {
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toMatchObject(
withProfile
);
}
});
it("leaves the pod spec untouched when no profile is configured", () => {
for (const profilePath of [undefined, ""]) {
expect(withRunnerSeccompProfile(basePodSpec, profilePath)).toBe(basePodSpec);
it("skips older runtimes under the default scope", () => {
for (const runtime of ["node", "node-22", "bun", undefined, null, ""]) {
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtime })).toBe(basePodSpec);
}
});
it("applies the profile to every runtime under the all scope", () => {
for (const runtime of ["node", "node-22", "bun", "node-24", undefined]) {
expect(
withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "all", runtime })
).toMatchObject(withProfile);
}
});
it("applies nothing under the none scope, whatever the runtime", () => {
for (const runtime of ["node-24", "bun", "node-22"]) {
expect(withRunnerSeccompProfile(basePodSpec, { ...base, runtimes: "none", runtime })).toBe(
basePodSpec
);
}
});
it("applies nothing when checkpoints are disabled", () => {
for (const runtimes of ["none", "node-24-plus", "all"] as const) {
expect(
withRunnerSeccompProfile(basePodSpec, { ...base, runtimes, checkpointsEnabled: false })
).toBe(basePodSpec);
}
});
});
@@ -135,9 +135,12 @@ export class KubernetesWorkloadManager implements WorkloadManager {
);
}
}
const podSpec = this.opts.checkpointsEnabled
? withRunnerSeccompProfile(basePodSpec, env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH)
: basePodSpec;
const podSpec = withRunnerSeccompProfile(basePodSpec, {
profilePath: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_PATH,
runtimes: env.KUBERNETES_RUNNER_SECCOMP_PROFILE_RUNTIMES,
runtime: opts.runtime,
checkpointsEnabled: this.opts.checkpointsEnabled,
});
await this.k8s.core.createNamespacedPod({
namespace: this.namespace,
@@ -54,26 +54,44 @@ export function withNodeSelector(
};
}
export type RunnerSeccompProfileOptions = {
profilePath: string;
runtimes: "none" | "node-24-plus" | "all";
runtime: string | null | undefined;
checkpointsEnabled: boolean | undefined;
};
/**
* Applies the runner seccomp profile. The profile is a node-local file installed
* outside this repo, so an empty path leaves the pod on the runtime default -
* pointing at a profile the nodes don't have fails pod creation.
* Applies the runner seccomp profile, which is a node-local file installed outside
* this repo - pointing a pod at a profile its node doesn't have fails pod creation,
* so every condition for skipping it lives here.
*
* "node-24-plus" matches the original rollout: node >= 24 always creates io_uring
* fds, which can't be checkpointed, and blocking io_uring_setup makes libuv fall
* back to epoll. Tolerates an "experimental-" prefix. "bun" matches only under "all".
*/
export function withRunnerSeccompProfile(
podSpec: Omit<k8s.V1PodSpec, "containers">,
profilePath: string | undefined
options: RunnerSeccompProfileOptions
): Omit<k8s.V1PodSpec, "containers"> {
if (!profilePath) {
if (!options.checkpointsEnabled || options.runtimes === "none") {
return podSpec;
}
if (options.runtimes === "node-24-plus") {
const match = options.runtime ? /^(?:experimental-)?node-(\d+)$/.exec(options.runtime) : null;
if (!match || Number(match[1]) < 24) {
return podSpec;
}
}
return {
...podSpec,
securityContext: {
...podSpec.securityContext,
seccompProfile: {
type: "Localhost",
localhostProfile: profilePath,
localhostProfile: options.profilePath,
},
},
};
@@ -42,6 +42,8 @@ export interface WorkloadManagerCreateOptions {
projectId: string;
deploymentFriendlyId: string;
deploymentVersion: string;
// Canonical runtime identifier (e.g. "node", "node-22", "node-24")
runtime?: string;
// When set, overrides the TRIGGER_DEPLOYMENT_ID value the runner forwards as its identity header.
deploymentToken?: string;
runId: string;