@namzu/sandbox 13.0.0 → 15.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1147 -0
- package/README.md +447 -0
- package/dist/backends/aci-standby-pool/index.d.ts.map +1 -1
- package/dist/backends/aci-standby-pool/index.js +13 -1
- package/dist/backends/aci-standby-pool/index.js.map +1 -1
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +19 -1
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/firecracker/index.d.ts.map +1 -1
- package/dist/backends/firecracker/index.js +12 -2
- package/dist/backends/firecracker/index.js.map +1 -1
- package/dist/backends/firecracker/protocol.d.ts +481 -8
- package/dist/backends/firecracker/protocol.d.ts.map +1 -1
- package/dist/backends/firecracker/protocol.js +136 -0
- package/dist/backends/firecracker/protocol.js.map +1 -1
- package/dist/backends/firecracker/transport.d.ts +642 -14
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +1307 -34
- package/dist/backends/firecracker/transport.js.map +1 -1
- package/dist/backends/kubernetes/egress-policy.d.ts +1296 -0
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -0
- package/dist/backends/kubernetes/egress-policy.js +2458 -0
- package/dist/backends/kubernetes/egress-policy.js.map +1 -0
- package/dist/backends/kubernetes/identity.d.ts +193 -0
- package/dist/backends/kubernetes/identity.d.ts.map +1 -0
- package/dist/backends/kubernetes/identity.js +147 -0
- package/dist/backends/kubernetes/identity.js.map +1 -0
- package/dist/backends/kubernetes/index.d.ts +1019 -0
- package/dist/backends/kubernetes/index.d.ts.map +1 -0
- package/dist/backends/kubernetes/index.js +1756 -0
- package/dist/backends/kubernetes/index.js.map +1 -0
- package/dist/backends/kubernetes/ingress-policy.d.ts +375 -0
- package/dist/backends/kubernetes/ingress-policy.d.ts.map +1 -0
- package/dist/backends/kubernetes/ingress-policy.js +1050 -0
- package/dist/backends/kubernetes/ingress-policy.js.map +1 -0
- package/dist/backends/kubernetes/k8s-client.d.ts +334 -0
- package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -0
- package/dist/backends/kubernetes/k8s-client.js +553 -0
- package/dist/backends/kubernetes/k8s-client.js.map +1 -0
- package/dist/backends/kubernetes/lease.d.ts +145 -0
- package/dist/backends/kubernetes/lease.d.ts.map +1 -0
- package/dist/backends/kubernetes/lease.js +201 -0
- package/dist/backends/kubernetes/lease.js.map +1 -0
- package/dist/backends/kubernetes/objects.d.ts +702 -0
- package/dist/backends/kubernetes/objects.d.ts.map +1 -0
- package/dist/backends/kubernetes/objects.js +518 -0
- package/dist/backends/kubernetes/objects.js.map +1 -0
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts +219 -0
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -0
- package/dist/backends/kubernetes/per-sandbox-policy.js +407 -0
- package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -0
- package/dist/backends/kubernetes/privilege-probe.d.ts +136 -0
- package/dist/backends/kubernetes/privilege-probe.d.ts.map +1 -0
- package/dist/backends/kubernetes/privilege-probe.js +185 -0
- package/dist/backends/kubernetes/privilege-probe.js.map +1 -0
- package/dist/backends/kubernetes/rbac.d.ts +153 -0
- package/dist/backends/kubernetes/rbac.d.ts.map +1 -0
- package/dist/backends/kubernetes/rbac.js +177 -0
- package/dist/backends/kubernetes/rbac.js.map +1 -0
- package/dist/backends/kubernetes/sandbox.d.ts +190 -0
- package/dist/backends/kubernetes/sandbox.d.ts.map +1 -0
- package/dist/backends/kubernetes/sandbox.js +433 -0
- package/dist/backends/kubernetes/sandbox.js.map +1 -0
- package/dist/backends/kubernetes/transport.d.ts +1048 -0
- package/dist/backends/kubernetes/transport.d.ts.map +1 -0
- package/dist/backends/kubernetes/transport.js +2093 -0
- package/dist/backends/kubernetes/transport.js.map +1 -0
- package/dist/backends/kubernetes/workspace.d.ts +1512 -0
- package/dist/backends/kubernetes/workspace.d.ts.map +1 -0
- package/dist/backends/kubernetes/workspace.js +3703 -0
- package/dist/backends/kubernetes/workspace.js.map +1 -0
- package/dist/backends/remote-execution-controller.d.ts +14 -0
- package/dist/backends/remote-execution-controller.d.ts.map +1 -1
- package/dist/backends/remote-execution-controller.js.map +1 -1
- package/dist/index.d.ts +350 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +344 -34
- package/dist/index.js.map +1 -1
- package/dist/testing/sandbox-conformance.d.ts +227 -0
- package/dist/testing/sandbox-conformance.d.ts.map +1 -0
- package/dist/testing/sandbox-conformance.js +896 -0
- package/dist/testing/sandbox-conformance.js.map +1 -0
- package/package.json +5 -4
- package/src/backends/aci-standby-pool/index.ts +16 -1
- package/src/backends/docker/index.ts +22 -1
- package/src/backends/firecracker/index.ts +14 -2
- package/src/backends/firecracker/protocol.ts +541 -6
- package/src/backends/firecracker/transport.ts +1687 -64
- package/src/backends/kubernetes/egress-policy.ts +3448 -0
- package/src/backends/kubernetes/identity.ts +261 -0
- package/src/backends/kubernetes/index.ts +2670 -0
- package/src/backends/kubernetes/ingress-policy.ts +1344 -0
- package/src/backends/kubernetes/k8s-client.ts +742 -0
- package/src/backends/kubernetes/lease.ts +254 -0
- package/src/backends/kubernetes/objects.ts +983 -0
- package/src/backends/kubernetes/per-sandbox-policy.ts +542 -0
- package/src/backends/kubernetes/privilege-probe.ts +261 -0
- package/src/backends/kubernetes/rbac.ts +192 -0
- package/src/backends/kubernetes/sandbox.ts +593 -0
- package/src/backends/kubernetes/transport.ts +2895 -0
- package/src/backends/kubernetes/workspace.ts +5640 -0
- package/src/backends/remote-execution-controller.ts +14 -0
- package/src/index.ts +838 -35
- package/src/testing/sandbox-conformance.ts +1202 -0
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acquire-time privilege probe: prove the guest process really was
|
|
3
|
+
* deprivileged, rather than assume the entrypoint did its job.
|
|
4
|
+
*
|
|
5
|
+
* The image's entrypoint mounts the workspace as root and then
|
|
6
|
+
* `exec setpriv --reuid --regid --clear-groups --inh-caps=-all
|
|
7
|
+
* --bounding-set=-all --no-new-privs -- tini -- node agent.cjs` (`tini` is
|
|
8
|
+
* the container's pid 1 so it can reap an orphan `agent.cjs` itself never
|
|
9
|
+
* spawned — see `k8s/entrypoint.sh` and the Dockerfile). Nothing in
|
|
10
|
+
* `agent.cjs` knows about any of that, and nothing on the host can see it
|
|
11
|
+
* either — an image built from an older entrypoint, a `RuntimeClass` change,
|
|
12
|
+
* a hand-edited `SandboxTemplate` all produce a sandbox that works perfectly
|
|
13
|
+
* and is not deprivileged. So the backend asks the guest, once, before it
|
|
14
|
+
* hands a caller a handle.
|
|
15
|
+
*
|
|
16
|
+
* ## Why `execute`, and why not `read-file`
|
|
17
|
+
*
|
|
18
|
+
* The guest's `read-file` resolves every path against `READ_ROOTS`
|
|
19
|
+
* (`WORKSPACE_ROOT` only), so it cannot reach `/proc` at all, and widening
|
|
20
|
+
* `READ_ROOTS` to make the probe work would hand every caller of `readFile`
|
|
21
|
+
* a window into the guest's process tree for the sake of one diagnostic.
|
|
22
|
+
* `handleExecute` jails only `cwd`, so a command whose ARGUMENT is an
|
|
23
|
+
* absolute path outside the workspace runs fine. The probe therefore spends
|
|
24
|
+
* one `exec` and touches no jail.
|
|
25
|
+
*
|
|
26
|
+
* ## Why all four masks
|
|
27
|
+
*
|
|
28
|
+
* Checking `CapEff` alone is a true-looking answer: an ordinary unprivileged
|
|
29
|
+
* process shows `CapEff: 0000000000000000` whether or not its bounding set
|
|
30
|
+
* was ever dropped, so a container running as uid 0 with the full bounding
|
|
31
|
+
* set still passes. `CapBnd` is the one that says a capability can never be
|
|
32
|
+
* regained; `CapInh` and `CapPrm` close the two ways one could be carried
|
|
33
|
+
* across an exec. `NoNewPrivs: 1` is what makes a setuid binary inside the
|
|
34
|
+
* guest unable to raise any of it back.
|
|
35
|
+
*
|
|
36
|
+
* ## Why every failure is a refusal
|
|
37
|
+
*
|
|
38
|
+
* A probe that could not run, one that never answered at all, output that
|
|
39
|
+
* could not be parsed and a process that is genuinely privileged are all
|
|
40
|
+
* reasons NOT to hand back a handle, and they are separated only in the error
|
|
41
|
+
* TEXT — a distroless image with no `cat` on `PATH` must be diagnosable as
|
|
42
|
+
* exactly that rather than read as a hardening failure. There is no
|
|
43
|
+
* configuration that turns this off.
|
|
44
|
+
*
|
|
45
|
+
* ## The clock is the caller's, and it lives in `index.ts`
|
|
46
|
+
*
|
|
47
|
+
* Nothing here has a timeout: the probe is one `exec`, and the budget it may
|
|
48
|
+
* spend belongs to the `create()` that ordered it. `admitProbedSandbox` runs
|
|
49
|
+
* it under an `OperationDeadline` and turns an expiry into
|
|
50
|
+
* {@link privilegeProbeTimedOut}, so a guest that accepts the connection and
|
|
51
|
+
* then goes quiet is refused on the caller's clock rather than on the
|
|
52
|
+
* execution controller's five-minute generic default.
|
|
53
|
+
*/
|
|
54
|
+
import type { SandboxExecResult } from '@namzu/sdk';
|
|
55
|
+
/**
|
|
56
|
+
* The probe command. `cat` rather than an absolute `/bin/cat` so a guest
|
|
57
|
+
* that keeps its coreutils somewhere else still answers, and rather than a
|
|
58
|
+
* shell so there is no quoting to get wrong. A guest without it fails with
|
|
59
|
+
* a spawn error the refusal repeats verbatim.
|
|
60
|
+
*/
|
|
61
|
+
export declare const PRIVILEGE_PROBE_COMMAND = "cat";
|
|
62
|
+
/** `/proc/self/status` — of the process the guest agent spawns, which
|
|
63
|
+
* inherits exactly the agent's own credentials and capability masks. */
|
|
64
|
+
export declare const PRIVILEGE_PROBE_ARGS: readonly string[];
|
|
65
|
+
/** Why a probe refused. The text says the same thing in words. */
|
|
66
|
+
export type PrivilegeProbeFailure =
|
|
67
|
+
/** The `exec` failed, exited non-zero, or never answered at all. */
|
|
68
|
+
'probe-failed'
|
|
69
|
+
/** It ran, but its output is not a readable `/proc/<pid>/status`. */
|
|
70
|
+
| 'unreadable-output'
|
|
71
|
+
/** It ran, it parsed, and the process has capabilities it should not. */
|
|
72
|
+
| 'privileged';
|
|
73
|
+
/**
|
|
74
|
+
* Raised by {@link runPrivilegeProbe} and {@link parseProcStatus}. The
|
|
75
|
+
* `reason` is the machine-readable form of the distinction the message
|
|
76
|
+
* draws in prose: `'privileged'` means the guest is under-hardened, and the
|
|
77
|
+
* other two mean the backend could not tell.
|
|
78
|
+
*/
|
|
79
|
+
export declare class KubernetesPrivilegeProbeError extends Error {
|
|
80
|
+
readonly reason: PrivilegeProbeFailure;
|
|
81
|
+
readonly name = "KubernetesPrivilegeProbeError";
|
|
82
|
+
constructor(reason: PrivilegeProbeFailure, message: string, options?: ErrorOptions);
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The five fields the probe reads, parsed. The four capability masks are
|
|
86
|
+
* `bigint` because a capability mask is 64 bits wide and `Number` stops
|
|
87
|
+
* being exact at 53 — `000001ffffffffff` is only 41 bits today, but a mask
|
|
88
|
+
* that silently rounds is precisely the bug this whole module exists to
|
|
89
|
+
* catch.
|
|
90
|
+
*/
|
|
91
|
+
export interface ProcStatusPrivileges {
|
|
92
|
+
readonly capInh: bigint;
|
|
93
|
+
readonly capPrm: bigint;
|
|
94
|
+
readonly capEff: bigint;
|
|
95
|
+
readonly capBnd: bigint;
|
|
96
|
+
/** `prctl(PR_GET_NO_NEW_PRIVS)`, 0 or 1 as the kernel prints it. */
|
|
97
|
+
readonly noNewPrivs: number;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Parse `/proc/<pid>/status` into the five fields that decide admission.
|
|
101
|
+
*
|
|
102
|
+
* Pure: no transport, no clock, no I/O. Everything it cannot read is a
|
|
103
|
+
* throw, never a default — a zero substituted for a missing mask is the one
|
|
104
|
+
* mistake that would make this function report hardening that is not there.
|
|
105
|
+
*/
|
|
106
|
+
export declare function parseProcStatus(text: string): ProcStatusPrivileges;
|
|
107
|
+
/**
|
|
108
|
+
* Admit only an all-zero capability set with `no_new_privs` set. Every
|
|
109
|
+
* non-zero mask is named in the refusal, because "one of them is set" sends
|
|
110
|
+
* the reader back to the guest to find out which.
|
|
111
|
+
*/
|
|
112
|
+
export declare function assertDeprivileged(privileges: ProcStatusPrivileges, sandboxName: string): void;
|
|
113
|
+
/**
|
|
114
|
+
* Run the probe over an already-built sandbox's `exec` and admit or refuse.
|
|
115
|
+
*
|
|
116
|
+
* `run` is the sandbox's own `exec`, not the raw transport, so the probe
|
|
117
|
+
* traverses exactly the path every later call will: reserve, admit, stream,
|
|
118
|
+
* confirm. A probe that cannot get through this is a sandbox a caller
|
|
119
|
+
* cannot use either.
|
|
120
|
+
*/
|
|
121
|
+
export declare function runPrivilegeProbe(run: (command: string, args: string[]) => Promise<SandboxExecResult>, sandboxName: string): Promise<ProcStatusPrivileges>;
|
|
122
|
+
/**
|
|
123
|
+
* The refusal for a probe that never answered.
|
|
124
|
+
*
|
|
125
|
+
* A guest that accepts the TCP connection and then goes quiet — an agent
|
|
126
|
+
* process out of memory, an event loop blocked by the workload, a container
|
|
127
|
+
* alive with a listener that has stopped reading — cannot be told apart from
|
|
128
|
+
* a healthy one by the wire alone, so the caller's acquire budget is the only
|
|
129
|
+
* thing that ends the wait. That expiry is a `'probe-failed'` like any other
|
|
130
|
+
* way the probe could not run, but it gets its own words: "the deadline
|
|
131
|
+
* expired" on its own says nothing about WHICH half of the acquire stopped
|
|
132
|
+
* answering, and a reader who sees `cat` blamed for a hang goes looking for a
|
|
133
|
+
* missing binary that is not missing.
|
|
134
|
+
*/
|
|
135
|
+
export declare function privilegeProbeTimedOut(sandboxName: string, timeoutMs: number, cause: unknown): KubernetesPrivilegeProbeError;
|
|
136
|
+
//# sourceMappingURL=privilege-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privilege-probe.d.ts","sourceRoot":"","sources":["../../../src/backends/kubernetes/privilege-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAoDG;AAEH,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAA;AAEnD;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,QAAQ,CAAA;AAE5C;wEACwE;AACxE,eAAO,MAAM,oBAAoB,EAAE,SAAS,MAAM,EAA0B,CAAA;AAE5E,kEAAkE;AAClE,MAAM,MAAM,qBAAqB;AAChC,oEAAoE;AAClE,cAAc;AAChB,qEAAqE;GACnE,mBAAmB;AACrB,yEAAyE;GACvE,YAAY,CAAA;AAEf;;;;;GAKG;AACH,qBAAa,6BAA8B,SAAQ,KAAK;IAItD,QAAQ,CAAC,MAAM,EAAE,qBAAqB;IAHvC,SAAkB,IAAI,mCAAkC;gBAG9C,MAAM,EAAE,qBAAqB,EACtC,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,YAAY;CAIvB;AAED;;;;;;GAMG;AACH,MAAM,WAAW,oBAAoB;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAA;IACvB,oEAAoE;IACpE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAA;CAC3B;AAuBD;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,oBAAoB,CA8BlE;AAED;;;;GAIG;AACH,wBAAgB,kBAAkB,CAAC,UAAU,EAAE,oBAAoB,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAe9F;AAED;;;;;;;GAOG;AACH,wBAAsB,iBAAiB,CACtC,GAAG,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,OAAO,CAAC,iBAAiB,CAAC,EACpE,WAAW,EAAE,MAAM,GACjB,OAAO,CAAC,oBAAoB,CAAC,CA6B/B;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,sBAAsB,CACrC,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,MAAM,EACjB,KAAK,EAAE,OAAO,GACZ,6BAA6B,CAQ/B"}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acquire-time privilege probe: prove the guest process really was
|
|
3
|
+
* deprivileged, rather than assume the entrypoint did its job.
|
|
4
|
+
*
|
|
5
|
+
* The image's entrypoint mounts the workspace as root and then
|
|
6
|
+
* `exec setpriv --reuid --regid --clear-groups --inh-caps=-all
|
|
7
|
+
* --bounding-set=-all --no-new-privs -- tini -- node agent.cjs` (`tini` is
|
|
8
|
+
* the container's pid 1 so it can reap an orphan `agent.cjs` itself never
|
|
9
|
+
* spawned — see `k8s/entrypoint.sh` and the Dockerfile). Nothing in
|
|
10
|
+
* `agent.cjs` knows about any of that, and nothing on the host can see it
|
|
11
|
+
* either — an image built from an older entrypoint, a `RuntimeClass` change,
|
|
12
|
+
* a hand-edited `SandboxTemplate` all produce a sandbox that works perfectly
|
|
13
|
+
* and is not deprivileged. So the backend asks the guest, once, before it
|
|
14
|
+
* hands a caller a handle.
|
|
15
|
+
*
|
|
16
|
+
* ## Why `execute`, and why not `read-file`
|
|
17
|
+
*
|
|
18
|
+
* The guest's `read-file` resolves every path against `READ_ROOTS`
|
|
19
|
+
* (`WORKSPACE_ROOT` only), so it cannot reach `/proc` at all, and widening
|
|
20
|
+
* `READ_ROOTS` to make the probe work would hand every caller of `readFile`
|
|
21
|
+
* a window into the guest's process tree for the sake of one diagnostic.
|
|
22
|
+
* `handleExecute` jails only `cwd`, so a command whose ARGUMENT is an
|
|
23
|
+
* absolute path outside the workspace runs fine. The probe therefore spends
|
|
24
|
+
* one `exec` and touches no jail.
|
|
25
|
+
*
|
|
26
|
+
* ## Why all four masks
|
|
27
|
+
*
|
|
28
|
+
* Checking `CapEff` alone is a true-looking answer: an ordinary unprivileged
|
|
29
|
+
* process shows `CapEff: 0000000000000000` whether or not its bounding set
|
|
30
|
+
* was ever dropped, so a container running as uid 0 with the full bounding
|
|
31
|
+
* set still passes. `CapBnd` is the one that says a capability can never be
|
|
32
|
+
* regained; `CapInh` and `CapPrm` close the two ways one could be carried
|
|
33
|
+
* across an exec. `NoNewPrivs: 1` is what makes a setuid binary inside the
|
|
34
|
+
* guest unable to raise any of it back.
|
|
35
|
+
*
|
|
36
|
+
* ## Why every failure is a refusal
|
|
37
|
+
*
|
|
38
|
+
* A probe that could not run, one that never answered at all, output that
|
|
39
|
+
* could not be parsed and a process that is genuinely privileged are all
|
|
40
|
+
* reasons NOT to hand back a handle, and they are separated only in the error
|
|
41
|
+
* TEXT — a distroless image with no `cat` on `PATH` must be diagnosable as
|
|
42
|
+
* exactly that rather than read as a hardening failure. There is no
|
|
43
|
+
* configuration that turns this off.
|
|
44
|
+
*
|
|
45
|
+
* ## The clock is the caller's, and it lives in `index.ts`
|
|
46
|
+
*
|
|
47
|
+
* Nothing here has a timeout: the probe is one `exec`, and the budget it may
|
|
48
|
+
* spend belongs to the `create()` that ordered it. `admitProbedSandbox` runs
|
|
49
|
+
* it under an `OperationDeadline` and turns an expiry into
|
|
50
|
+
* {@link privilegeProbeTimedOut}, so a guest that accepts the connection and
|
|
51
|
+
* then goes quiet is refused on the caller's clock rather than on the
|
|
52
|
+
* execution controller's five-minute generic default.
|
|
53
|
+
*/
|
|
54
|
+
/**
|
|
55
|
+
* The probe command. `cat` rather than an absolute `/bin/cat` so a guest
|
|
56
|
+
* that keeps its coreutils somewhere else still answers, and rather than a
|
|
57
|
+
* shell so there is no quoting to get wrong. A guest without it fails with
|
|
58
|
+
* a spawn error the refusal repeats verbatim.
|
|
59
|
+
*/
|
|
60
|
+
export const PRIVILEGE_PROBE_COMMAND = 'cat';
|
|
61
|
+
/** `/proc/self/status` — of the process the guest agent spawns, which
|
|
62
|
+
* inherits exactly the agent's own credentials and capability masks. */
|
|
63
|
+
export const PRIVILEGE_PROBE_ARGS = ['/proc/self/status'];
|
|
64
|
+
/**
|
|
65
|
+
* Raised by {@link runPrivilegeProbe} and {@link parseProcStatus}. The
|
|
66
|
+
* `reason` is the machine-readable form of the distinction the message
|
|
67
|
+
* draws in prose: `'privileged'` means the guest is under-hardened, and the
|
|
68
|
+
* other two mean the backend could not tell.
|
|
69
|
+
*/
|
|
70
|
+
export class KubernetesPrivilegeProbeError extends Error {
|
|
71
|
+
reason;
|
|
72
|
+
name = 'KubernetesPrivilegeProbeError';
|
|
73
|
+
constructor(reason, message, options) {
|
|
74
|
+
super(message, options);
|
|
75
|
+
this.reason = reason;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const CAPABILITY_FIELDS = ['CapInh', 'CapPrm', 'CapEff', 'CapBnd'];
|
|
79
|
+
const NO_NEW_PRIVS_FIELD = 'NoNewPrivs';
|
|
80
|
+
/** Clip a value before it goes into an error message. */
|
|
81
|
+
function clip(value) {
|
|
82
|
+
return value.length > 40 ? `${value.slice(0, 40)}…` : value;
|
|
83
|
+
}
|
|
84
|
+
function readField(text, field) {
|
|
85
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
86
|
+
const colon = rawLine.indexOf(':');
|
|
87
|
+
if (colon < 0)
|
|
88
|
+
continue;
|
|
89
|
+
if (rawLine.slice(0, colon).trim() !== field)
|
|
90
|
+
continue;
|
|
91
|
+
return rawLine.slice(colon + 1).trim();
|
|
92
|
+
}
|
|
93
|
+
throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but its output carries no ${field} line, so this sandbox's privileges could not be read. The probe is \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` — a guest whose /proc is not mounted, or whose kernel does not publish ${field}, cannot be admitted, because an unreadable answer is not a safe one.`);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Parse `/proc/<pid>/status` into the five fields that decide admission.
|
|
97
|
+
*
|
|
98
|
+
* Pure: no transport, no clock, no I/O. Everything it cannot read is a
|
|
99
|
+
* throw, never a default — a zero substituted for a missing mask is the one
|
|
100
|
+
* mistake that would make this function report hardening that is not there.
|
|
101
|
+
*/
|
|
102
|
+
export function parseProcStatus(text) {
|
|
103
|
+
const masks = CAPABILITY_FIELDS.map((field) => {
|
|
104
|
+
const raw = readField(text, field);
|
|
105
|
+
// The kernel prints a bare, fixed-width hex mask with no `0x`. A
|
|
106
|
+
// `0x` prefix, a sign, whitespace inside, or anything non-hex means
|
|
107
|
+
// this is not the file this parser thinks it is.
|
|
108
|
+
if (!/^[0-9a-fA-F]+$/.test(raw)) {
|
|
109
|
+
throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but ${field} is ${JSON.stringify(clip(raw))}, which is not the bare hexadecimal capability mask /proc/<pid>/status publishes, so this sandbox's privileges could not be read.`);
|
|
110
|
+
}
|
|
111
|
+
return BigInt(`0x${raw}`);
|
|
112
|
+
});
|
|
113
|
+
const noNewPrivsRaw = readField(text, NO_NEW_PRIVS_FIELD);
|
|
114
|
+
if (!/^\d+$/.test(noNewPrivsRaw)) {
|
|
115
|
+
throw new KubernetesPrivilegeProbeError('unreadable-output', `the privilege probe ran but ${NO_NEW_PRIVS_FIELD} is ${JSON.stringify(clip(noNewPrivsRaw))}, which is not the integer /proc/<pid>/status publishes, so this sandbox's privileges could not be read.`);
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
capInh: masks[0],
|
|
119
|
+
capPrm: masks[1],
|
|
120
|
+
capEff: masks[2],
|
|
121
|
+
capBnd: masks[3],
|
|
122
|
+
noNewPrivs: Number(noNewPrivsRaw),
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Admit only an all-zero capability set with `no_new_privs` set. Every
|
|
127
|
+
* non-zero mask is named in the refusal, because "one of them is set" sends
|
|
128
|
+
* the reader back to the guest to find out which.
|
|
129
|
+
*/
|
|
130
|
+
export function assertDeprivileged(privileges, sandboxName) {
|
|
131
|
+
const offenders = [];
|
|
132
|
+
if (privileges.capInh !== 0n)
|
|
133
|
+
offenders.push(`CapInh=${privileges.capInh.toString(16)}`);
|
|
134
|
+
if (privileges.capPrm !== 0n)
|
|
135
|
+
offenders.push(`CapPrm=${privileges.capPrm.toString(16)}`);
|
|
136
|
+
if (privileges.capEff !== 0n)
|
|
137
|
+
offenders.push(`CapEff=${privileges.capEff.toString(16)}`);
|
|
138
|
+
if (privileges.capBnd !== 0n)
|
|
139
|
+
offenders.push(`CapBnd=${privileges.capBnd.toString(16)}`);
|
|
140
|
+
if (privileges.noNewPrivs !== 1)
|
|
141
|
+
offenders.push(`NoNewPrivs=${privileges.noNewPrivs}`);
|
|
142
|
+
if (offenders.length === 0)
|
|
143
|
+
return;
|
|
144
|
+
throw new KubernetesPrivilegeProbeError('privileged', `kubernetes sandbox ${sandboxName} is PRIVILEGED and was refused: ${offenders.join(', ')} (every capability mask must be 0 and NoNewPrivs must be 1). The probe ran and was read successfully — this is the guest's real state, not a diagnostic failure. The image's entrypoint is expected to end with \`exec setpriv --reuid --regid --clear-groups --inh-caps=-all --bounding-set=-all --no-new-privs -- tini -- node agent.cjs\`; a sandbox that reaches this message is running with capabilities the workload could use.`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* Run the probe over an already-built sandbox's `exec` and admit or refuse.
|
|
148
|
+
*
|
|
149
|
+
* `run` is the sandbox's own `exec`, not the raw transport, so the probe
|
|
150
|
+
* traverses exactly the path every later call will: reserve, admit, stream,
|
|
151
|
+
* confirm. A probe that cannot get through this is a sandbox a caller
|
|
152
|
+
* cannot use either.
|
|
153
|
+
*/
|
|
154
|
+
export async function runPrivilegeProbe(run, sandboxName) {
|
|
155
|
+
let result;
|
|
156
|
+
try {
|
|
157
|
+
result = await run(PRIVILEGE_PROBE_COMMAND, [...PRIVILEGE_PROBE_ARGS]);
|
|
158
|
+
}
|
|
159
|
+
catch (error) {
|
|
160
|
+
throw new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: ${error instanceof Error ? error.message : String(error)}. The probe is \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\`; an image without it on PATH cannot be admitted, because a sandbox whose privileges cannot be checked is refused rather than trusted.`, { cause: error });
|
|
161
|
+
}
|
|
162
|
+
if (result.exitCode !== 0) {
|
|
163
|
+
throw new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` exited ${result.exitCode}${result.stderr.trim() ? ` (${clip(result.stderr.trim())})` : ''}. This is a diagnostic failure, not a privilege failure — the sandbox is refused because its state is unknown.`);
|
|
164
|
+
}
|
|
165
|
+
const privileges = parseProcStatus(result.stdout);
|
|
166
|
+
assertDeprivileged(privileges, sandboxName);
|
|
167
|
+
return privileges;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The refusal for a probe that never answered.
|
|
171
|
+
*
|
|
172
|
+
* A guest that accepts the TCP connection and then goes quiet — an agent
|
|
173
|
+
* process out of memory, an event loop blocked by the workload, a container
|
|
174
|
+
* alive with a listener that has stopped reading — cannot be told apart from
|
|
175
|
+
* a healthy one by the wire alone, so the caller's acquire budget is the only
|
|
176
|
+
* thing that ends the wait. That expiry is a `'probe-failed'` like any other
|
|
177
|
+
* way the probe could not run, but it gets its own words: "the deadline
|
|
178
|
+
* expired" on its own says nothing about WHICH half of the acquire stopped
|
|
179
|
+
* answering, and a reader who sees `cat` blamed for a hang goes looking for a
|
|
180
|
+
* missing binary that is not missing.
|
|
181
|
+
*/
|
|
182
|
+
export function privilegeProbeTimedOut(sandboxName, timeoutMs, cause) {
|
|
183
|
+
return new KubernetesPrivilegeProbeError('probe-failed', `the privilege probe could not run in kubernetes sandbox ${sandboxName}: the guest accepted the connection and did not answer \`${PRIVILEGE_PROBE_COMMAND} ${PRIVILEGE_PROBE_ARGS.join(' ')}\` within ${timeoutMs} ms, so the probe was abandoned. This is a diagnostic failure, not a privilege failure — the sandbox is refused because its state is unknown. A wedged agent (out of memory, an event loop blocked by the workload) looks exactly like this from the host; check the pod's logs before raising the acquire budget.`, { cause });
|
|
184
|
+
}
|
|
185
|
+
//# sourceMappingURL=privilege-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"privilege-probe.js","sourceRoot":"","sources":["../../../src/backends/kubernetes/privilege-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The verbs a CLAIM-ONLY host issues — the pool-only path, written down once
|
|
3
|
+
* so `k8s/manifests/rbac-claimant.yaml`, and the test that compares it to both
|
|
4
|
+
* this list and the call sites behind it, cannot drift from the code they
|
|
5
|
+
* describe.
|
|
6
|
+
*
|
|
7
|
+
* A host that sets `warmPoolName` and never creates a `Sandbox` itself needs
|
|
8
|
+
* a strictly smaller grant than `k8s/manifests/rbac.yaml`'s, and the smaller
|
|
9
|
+
* it is the more it matters that it is EXACT. Two failure directions, and
|
|
10
|
+
* this constant is the one place both are decided:
|
|
11
|
+
*
|
|
12
|
+
* - **Too few verbs** is a `403` on a flow nobody ran in review. The four
|
|
13
|
+
* `list` verbs are the load-bearing ones, and the two a host can go a long
|
|
14
|
+
* time without noticing are the sharp ones: `sandboxclaims: list` is the
|
|
15
|
+
* crash-recovery read (`releaseKubernetesTaskSandboxes`) and the
|
|
16
|
+
* pool-membership read (`readKubernetesTaskCapacity`), and `pods: list` is
|
|
17
|
+
* `readBoundPod`'s fallback read by the bound Sandbox's own selector and
|
|
18
|
+
* `readKubernetesTaskCapacity`'s count of Pending pods. The ACQUIRE path
|
|
19
|
+
* never notices either is missing — `create()` never lists either
|
|
20
|
+
* collection, so a claim is still created, bound, renewed and released
|
|
21
|
+
* exactly as before — which makes the failure DEFERRED rather than quiet:
|
|
22
|
+
* the `await client.request` inside each of those two functions rejects
|
|
23
|
+
* with a `KubernetesCredentialError` (`403`), and neither call catches one,
|
|
24
|
+
* so the first release or capacity read a host makes fails loudly, with the
|
|
25
|
+
* refused verb and the collection's path in the error. The other two are
|
|
26
|
+
* load-bearing SOONER: `list` is what both policy checks need, and the
|
|
27
|
+
* ingress one runs by default before every create.
|
|
28
|
+
* - **One verb too many** is the vulnerability class issue #500 is about: a
|
|
29
|
+
* `sandboxes: create` grant against a namespace running a privileged
|
|
30
|
+
* workspace template is an arbitrary privileged pod for anyone holding
|
|
31
|
+
* it. Every verb below has a call site named against it in the list.
|
|
32
|
+
*
|
|
33
|
+
* The path this describes is the CLAIM path, not the task tier as a whole:
|
|
34
|
+
* with `warmPoolName` unset the same backend POSTs a `Sandbox` instead of a
|
|
35
|
+
* claim, reads its `SandboxTemplate` first, and patches and deletes that
|
|
36
|
+
* object — none of which is granted here, deliberately. See the manifest's
|
|
37
|
+
* own header for what it declines to grant and why.
|
|
38
|
+
*
|
|
39
|
+
* Kept honest by a test rather than by review, and the test reaches the CODE
|
|
40
|
+
* rather than only this list: `k8s/__tests__/manifests.test.ts` reads the
|
|
41
|
+
* pool-only path's own files — `index.ts`, `objects.ts`, `ingress-policy.ts`,
|
|
42
|
+
* `egress-policy.ts` — as text, resolves every literal `client.request(
|
|
43
|
+
* '<METHOD>', <path>, …)` in them to the `(apiGroup, resource, verb)` triple
|
|
44
|
+
* the builder that path goes through addresses, and FAILS on anything it
|
|
45
|
+
* cannot resolve — an unknown builder, a path it cannot follow, a builder
|
|
46
|
+
* call with anything appended to it (`podPath(ns, name) + '/log'` is the
|
|
47
|
+
* `pods/log` subresource, which RBAC authorizes separately), a name the source
|
|
48
|
+
* writes a second path to — the guard reads four spellings, wherever each is
|
|
49
|
+
* written rather than only where it opens a statement: a plain assignment
|
|
50
|
+
* (`path = …`, the one-line branch included), a compound one (`path += …`), a
|
|
51
|
+
* `for (path of …)` binding, and a destructuring target (`({ path } = …)`) — as
|
|
52
|
+
* a `const`/`let`/`var`-declared name, whose initializer is then not what a
|
|
53
|
+
* request through it sends, or as a wrapper's own path parameter, whose call
|
|
54
|
+
* sites then do not say what it sends — a path
|
|
55
|
+
* that is a parameter of a body whose call sites the scan has not been told
|
|
56
|
+
* about, a wrapper declared as such in a file the scan does not read — so a new
|
|
57
|
+
* request has to declare itself instead of escaping. WHICH files those are
|
|
58
|
+
* is not a list that can quietly stop covering the path: every `.ts` under
|
|
59
|
+
* `src/backends/kubernetes/` that reaches the client — calling
|
|
60
|
+
* `client.request(…)` itself in either spelling the scan reads, the literal
|
|
61
|
+
* one or a computed `client['request']`, or naming `listPolicies`, the one
|
|
62
|
+
* wrapper the scan knows, which takes its path as an argument; every half read
|
|
63
|
+
* on text with its comments blanked — is either scanned or declared off the
|
|
64
|
+
* path with its reason, so a file that reaches the client fails that test
|
|
65
|
+
* until someone decides which of the two it is. The triples it
|
|
66
|
+
* observes must be exactly this list, plus the four the pool-LESS branch
|
|
67
|
+
* issues and this grant declines by design. A request added to the pool-only
|
|
68
|
+
* path therefore fails that test naming the resource, the file and the line,
|
|
69
|
+
* instead of surfacing as a `403` in a host's log — and so does a rule here
|
|
70
|
+
* with no call site, which is a grant wider than the code needs.
|
|
71
|
+
*
|
|
72
|
+
* What that test still cannot see, so none of the above reads stronger than
|
|
73
|
+
* it is: the files it declares off the path — `workspace.ts`, `k8s-client.ts`,
|
|
74
|
+
* `transport.ts` — are never resolved to triples, a wrapper call site in one
|
|
75
|
+
* of them included, since call sites are read from the scanned files only; and
|
|
76
|
+
* that a claimant host never reaches `workspace.ts` is a claim about the
|
|
77
|
+
* DEPLOYMENT (`warmPoolName` set, no `Sandbox` created by that host), which
|
|
78
|
+
* nothing in this repository measures. A wrapper reached by ANOTHER name is outside that file predicate,
|
|
79
|
+
* which matches the wrapper's name and not its identity: a file importing a
|
|
80
|
+
* re-export of it, or calling a helper one level further out that calls it, is
|
|
81
|
+
* neither scanned nor required to be declared. A request whose path is a
|
|
82
|
+
* parameter of an expression-bodied arrow is likewise outside what the scan
|
|
83
|
+
* matches, and a body the scan could not register — a class method, or an
|
|
84
|
+
* object literal's shorthand method, declared INSIDE one of the bodies it
|
|
85
|
+
* reads — is attributed to the body that encloses it, so its own parameters
|
|
86
|
+
* are invisible. The assignment guard is by name in the file it resolves in,
|
|
87
|
+
* not by scope, so a BINDING it registers nowhere shadows nothing there: a
|
|
88
|
+
* destructuring parameter (`function f({ path })`) keeps its positional slot
|
|
89
|
+
* without a name this scan can match, a `catch (path)` is not read at all, and
|
|
90
|
+
* a write made to an exported binding from ANOTHER module is not in that file's
|
|
91
|
+
* text — which is also to say that an assignment to a same-named local in an
|
|
92
|
+
* unrelated function of the same file refuses the request too, a false refusal
|
|
93
|
+
* rather than an escape. A parameter carrying a DEFAULT is written with an `=`
|
|
94
|
+
* (`function f(path = …)`, `function f({ path } = {})`), so the guard reads it
|
|
95
|
+
* and refuses the request rather than resolving it — a refusal, where the bare
|
|
96
|
+
* pattern would have resolved or failed; and a destructuring pattern nested
|
|
97
|
+
* inside another (`({ a: { path } } = …)`) is read by neither spelling, which
|
|
98
|
+
* is measured green.
|
|
99
|
+
*
|
|
100
|
+
* **That list is the SHORT form, not the list.** It is carried in full in the
|
|
101
|
+
* header of `k8s/__tests__/manifests.test.ts`, which also holds two bullets
|
|
102
|
+
* this paragraph does not: `a path assembled by an operation the scan does not
|
|
103
|
+
* model`, and `the cluster` (nothing there contacts an API server, so no live
|
|
104
|
+
* `Role` is measured).
|
|
105
|
+
*
|
|
106
|
+
* Exported from the package root because the grant is a property of the
|
|
107
|
+
* backend, not of a file this package does not publish: `k8s/` is outside
|
|
108
|
+
* the `files` array, so a consumer cannot read the YAML out of `node_modules`
|
|
109
|
+
* — it can, however, compare its own applied `Role` against this.
|
|
110
|
+
*/
|
|
111
|
+
/** An RBAC verb this backend issues. No wildcard, and none is ever added. */
|
|
112
|
+
export type KubernetesRbacVerb = 'create' | 'get' | 'list' | 'patch' | 'delete';
|
|
113
|
+
/**
|
|
114
|
+
* One `rules[]` entry, in the shape RBAC itself uses — one resource per
|
|
115
|
+
* entry, which is also how the shipped manifests are written, so a rule here
|
|
116
|
+
* and a rule there compare field for field.
|
|
117
|
+
*/
|
|
118
|
+
export interface KubernetesRbacRule {
|
|
119
|
+
/** The `apiGroups` entry, verbatim: `''` for the core group. */
|
|
120
|
+
readonly apiGroup: string;
|
|
121
|
+
readonly resource: string;
|
|
122
|
+
readonly verbs: readonly KubernetesRbacVerb[];
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Every verb the pool-only path issues, each pinned to its call site in
|
|
126
|
+
* `./index.ts` (and, for the policy checks, `./ingress-policy.ts` /
|
|
127
|
+
* `./egress-policy.ts`):
|
|
128
|
+
*
|
|
129
|
+
* - `sandboxclaims` — `create` is the acquire's POST (`createPath`), `get`
|
|
130
|
+
* the readiness poll that waits for the controller to bind one, `patch`
|
|
131
|
+
* the lease renewal that keeps a long run's claim alive, `delete` the
|
|
132
|
+
* release `destroy()` and a failed acquire's cleanup both send, and
|
|
133
|
+
* `list` the two host-invoked reads named above.
|
|
134
|
+
* - `sandboxes: get` — read for `status.selector` when the pod-by-name GET
|
|
135
|
+
* comes back gone, which on the claim path is the ONLY way to find the
|
|
136
|
+
* bound pod: a claim has no `podSelector` of its own to fall back on.
|
|
137
|
+
* - `sandboxwarmpools: get` — `readKubernetesTaskCapacity` reads the
|
|
138
|
+
* configured pool's own `status.readyReplicas`/`spec.replicas` before a
|
|
139
|
+
* host admits more work.
|
|
140
|
+
* - `pods` — `get` by name for the agent's bind token and, under
|
|
141
|
+
* `agentAddress: 'pod-ip'`, its address, plus the whole-namespace `list`
|
|
142
|
+
* that counts in-flight scale-up. The two policy checks use NO pod
|
|
143
|
+
* subresource; the agent's protocol is a socket the guest serves, not
|
|
144
|
+
* `pods/exec`.
|
|
145
|
+
* - `networkpolicies` / `ciliumnetworkpolicies` — `list` is the ingress
|
|
146
|
+
* coverage check (on by default before every create) and the egress UNION
|
|
147
|
+
* check (on by default whenever `config.egress` is set); `get` is the
|
|
148
|
+
* egress named-object check, also only under `config.egress`. Under
|
|
149
|
+
* `engine: 'core'` the Cilium rule simply never matches, and a cluster
|
|
150
|
+
* with no Cilium CRDs installed never has an object to match it.
|
|
151
|
+
*/
|
|
152
|
+
export declare const KUBERNETES_CLAIMANT_RBAC_RULES: readonly KubernetesRbacRule[];
|
|
153
|
+
//# sourceMappingURL=rbac.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rbac.d.ts","sourceRoot":"","sources":["../../../src/backends/kubernetes/rbac.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6GG;AAEH,6EAA6E;AAC7E,MAAM,MAAM,kBAAkB,GAAG,QAAQ,GAAG,KAAK,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,CAAA;AAE/E;;;;GAIG;AACH,MAAM,WAAW,kBAAkB;IAClC,gEAAgE;IAChE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAA;IACzB,QAAQ,CAAC,KAAK,EAAE,SAAS,kBAAkB,EAAE,CAAA;CAC7C;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,eAAO,MAAM,8BAA8B,EAAE,SAAS,kBAAkB,EAqCtE,CAAA"}
|