@namzu/sandbox 15.0.0 → 16.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 +86 -0
- package/README.md +59 -0
- package/dist/backends/docker/index.d.ts +169 -6
- package/dist/backends/docker/index.d.ts.map +1 -1
- package/dist/backends/docker/index.js +480 -84
- package/dist/backends/docker/index.js.map +1 -1
- package/dist/backends/kubernetes/egress-policy.d.ts +193 -102
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/egress-policy.js +321 -146
- package/dist/backends/kubernetes/egress-policy.js.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts +6 -6
- package/dist/backends/kubernetes/per-sandbox-policy.d.ts.map +1 -1
- package/dist/backends/kubernetes/per-sandbox-policy.js +21 -53
- package/dist/backends/kubernetes/per-sandbox-policy.js.map +1 -1
- package/dist/index.d.ts +63 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +33 -5
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/backends/docker/index.ts +595 -99
- package/src/backends/kubernetes/egress-policy.ts +455 -187
- package/src/backends/kubernetes/per-sandbox-policy.ts +21 -66
- package/src/index.ts +73 -5
|
@@ -87,6 +87,7 @@ import {
|
|
|
87
87
|
type KubernetesTranslatedEgressPolicy,
|
|
88
88
|
PER_SANDBOX_NARROWING_REFUSAL,
|
|
89
89
|
assertHostsFitNarrowing,
|
|
90
|
+
assertUsableHost,
|
|
90
91
|
buildCiliumEgressManifest,
|
|
91
92
|
perSandboxEgressLabelKey,
|
|
92
93
|
verifyEgressPolicyApplied,
|
|
@@ -214,18 +215,15 @@ export class KubernetesAdmissionFenceMissingError extends Error {
|
|
|
214
215
|
* Raised for an `allowedHosts` entry this backend will not translate —
|
|
215
216
|
* re-exported rather than defined here.
|
|
216
217
|
*
|
|
217
|
-
* It lives in `egress-policy.ts` beside {@link assertHostsFitNarrowing}
|
|
218
|
-
* because the config-level
|
|
219
|
-
* class for the same entry
|
|
220
|
-
* here would make the two modules import each other. The
|
|
221
|
-
* re-exported so every existing import path — including the
|
|
222
|
-
* keeps resolving.
|
|
218
|
+
* It lives in `egress-policy.ts` beside {@link assertHostsFitNarrowing} and
|
|
219
|
+
* {@link assertUsableHost}, because the config-level translation raises the
|
|
220
|
+
* same class for the same entry and this module imports both checks from
|
|
221
|
+
* there: defining it here would make the two modules import each other. The
|
|
222
|
+
* identifier is re-exported so every existing import path — including the
|
|
223
|
+
* package index — keeps resolving.
|
|
223
224
|
*/
|
|
224
225
|
export { KubernetesNetworkPolicyHostError }
|
|
225
226
|
|
|
226
|
-
/** A DNS name, lowercase, no scheme, no port, no wildcard. */
|
|
227
|
-
const DNS_NAME = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/
|
|
228
|
-
|
|
229
227
|
/**
|
|
230
228
|
* One `allowedHosts` entry, validated and canonicalised to the bytes a
|
|
231
229
|
* `CiliumNetworkPolicy` should carry.
|
|
@@ -235,6 +233,13 @@ const DNS_NAME = /^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?
|
|
|
235
233
|
* `matchName` is compared against what the DNS proxy saw — lowercase. The
|
|
236
234
|
* docker backend accepts either case, and a list that worked there and threw
|
|
237
235
|
* here would be a portability trap with no boundary behind it.
|
|
236
|
+
*
|
|
237
|
+
* The grammar itself is {@link assertUsableHost}'s, in `egress-policy.ts`
|
|
238
|
+
* beside the error it throws, because it is no longer only this writer's: the
|
|
239
|
+
* shared translation calls it too, so a config-level allowlist is refused the
|
|
240
|
+
* same entries this path refuses. What is HERE is the part that is this
|
|
241
|
+
* writer's alone — the canonicalisation, applied before the fence is read and
|
|
242
|
+
* before anything is queued behind a previous call.
|
|
238
243
|
*/
|
|
239
244
|
function normalizeHost(entry: string): string {
|
|
240
245
|
if (typeof entry !== 'string') {
|
|
@@ -245,55 +250,6 @@ function normalizeHost(entry: string): string {
|
|
|
245
250
|
return canonical
|
|
246
251
|
}
|
|
247
252
|
|
|
248
|
-
function assertUsableHost(entry: string): void {
|
|
249
|
-
if (typeof entry !== 'string' || entry === '') {
|
|
250
|
-
throw new KubernetesNetworkPolicyHostError(String(entry), 'is empty')
|
|
251
|
-
}
|
|
252
|
-
const bare = entry.startsWith('.') ? entry.slice(1) : entry
|
|
253
|
-
if (bare === '') {
|
|
254
|
-
throw new KubernetesNetworkPolicyHostError(entry, 'names no domain after its leading dot')
|
|
255
|
-
}
|
|
256
|
-
if (entry.includes('*')) {
|
|
257
|
-
throw new KubernetesNetworkPolicyHostError(
|
|
258
|
-
entry,
|
|
259
|
-
"contains a glob; a domain and its subdomains are written with a leading dot ('.example.com'), which becomes matchName plus matchPattern",
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
if (!DNS_NAME.test(bare)) {
|
|
263
|
-
throw new KubernetesNetworkPolicyHostError(
|
|
264
|
-
entry,
|
|
265
|
-
'is not a DNS name (a scheme, a path, a port suffix and an IP address all land here; letter case is canonicalised before this check, so it is never the cause)',
|
|
266
|
-
)
|
|
267
|
-
}
|
|
268
|
-
if (bare.length > 253) {
|
|
269
|
-
throw new KubernetesNetworkPolicyHostError(entry, 'is longer than a DNS name may be')
|
|
270
|
-
}
|
|
271
|
-
// A leading-dot entry becomes `matchPattern: '*.<domain>'`, and a
|
|
272
|
-
// single-label domain there is a whole public suffix — `.com`, `.org`.
|
|
273
|
-
// That is not an allowlist entry, and the shipped admission fence refuses
|
|
274
|
-
// the pattern it would produce, so refusing it HERE is what turns an
|
|
275
|
-
// opaque 403 from the API server into an error naming the entry.
|
|
276
|
-
if (entry.startsWith('.') && !bare.includes('.')) {
|
|
277
|
-
throw new KubernetesNetworkPolicyHostError(
|
|
278
|
-
entry,
|
|
279
|
-
"names a whole top-level domain ('.com' means every name under it); a domain entry needs at least two labels, as in '.example.com', and the shipped admission policy refuses the '*.com' pattern this would emit",
|
|
280
|
-
)
|
|
281
|
-
}
|
|
282
|
-
// An IPv4 literal passes the grammar above — every label is digits, and
|
|
283
|
-
// digits are legal in a DNS label. It is still not a hostname: a DNS
|
|
284
|
-
// top-level label is never all-numeric, and Cilium's `matchName` is
|
|
285
|
-
// compared against names the DNS proxy SAW, which an address never is. A
|
|
286
|
-
// policy carrying one is admitted and matches nothing, which reads from
|
|
287
|
-
// outside exactly like a policy that is working.
|
|
288
|
-
const lastLabel = bare.slice(bare.lastIndexOf('.') + 1)
|
|
289
|
-
if (/^[0-9]+$/.test(lastLabel)) {
|
|
290
|
-
throw new KubernetesNetworkPolicyHostError(
|
|
291
|
-
entry,
|
|
292
|
-
'ends in an all-numeric label, so it is an address rather than a hostname; toFQDNs matches names a DNS lookup returned, and an address is never one of them (use config.egress.policy for address-based egress)',
|
|
293
|
-
)
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
253
|
/**
|
|
298
254
|
* Raised when the object an acquire created reported no `metadata.uid`.
|
|
299
255
|
*
|
|
@@ -434,7 +390,7 @@ export function buildPerSandboxPolicySetter(
|
|
|
434
390
|
policyKind: 'static',
|
|
435
391
|
...(perSandbox.narrowing !== undefined ? { narrowing: perSandbox.narrowing } : {}),
|
|
436
392
|
ownerReferences: [perSandboxPolicyOwnerReference(owner)],
|
|
437
|
-
|
|
393
|
+
refusalContext: PER_SANDBOX_NARROWING_REFUSAL,
|
|
438
394
|
})
|
|
439
395
|
|
|
440
396
|
const write = async (translated: KubernetesTranslatedEgressPolicy): Promise<void> => {
|
|
@@ -497,13 +453,12 @@ export function buildPerSandboxPolicySetter(
|
|
|
497
453
|
perSandbox.narrowing,
|
|
498
454
|
// The per-sandbox context, from `egress-policy.ts` rather than
|
|
499
455
|
// rebuilt here: it is the SAME value `buildCiliumEgressManifest`
|
|
500
|
-
// refuses by on this path (`
|
|
501
|
-
//
|
|
502
|
-
//
|
|
503
|
-
//
|
|
504
|
-
// config
|
|
505
|
-
//
|
|
506
|
-
// and its subdomains.
|
|
456
|
+
// refuses by on this path (`refusalContext`), so the earlier check
|
|
457
|
+
// and the translation's own cannot name different fields — and the
|
|
458
|
+
// sentence it carries is the true one here, where the entry really
|
|
459
|
+
// does become a name plus a `*.domain` pattern. Sending this caller
|
|
460
|
+
// to `config.egress.ciliumNarrowing` would name a field a
|
|
461
|
+
// `perSandbox` host never set.
|
|
507
462
|
PER_SANDBOX_NARROWING_REFUSAL,
|
|
508
463
|
)
|
|
509
464
|
// A previous call's failure belongs to that caller; this one still runs,
|
package/src/index.ts
CHANGED
|
@@ -9,11 +9,29 @@
|
|
|
9
9
|
*
|
|
10
10
|
* Two tiers, each a trust boundary:
|
|
11
11
|
*
|
|
12
|
-
* • `container` — one OCI container per task,
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* • `container` — one OCI container per task, with the container itself
|
|
13
|
+
* as the boundary: kernel namespaces, or a userspace-kernel runtime
|
|
14
|
+
* where one is installed. The same path on a laptop and on a Linux
|
|
15
|
+
* replica anywhere. The tier for trusted prompts and contained
|
|
16
|
+
* workloads. What confines the workload INSIDE the container is
|
|
17
|
+
* applied by each backend and documented where it is applied, not
|
|
18
|
+
* promised here: `container:docker` drops every capability, sets
|
|
19
|
+
* no-new-privileges, gives the container an IPC namespace nothing else can
|
|
20
|
+
* join and mounts its root filesystem read-only over a named writable set
|
|
21
|
+
* (see `backends/docker/index.ts`), while the ACI standby pool takes
|
|
22
|
+
* its controls from the container-group profile its pool was built
|
|
23
|
+
* from. This line used to claim "seccomp on, tmpfs workdir, no
|
|
24
|
+
* network unless asked" on both their behalf, and none of the three
|
|
25
|
+
* is a property this package establishes: nothing here passes a
|
|
26
|
+
* seccomp flag, so what filters a container's syscalls is the daemon's
|
|
27
|
+
* own profile rather than a default this package sets; the directory
|
|
28
|
+
* the agent works in is the layout's `outputs` bind mount — a host
|
|
29
|
+
* directory the run is collected from — rather than a tmpfs that would
|
|
30
|
+
* lose it when the container exits; and the network a container is
|
|
31
|
+
* attached to is a fact about
|
|
32
|
+
* the backend's own configuration — a daemon network whose internals
|
|
33
|
+
* the egress policy is checked against, or the container group's
|
|
34
|
+
* subnet or public address.
|
|
17
35
|
*
|
|
18
36
|
* • `microvm` — one hardware-virtualized guest per task. The boundary
|
|
19
37
|
* to reach for when the prompt itself is adversarial, at the cost
|
|
@@ -685,6 +703,46 @@ export interface ContainerBackendConfig {
|
|
|
685
703
|
* collisions with Docker / orchestrator labels.
|
|
686
704
|
*/
|
|
687
705
|
readonly labels?: Readonly<Record<string, string>>
|
|
706
|
+
/**
|
|
707
|
+
* CPU cores the container may use, rendered as `--cpus`. Unset by
|
|
708
|
+
* default, like `memoryLimitMb` and `maxProcesses`, and for the same
|
|
709
|
+
* reason: the value that is right is a property of the host's machine
|
|
710
|
+
* and of the workload, and a number chosen here would silently throttle
|
|
711
|
+
* runs that finish inside their timeout today.
|
|
712
|
+
*
|
|
713
|
+
* It is set at provider construction rather than per `create()` call,
|
|
714
|
+
* because the documented deployment builds one provider per task — and
|
|
715
|
+
* because the ACI and kubernetes backends cannot apply a per-sandbox CPU
|
|
716
|
+
* limit, so a per-call field would be a control they would have to
|
|
717
|
+
* refuse. See `backends/docker/index.ts` for what it renders.
|
|
718
|
+
*/
|
|
719
|
+
readonly cpuLimit?: number
|
|
720
|
+
/**
|
|
721
|
+
* Mount the container's root filesystem read-only. Default `true`.
|
|
722
|
+
*
|
|
723
|
+
* On by default with the paths that stay writable named in
|
|
724
|
+
* `backends/docker/index.ts` (`writableRootfsPaths` extends them). Set it
|
|
725
|
+
* to `false` to make the whole container filesystem writable again, which a
|
|
726
|
+
* host whose image writes somewhere the writable set cannot describe needs,
|
|
727
|
+
* and which is why the switch exists rather than the baseline being
|
|
728
|
+
* unconditional. It gives up that one control: the capability drop,
|
|
729
|
+
* `no-new-privileges` and `--ipc private` are applied to every container
|
|
730
|
+
* whatever this says, so it is not a way back to the previous argv.
|
|
731
|
+
*/
|
|
732
|
+
readonly readOnlyRootfs?: boolean
|
|
733
|
+
/**
|
|
734
|
+
* Extra paths to keep writable under `--read-only`, each mounted
|
|
735
|
+
* `--tmpfs`.
|
|
736
|
+
*
|
|
737
|
+
* The default set is the reference image's needs, read off its Dockerfile.
|
|
738
|
+
* A host that points `image` at its own build says what that image needs
|
|
739
|
+
* here, because the backend cannot read an image's writable set and the
|
|
740
|
+
* alternative to asking is guessing. Setting this beside
|
|
741
|
+
* `readOnlyRootfs: false` is refused: with a writable root filesystem the
|
|
742
|
+
* mounts would add nothing, and accepting a control that is not applied is
|
|
743
|
+
* the failure this package refuses everywhere else.
|
|
744
|
+
*/
|
|
745
|
+
readonly writableRootfsPaths?: readonly string[]
|
|
688
746
|
}
|
|
689
747
|
|
|
690
748
|
/**
|
|
@@ -1260,6 +1318,11 @@ function pickBackend(config: SandboxProviderConfig): SandboxBackend {
|
|
|
1260
1318
|
...(backend.network !== undefined ? { network: backend.network } : {}),
|
|
1261
1319
|
...(backend.allowInwardFor !== undefined ? { allowInwardFor: backend.allowInwardFor } : {}),
|
|
1262
1320
|
...(backend.labels !== undefined ? { labels: backend.labels } : {}),
|
|
1321
|
+
...(backend.cpuLimit !== undefined ? { cpuLimit: backend.cpuLimit } : {}),
|
|
1322
|
+
...(backend.readOnlyRootfs !== undefined ? { readOnlyRootfs: backend.readOnlyRootfs } : {}),
|
|
1323
|
+
...(backend.writableRootfsPaths !== undefined
|
|
1324
|
+
? { writableRootfsPaths: backend.writableRootfsPaths }
|
|
1325
|
+
: {}),
|
|
1263
1326
|
})
|
|
1264
1327
|
}
|
|
1265
1328
|
if (backend.tier === 'container' && backend.runtime === 'runsc') {
|
|
@@ -1280,6 +1343,11 @@ function pickBackend(config: SandboxProviderConfig): SandboxBackend {
|
|
|
1280
1343
|
...(backend.network !== undefined ? { network: backend.network } : {}),
|
|
1281
1344
|
...(backend.allowInwardFor !== undefined ? { allowInwardFor: backend.allowInwardFor } : {}),
|
|
1282
1345
|
...(backend.labels !== undefined ? { labels: backend.labels } : {}),
|
|
1346
|
+
...(backend.cpuLimit !== undefined ? { cpuLimit: backend.cpuLimit } : {}),
|
|
1347
|
+
...(backend.readOnlyRootfs !== undefined ? { readOnlyRootfs: backend.readOnlyRootfs } : {}),
|
|
1348
|
+
...(backend.writableRootfsPaths !== undefined
|
|
1349
|
+
? { writableRootfsPaths: backend.writableRootfsPaths }
|
|
1350
|
+
: {}),
|
|
1283
1351
|
})
|
|
1284
1352
|
}
|
|
1285
1353
|
// `microvm:self-hosted` targeting the OWNED Azure Firecracker
|