@namzu/sandbox 13.0.0 → 14.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 +309 -0
- package/README.md +151 -0
- package/dist/backends/firecracker/protocol.d.ts +22 -0
- package/dist/backends/firecracker/protocol.d.ts.map +1 -1
- package/dist/backends/firecracker/protocol.js.map +1 -1
- package/dist/backends/firecracker/transport.d.ts +104 -9
- package/dist/backends/firecracker/transport.d.ts.map +1 -1
- package/dist/backends/firecracker/transport.js +139 -13
- package/dist/backends/firecracker/transport.js.map +1 -1
- package/dist/backends/kubernetes/egress-policy.d.ts +219 -0
- package/dist/backends/kubernetes/egress-policy.d.ts.map +1 -0
- package/dist/backends/kubernetes/egress-policy.js +314 -0
- package/dist/backends/kubernetes/egress-policy.js.map +1 -0
- package/dist/backends/kubernetes/index.d.ts +374 -0
- package/dist/backends/kubernetes/index.d.ts.map +1 -0
- package/dist/backends/kubernetes/index.js +671 -0
- package/dist/backends/kubernetes/index.js.map +1 -0
- package/dist/backends/kubernetes/k8s-client.d.ts +125 -0
- package/dist/backends/kubernetes/k8s-client.d.ts.map +1 -0
- package/dist/backends/kubernetes/k8s-client.js +246 -0
- package/dist/backends/kubernetes/k8s-client.js.map +1 -0
- package/dist/backends/kubernetes/lease.d.ts +119 -0
- package/dist/backends/kubernetes/lease.d.ts.map +1 -0
- package/dist/backends/kubernetes/lease.js +151 -0
- package/dist/backends/kubernetes/lease.js.map +1 -0
- package/dist/backends/kubernetes/objects.d.ts +282 -0
- package/dist/backends/kubernetes/objects.d.ts.map +1 -0
- package/dist/backends/kubernetes/objects.js +156 -0
- package/dist/backends/kubernetes/objects.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/sandbox.d.ts +123 -0
- package/dist/backends/kubernetes/sandbox.d.ts.map +1 -0
- package/dist/backends/kubernetes/sandbox.js +299 -0
- package/dist/backends/kubernetes/sandbox.js.map +1 -0
- package/dist/backends/kubernetes/transport.d.ts +122 -0
- package/dist/backends/kubernetes/transport.d.ts.map +1 -0
- package/dist/backends/kubernetes/transport.js +197 -0
- package/dist/backends/kubernetes/transport.js.map +1 -0
- package/dist/backends/kubernetes/workspace.d.ts +381 -0
- package/dist/backends/kubernetes/workspace.d.ts.map +1 -0
- package/dist/backends/kubernetes/workspace.js +1064 -0
- package/dist/backends/kubernetes/workspace.js.map +1 -0
- package/dist/index.d.ts +132 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +102 -34
- package/dist/index.js.map +1 -1
- package/dist/testing/sandbox-conformance.d.ts +193 -0
- package/dist/testing/sandbox-conformance.d.ts.map +1 -0
- package/dist/testing/sandbox-conformance.js +465 -0
- package/dist/testing/sandbox-conformance.js.map +1 -0
- package/package.json +5 -4
- package/src/backends/firecracker/protocol.ts +27 -0
- package/src/backends/firecracker/transport.ts +199 -28
- package/src/backends/kubernetes/egress-policy.ts +437 -0
- package/src/backends/kubernetes/index.ts +1012 -0
- package/src/backends/kubernetes/k8s-client.ts +352 -0
- package/src/backends/kubernetes/lease.ts +198 -0
- package/src/backends/kubernetes/objects.ts +363 -0
- package/src/backends/kubernetes/privilege-probe.ts +261 -0
- package/src/backends/kubernetes/sandbox.ts +395 -0
- package/src/backends/kubernetes/transport.ts +286 -0
- package/src/backends/kubernetes/workspace.ts +1386 -0
- package/src/index.ts +257 -35
- package/src/testing/sandbox-conformance.ts +667 -0
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Egress policy translation for the Kubernetes backend, and the
|
|
3
|
+
* verify-not-trust check that stops a misconfigured cluster from silently
|
|
4
|
+
* running unenforced.
|
|
5
|
+
*
|
|
6
|
+
* Mirrors two existing shapes rather than inventing a third:
|
|
7
|
+
*
|
|
8
|
+
* - `../firecracker/index.ts`'s `resolveNetworkPolicy` — a pure(ish) mapping
|
|
9
|
+
* from the shared {@link EgressPolicy} union to what one specific
|
|
10
|
+
* enforcement point can actually carry, with an exhaustive switch that
|
|
11
|
+
* refuses an unknown kind instead of defaulting to open.
|
|
12
|
+
* - `../docker/index.ts`'s `assertNetworkCarriesThePolicy` — the refusal
|
|
13
|
+
* discipline. That function does not trust a docker network's NAME to
|
|
14
|
+
* mean "internal"; it inspects the daemon's own `{{.Internal}}` flag. The
|
|
15
|
+
* same principle drives {@link verifyEgressPolicyApplied} here: this
|
|
16
|
+
* backend does not trust that an operator applied the right
|
|
17
|
+
* `NetworkPolicy` because a name matches — it GETs the live object and
|
|
18
|
+
* compares its shape.
|
|
19
|
+
*
|
|
20
|
+
* ## What core `NetworkPolicy` can and cannot express
|
|
21
|
+
*
|
|
22
|
+
* Core Kubernetes `NetworkPolicy` has exactly three ways to name a
|
|
23
|
+
* destination: `ipBlock` (CIDR), `podSelector` and `namespaceSelector`. There
|
|
24
|
+
* is no hostname or FQDN concept anywhere in the resource. So of the four
|
|
25
|
+
* {@link EgressPolicy} kinds:
|
|
26
|
+
*
|
|
27
|
+
* - `deny-all` and `allow-all` are fully expressible — neither needs a
|
|
28
|
+
* hostname.
|
|
29
|
+
* - `static` and `resolver` are hostname allowlists and are NOT expressible
|
|
30
|
+
* under core `NetworkPolicy` at all. {@link assertEgressPolicyIsEnforceable}
|
|
31
|
+
* throws a named error for both, rather than silently downgrading to an
|
|
32
|
+
* `HTTP_PROXY`/`HTTPS_PROXY` environment variable the way the container
|
|
33
|
+
* tier's open allowlist gap does (`#398`) — a workload that ignores a
|
|
34
|
+
* proxy variable bypasses it entirely, and repeating that mistake behind a
|
|
35
|
+
* Kubernetes-shaped manifest would not fix it.
|
|
36
|
+
*
|
|
37
|
+
* `KubernetesEgressConfig.engine: 'cilium'` is the one escape hatch, and it
|
|
38
|
+
* is opt-in: a cluster that actually runs Cilium can express a hostname
|
|
39
|
+
* allowlist as a `CiliumNetworkPolicy` with `toFQDNs`, so declaring the
|
|
40
|
+
* engine turns the refusal into an emission. No other engine is supported in
|
|
41
|
+
* this batch — Calico's `HostEndpoint`/`GlobalNetworkPolicy` and any other
|
|
42
|
+
* FQDN-capable CNI would need their own translation, added as their own
|
|
43
|
+
* `engine` value with their own resource shape, not folded into this one by
|
|
44
|
+
* guessing.
|
|
45
|
+
*
|
|
46
|
+
* ## Why a NetworkPolicy always allows the cluster's own DNS
|
|
47
|
+
*
|
|
48
|
+
* agent-sandbox's OWN managed `NetworkPolicy`
|
|
49
|
+
* (`SandboxTemplateSpec.networkPolicy` /
|
|
50
|
+
* `networkPolicyManagement: Managed`, the CRD's default) already leaves the
|
|
51
|
+
* cluster's own name service reachable — the upstream reference deployment
|
|
52
|
+
* (kubernetes-sigs/agent-sandbox `examples/kata-aks/sandboxtemplate.yaml`)
|
|
53
|
+
* ships exactly "ingress from the router only, egress limited to DNS (53)
|
|
54
|
+
* and TCP/443". {@link CLUSTER_DNS_EGRESS_RULE} matches that convention
|
|
55
|
+
* (CoreDNS/kube-dns lives in the `kube-system` namespace on every cluster
|
|
56
|
+
* this was checked against) rather than inventing a different one.
|
|
57
|
+
*
|
|
58
|
+
* This is not a case of one policy overriding another: the Kubernetes API
|
|
59
|
+
* server UNIONS every `NetworkPolicy` selecting a pod — traffic is allowed if
|
|
60
|
+
* ANY matching policy's rule allows it, never the intersection — so this
|
|
61
|
+
* rule does not need to duplicate the managed policy to avoid "fighting" it
|
|
62
|
+
* in the sense of narrowing anything. It exists so THIS backend's own
|
|
63
|
+
* translated policy is self-sufficient: on a cluster where an operator left
|
|
64
|
+
* `networkPolicyManagement: Unmanaged`, or copied the managed shape
|
|
65
|
+
* imperfectly, `deny-all` still means "the cluster's own name service,
|
|
66
|
+
* nothing else" instead of a pod that cannot even resolve
|
|
67
|
+
* `kubernetes.default`.
|
|
68
|
+
*
|
|
69
|
+
* ## Never a proxy environment variable
|
|
70
|
+
*
|
|
71
|
+
* No code path in this module emits `HTTP_PROXY`, `HTTPS_PROXY` or any other
|
|
72
|
+
* proxy variable, and none of the module's own string constants contain
|
|
73
|
+
* those substrings either — a dedicated test scans both. Proxy variables are
|
|
74
|
+
* advisory: a process that does not read them is not bounded by them. The
|
|
75
|
+
* boundary here is the `NetworkPolicy` object itself, enforced by the CNI in
|
|
76
|
+
* the kernel, not by the workload's cooperation.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
import { isDeepStrictEqual } from 'node:util'
|
|
80
|
+
import type { EgressPolicy } from '../../index.js'
|
|
81
|
+
import { KubernetesAlreadyGoneError, type KubernetesClient } from './k8s-client.js'
|
|
82
|
+
import {
|
|
83
|
+
CILIUM_NETWORK_POLICY_API_GROUP,
|
|
84
|
+
CILIUM_NETWORK_POLICY_API_VERSION,
|
|
85
|
+
CORE_NETWORK_POLICY_API_GROUP,
|
|
86
|
+
CORE_NETWORK_POLICY_API_VERSION,
|
|
87
|
+
ciliumNetworkPolicyPath,
|
|
88
|
+
networkPolicyPath,
|
|
89
|
+
sandboxTemplateLabel,
|
|
90
|
+
} from './objects.js'
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* `'core'` (the default) is plain Kubernetes `NetworkPolicy` — every cluster
|
|
94
|
+
* has it, and it cannot express a hostname. `'cilium'` opts into emitting a
|
|
95
|
+
* `CiliumNetworkPolicy` for a hostname allowlist, and is meaningless unless
|
|
96
|
+
* the target cluster actually runs Cilium as its CNI.
|
|
97
|
+
*/
|
|
98
|
+
export type KubernetesEgressEngine = 'core' | 'cilium'
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* The config-level egress hook on {@link KubernetesBackendConfig}.
|
|
102
|
+
*
|
|
103
|
+
* Deliberately CONFIG-level, not per-`create()`: the enforcement point is a
|
|
104
|
+
* `NetworkPolicy` attached to the `SandboxTemplate` (or, for `static`, to a
|
|
105
|
+
* dedicated `CiliumNetworkPolicy`), and neither can be rewritten per running
|
|
106
|
+
* sandbox — the same reason `SandboxBackendOptions.egress` is refused by
|
|
107
|
+
* `assertEnforceable` in `index.ts`. That refusal is unchanged by this file;
|
|
108
|
+
* it covers a caller trying to override egress PER SANDBOX, while this is
|
|
109
|
+
* the one egress policy the whole backend enforces.
|
|
110
|
+
*/
|
|
111
|
+
export interface KubernetesEgressConfig {
|
|
112
|
+
readonly policy: EgressPolicy
|
|
113
|
+
/**
|
|
114
|
+
* Name of the `NetworkPolicy` (or `CiliumNetworkPolicy`, under the
|
|
115
|
+
* `'cilium'` engine) an operator applied. Defaults to
|
|
116
|
+
* {@link defaultEgressPolicyName}'s output for the configured
|
|
117
|
+
* `sandboxTemplateName`.
|
|
118
|
+
*/
|
|
119
|
+
readonly networkPolicyName?: string
|
|
120
|
+
/** Default `'core'`. See the type doc. */
|
|
121
|
+
readonly engine?: KubernetesEgressEngine
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Where a translated policy is targeted, and what its `podSelector` names. */
|
|
125
|
+
export interface EgressPolicyTarget {
|
|
126
|
+
readonly namespace: string
|
|
127
|
+
readonly name: string
|
|
128
|
+
/**
|
|
129
|
+
* The label VALUE the translated policy's `podSelector` /
|
|
130
|
+
* `endpointSelector` matches — see {@link sandboxTemplateLabel} for the
|
|
131
|
+
* key and why every Sandbox this backend produces, pooled or direct,
|
|
132
|
+
* carries it.
|
|
133
|
+
*/
|
|
134
|
+
readonly sandboxTemplateName: string
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A translated policy: which kind of Kubernetes object it is, where it
|
|
139
|
+
* lives, and the manifest an operator applies verbatim. `kind` is the
|
|
140
|
+
* ENFORCEMENT RESOURCE this translation actually produced — for `deny-all`
|
|
141
|
+
* and `allow-all` that is always `'NetworkPolicy'`, even when
|
|
142
|
+
* `KubernetesEgressConfig.engine` is `'cilium'`, because core
|
|
143
|
+
* `NetworkPolicy` already expresses both fully and Cilium enforces plain
|
|
144
|
+
* `NetworkPolicy` objects too. `engine` only changes the outcome for a
|
|
145
|
+
* hostname allowlist, which needs Cilium's CRD to exist at all.
|
|
146
|
+
*/
|
|
147
|
+
export interface KubernetesTranslatedEgressPolicy {
|
|
148
|
+
readonly kind: 'NetworkPolicy' | 'CiliumNetworkPolicy'
|
|
149
|
+
readonly namespace: string
|
|
150
|
+
readonly name: string
|
|
151
|
+
readonly manifest: Readonly<Record<string, unknown>>
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** `${sandboxTemplateName}-egress`, the name {@link KubernetesEgressConfig.networkPolicyName} defaults to. */
|
|
155
|
+
export function defaultEgressPolicyName(sandboxTemplateName: string): string {
|
|
156
|
+
return `${sandboxTemplateName}-egress`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Named refusal for a hostname allowlist with no FQDN-capable engine
|
|
161
|
+
* declared. Thrown SYNCHRONOUSLY from `buildKubernetesBackend` — see
|
|
162
|
+
* {@link assertEgressPolicyIsEnforceable} — so a misconfiguration surfaces
|
|
163
|
+
* during host wiring, the same moment `assertRuntimeClassIsApplicable` and
|
|
164
|
+
* the readiness-bounds validation do, rather than on the first `create()`.
|
|
165
|
+
*/
|
|
166
|
+
export class KubernetesUnenforceableEgressPolicyError extends Error {
|
|
167
|
+
override readonly name = 'KubernetesUnenforceableEgressPolicyError'
|
|
168
|
+
|
|
169
|
+
constructor(readonly policyKind: 'static' | 'resolver') {
|
|
170
|
+
super(
|
|
171
|
+
`The kubernetes sandbox backend cannot enforce an egress policy of kind '${policyKind}' as a NetworkPolicy: core Kubernetes NetworkPolicy has only ipBlock, podSelector and namespaceSelector — it has no hostname/FQDN concept at all. This cluster needs an FQDN-capable policy engine: set config.egress.engine to 'cilium' if the target cluster actually runs it (see docs/sdk/kubernetes-sandbox.md's egress section), or configure an egress policy of 'deny-all' or 'allow-all' instead. Refusing rather than silently downgrading to an HTTP_PROXY/HTTPS_PROXY environment variable an uncooperative process can ignore.`,
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Synchronous, no-I/O precondition: can `policy.kind` be enforced under
|
|
178
|
+
* `engine` at all. Deliberately decided from the KIND alone — a `resolver`
|
|
179
|
+
* policy's `resolve()` is never invoked here, both because calling it just to
|
|
180
|
+
* prove a refusal would be wasted work (and possibly a side effect the host
|
|
181
|
+
* did not expect yet) and because this has to stay callable synchronously
|
|
182
|
+
* from `buildKubernetesBackend`, which contacts nothing.
|
|
183
|
+
*/
|
|
184
|
+
export function assertEgressPolicyIsEnforceable(
|
|
185
|
+
policy: EgressPolicy,
|
|
186
|
+
engine: KubernetesEgressEngine,
|
|
187
|
+
): void {
|
|
188
|
+
if ((policy.kind === 'static' || policy.kind === 'resolver') && engine !== 'cilium') {
|
|
189
|
+
throw new KubernetesUnenforceableEgressPolicyError(policy.kind)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* The cluster DNS egress rule every translated `NetworkPolicy` carries,
|
|
195
|
+
* including on `deny-all`. See the module doc's "Why a NetworkPolicy always
|
|
196
|
+
* allows the cluster's own DNS" section.
|
|
197
|
+
*/
|
|
198
|
+
const CLUSTER_DNS_EGRESS_RULE = {
|
|
199
|
+
to: [{ namespaceSelector: { matchLabels: { 'kubernetes.io/metadata.name': 'kube-system' } } }],
|
|
200
|
+
ports: [
|
|
201
|
+
{ protocol: 'UDP', port: 53 },
|
|
202
|
+
{ protocol: 'TCP', port: 53 },
|
|
203
|
+
],
|
|
204
|
+
} as const
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Cilium requires DNS lookups to be explicitly allowed AND made visible to
|
|
208
|
+
* the agent before `toFQDNs` enforcement can match anything a name resolves
|
|
209
|
+
* to: without a preceding rule granting DNS and turning on visibility, the
|
|
210
|
+
* sandbox's own lookups for the allowed hosts are invisible to Cilium's
|
|
211
|
+
* FQDN-to-IP mapping and `toFQDNs` matches nothing, which would make a
|
|
212
|
+
* `static`/`resolver` policy fail closed for every host rather than only the
|
|
213
|
+
* disallowed ones.
|
|
214
|
+
*
|
|
215
|
+
* Shape verified against Cilium's own worked example — the `toFQDNs`
|
|
216
|
+
* `CiliumNetworkPolicy` at https://docs.cilium.io/en/stable/security/dns/
|
|
217
|
+
* (the "DNS Based" policy walkthrough) allows egress to `kube-dns` on port 53
|
|
218
|
+
* with `rules.dns: [{ matchPattern: "*" }]` alongside its own `toFQDNs` rule
|
|
219
|
+
* — this constant reproduces that DNS-allow rule verbatim, including the
|
|
220
|
+
* `k8s:` label-source prefix on both `toEndpoints` selector keys (an
|
|
221
|
+
* unprefixed key defaults to label source `any`, a superset that would still
|
|
222
|
+
* match, but `any` is not what the cited example uses).
|
|
223
|
+
*/
|
|
224
|
+
const CILIUM_DNS_VISIBILITY_RULE = {
|
|
225
|
+
toEndpoints: [
|
|
226
|
+
{
|
|
227
|
+
matchLabels: {
|
|
228
|
+
'k8s:io.kubernetes.pod.namespace': 'kube-system',
|
|
229
|
+
'k8s:k8s-app': 'kube-dns',
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
],
|
|
233
|
+
toPorts: [
|
|
234
|
+
{
|
|
235
|
+
ports: [{ port: '53', protocol: 'ANY' }],
|
|
236
|
+
rules: { dns: [{ matchPattern: '*' }] },
|
|
237
|
+
},
|
|
238
|
+
],
|
|
239
|
+
} as const
|
|
240
|
+
|
|
241
|
+
function buildCoreNetworkPolicy(
|
|
242
|
+
target: EgressPolicyTarget,
|
|
243
|
+
egress: readonly Readonly<Record<string, unknown>>[],
|
|
244
|
+
): KubernetesTranslatedEgressPolicy {
|
|
245
|
+
return {
|
|
246
|
+
kind: 'NetworkPolicy',
|
|
247
|
+
namespace: target.namespace,
|
|
248
|
+
name: target.name,
|
|
249
|
+
manifest: {
|
|
250
|
+
apiVersion: `${CORE_NETWORK_POLICY_API_GROUP}/${CORE_NETWORK_POLICY_API_VERSION}`,
|
|
251
|
+
kind: 'NetworkPolicy',
|
|
252
|
+
metadata: { name: target.name, namespace: target.namespace },
|
|
253
|
+
spec: {
|
|
254
|
+
podSelector: { matchLabels: sandboxTemplateLabel(target.sandboxTemplateName) },
|
|
255
|
+
policyTypes: ['Egress'],
|
|
256
|
+
egress,
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function buildCiliumNetworkPolicy(
|
|
263
|
+
target: EgressPolicyTarget,
|
|
264
|
+
allowedHosts: readonly string[],
|
|
265
|
+
): KubernetesTranslatedEgressPolicy {
|
|
266
|
+
return {
|
|
267
|
+
kind: 'CiliumNetworkPolicy',
|
|
268
|
+
namespace: target.namespace,
|
|
269
|
+
name: target.name,
|
|
270
|
+
manifest: {
|
|
271
|
+
apiVersion: `${CILIUM_NETWORK_POLICY_API_GROUP}/${CILIUM_NETWORK_POLICY_API_VERSION}`,
|
|
272
|
+
kind: 'CiliumNetworkPolicy',
|
|
273
|
+
metadata: { name: target.name, namespace: target.namespace },
|
|
274
|
+
spec: {
|
|
275
|
+
endpointSelector: { matchLabels: sandboxTemplateLabel(target.sandboxTemplateName) },
|
|
276
|
+
egress: [
|
|
277
|
+
CILIUM_DNS_VISIBILITY_RULE,
|
|
278
|
+
{ toFQDNs: allowedHosts.map((host) => ({ matchName: host })) },
|
|
279
|
+
],
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* The pure translation: an {@link EgressPolicy} plus the declared
|
|
287
|
+
* {@link KubernetesEgressEngine} in, the concrete manifest this backend can
|
|
288
|
+
* ask an operator to apply out. Async only because `resolver` carries an
|
|
289
|
+
* async `resolve()` — `deny-all`, `allow-all` and `static` never await
|
|
290
|
+
* anything.
|
|
291
|
+
*
|
|
292
|
+
* `resolver` is resolved ONCE here (at whatever call site first needs the
|
|
293
|
+
* manifest — `buildKubernetesBackend`'s first `create()`, in practice), not
|
|
294
|
+
* once per `create()` the way the docker and Firecracker backends re-resolve
|
|
295
|
+
* a PER-SANDBOX `resolver` policy on every call. That is a deliberate
|
|
296
|
+
* difference, not a shortcut: the enforcement point here is one
|
|
297
|
+
* `NetworkPolicy`/`CiliumNetworkPolicy` object shared by every sandbox this
|
|
298
|
+
* backend produces, so there is exactly one manifest to compare the resolved
|
|
299
|
+
* hosts against, and re-resolving on every `create()` would only produce a
|
|
300
|
+
* value nothing downstream re-applies to the cluster.
|
|
301
|
+
*/
|
|
302
|
+
export async function translateEgressPolicy(
|
|
303
|
+
policy: EgressPolicy,
|
|
304
|
+
engine: KubernetesEgressEngine,
|
|
305
|
+
target: EgressPolicyTarget,
|
|
306
|
+
): Promise<KubernetesTranslatedEgressPolicy> {
|
|
307
|
+
assertEgressPolicyIsEnforceable(policy, engine)
|
|
308
|
+
|
|
309
|
+
switch (policy.kind) {
|
|
310
|
+
case 'deny-all':
|
|
311
|
+
return buildCoreNetworkPolicy(target, [CLUSTER_DNS_EGRESS_RULE])
|
|
312
|
+
case 'allow-all':
|
|
313
|
+
// No `to`/`ports` on an egress rule matches every destination and
|
|
314
|
+
// every port. `CLUSTER_DNS_EGRESS_RULE` is a strict subset of this,
|
|
315
|
+
// so it is folded in rather than listed twice.
|
|
316
|
+
return buildCoreNetworkPolicy(target, [{}])
|
|
317
|
+
case 'static':
|
|
318
|
+
// `assertEgressPolicyIsEnforceable` already threw above unless
|
|
319
|
+
// `engine === 'cilium'`, so reaching here means it did not.
|
|
320
|
+
return buildCiliumNetworkPolicy(target, policy.allowedHosts)
|
|
321
|
+
case 'resolver': {
|
|
322
|
+
const allowedHosts = await policy.resolve()
|
|
323
|
+
return buildCiliumNetworkPolicy(target, allowedHosts)
|
|
324
|
+
}
|
|
325
|
+
default: {
|
|
326
|
+
const exhaustive: never = policy
|
|
327
|
+
throw new Error(
|
|
328
|
+
`kubernetes: unhandled egress policy kind ${JSON.stringify(exhaustive)}. Refusing rather than defaulting to unrestricted network access.`,
|
|
329
|
+
)
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Raised by {@link verifyEgressPolicyApplied} when the named object does not
|
|
336
|
+
* exist at all. This backend never CREATES the policy itself — like the
|
|
337
|
+
* docker backend's network, the boundary is operator-applied so it gets
|
|
338
|
+
* reviewed outside the hot create path, by whoever has cluster-admin rather
|
|
339
|
+
* than by whatever created the ServiceAccount token this backend runs with.
|
|
340
|
+
*/
|
|
341
|
+
export class KubernetesEgressPolicyNotAppliedError extends Error {
|
|
342
|
+
override readonly name = 'KubernetesEgressPolicyNotAppliedError'
|
|
343
|
+
|
|
344
|
+
constructor(
|
|
345
|
+
readonly resourceKind: 'NetworkPolicy' | 'CiliumNetworkPolicy',
|
|
346
|
+
readonly path: string,
|
|
347
|
+
) {
|
|
348
|
+
super(
|
|
349
|
+
`kubernetes: config.egress is set but no ${resourceKind} exists at ${path}. This backend never creates the egress policy itself — apply the manifest it computed (see docs/sdk/kubernetes-sandbox.md's egress section) before creating a sandbox with an egress policy configured. Refusing rather than creating a sandbox with no enforced network boundary.`,
|
|
350
|
+
)
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Raised when the applied object exists but does not match the translation. */
|
|
355
|
+
export class KubernetesEgressPolicyMismatchError extends Error {
|
|
356
|
+
override readonly name = 'KubernetesEgressPolicyMismatchError'
|
|
357
|
+
|
|
358
|
+
constructor(
|
|
359
|
+
readonly resourceKind: 'NetworkPolicy' | 'CiliumNetworkPolicy',
|
|
360
|
+
readonly path: string,
|
|
361
|
+
readonly reason: string,
|
|
362
|
+
) {
|
|
363
|
+
super(
|
|
364
|
+
`kubernetes: the ${resourceKind} at ${path} does not match the egress policy config.egress computed: ${reason}. Re-apply the manifest this backend translated rather than hand-editing the cluster object — a policy that has drifted from config silently changes what a sandbox can reach without config saying so.`,
|
|
365
|
+
)
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Verify-not-trust: GET the object {@link translateEgressPolicy} named and
|
|
371
|
+
* assert its shape actually matches, rather than trusting that an object
|
|
372
|
+
* with the right name does what config says. Mirrors
|
|
373
|
+
* `../docker/index.ts`'s `assertNetworkCarriesThePolicy`, which inspects the
|
|
374
|
+
* daemon's own `{{.Internal}}` flag instead of trusting a network's name.
|
|
375
|
+
*
|
|
376
|
+
* Checks exactly three things, each named separately in a mismatch so an
|
|
377
|
+
* operator sees which one to fix:
|
|
378
|
+
* - the selector (`podSelector` for `NetworkPolicy`, `endpointSelector` for
|
|
379
|
+
* `CiliumNetworkPolicy`) carries the expected template label:
|
|
380
|
+
* - `NetworkPolicy` additionally declares `policyTypes` including
|
|
381
|
+
* `'Egress'` — a `NetworkPolicy` with an `egress` array but no `'Egress'`
|
|
382
|
+
* in `policyTypes` enforces nothing on egress at all;
|
|
383
|
+
* - the `egress` rule array matches the translation exactly.
|
|
384
|
+
*
|
|
385
|
+
* Never mutates and never creates — a 404/410 is refused, not repaired.
|
|
386
|
+
*/
|
|
387
|
+
export async function verifyEgressPolicyApplied(
|
|
388
|
+
client: KubernetesClient,
|
|
389
|
+
translated: KubernetesTranslatedEgressPolicy,
|
|
390
|
+
signal?: AbortSignal,
|
|
391
|
+
): Promise<void> {
|
|
392
|
+
const path =
|
|
393
|
+
translated.kind === 'CiliumNetworkPolicy'
|
|
394
|
+
? ciliumNetworkPolicyPath(translated.namespace, translated.name)
|
|
395
|
+
: networkPolicyPath(translated.namespace, translated.name)
|
|
396
|
+
|
|
397
|
+
let resource: { readonly spec?: Readonly<Record<string, unknown>> } | undefined
|
|
398
|
+
try {
|
|
399
|
+
resource = await client.request('GET', path, undefined, signal)
|
|
400
|
+
} catch (err) {
|
|
401
|
+
if (err instanceof KubernetesAlreadyGoneError) {
|
|
402
|
+
throw new KubernetesEgressPolicyNotAppliedError(translated.kind, path)
|
|
403
|
+
}
|
|
404
|
+
throw err
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
const expectedSpec = (translated.manifest as { readonly spec: Record<string, unknown> }).spec
|
|
408
|
+
const actualSpec = resource?.spec ?? {}
|
|
409
|
+
const selectorKey = translated.kind === 'CiliumNetworkPolicy' ? 'endpointSelector' : 'podSelector'
|
|
410
|
+
|
|
411
|
+
if (!isDeepStrictEqual(actualSpec[selectorKey], expectedSpec[selectorKey])) {
|
|
412
|
+
throw new KubernetesEgressPolicyMismatchError(
|
|
413
|
+
translated.kind,
|
|
414
|
+
path,
|
|
415
|
+
`spec.${selectorKey} is ${JSON.stringify(actualSpec[selectorKey])}, expected ${JSON.stringify(expectedSpec[selectorKey])} — the label every Sandbox this backend creates carries`,
|
|
416
|
+
)
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (translated.kind === 'NetworkPolicy') {
|
|
420
|
+
const policyTypes = actualSpec.policyTypes
|
|
421
|
+
if (!Array.isArray(policyTypes) || !policyTypes.includes('Egress')) {
|
|
422
|
+
throw new KubernetesEgressPolicyMismatchError(
|
|
423
|
+
translated.kind,
|
|
424
|
+
path,
|
|
425
|
+
`spec.policyTypes is ${JSON.stringify(policyTypes)}, expected an array containing 'Egress' — without it, spec.egress enforces nothing`,
|
|
426
|
+
)
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
if (!isDeepStrictEqual(actualSpec.egress, expectedSpec.egress)) {
|
|
431
|
+
throw new KubernetesEgressPolicyMismatchError(
|
|
432
|
+
translated.kind,
|
|
433
|
+
path,
|
|
434
|
+
`spec.egress is ${JSON.stringify(actualSpec.egress)}, expected ${JSON.stringify(expectedSpec.egress)}`,
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
}
|