@jr2/orchestrator 0.2.0 → 0.3.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.
@@ -0,0 +1,899 @@
1
+ // The Custodian (ADR-0059): the jr2-composed container in every Harness pod that holds the pod's
2
+ // credentials and carries its traffic out — the Agent's Menu and the ask toward the Orchestrator
3
+ // (ADR-0013, ADR-0053), and, when a held secret exists, the Harness's outbound HTTPS.
4
+ //
5
+ // Its engine is Envoy, the upstream distroless image pinned by digest: jr2 deploys it and never
6
+ // builds it (ADR-0038). This module is the jr2 half — everything Envoy is told, and where:
7
+ //
8
+ // custodianBootstrap(...) the static bootstrap `jr2 up` writes into the `jr2-held` ConfigMap:
9
+ // no admin listener, no xDS, one internal listener per bound host
10
+ // custodianScript(...) the Lua the bootstrap runs: `custodian.lua`, with a generated table
11
+ // of names, headers, paths and targets in front — never a value
12
+ // custodianComposition(...) what a Harness pod gains: the Custodian container, its volumes, and
13
+ // the Harness container's env and mounts. BOTH placements call it —
14
+ // `kubectlSandbox` per provision, `instanceHarnessObjects` at converge —
15
+ // so the two pod shapes cannot drift
16
+ //
17
+ // The Custodian is a separate container from the Harness, and that is the point: the Agent
18
+ // executes code in the Harness container and nothing here (ADR-0005). What it holds — the
19
+ // Sandbox token and every held secret — is mounted into this container alone, and
20
+ // `shareProcessNamespace` stays off, so the Agent cannot read its env, its files or its `/proc`.
21
+
22
+ import { readFileSync } from "node:fs";
23
+ import type { HarnessEnvVar } from "./config.ts";
24
+ import {
25
+ CREDENTIAL_HEADERS,
26
+ SANDBOX_TOKEN_NAME,
27
+ standIn,
28
+ type HeldBinding,
29
+ type HeldManifest,
30
+ } from "./held-secrets.ts";
31
+ import {
32
+ CA_CONFIGMAP,
33
+ CUSTODIAN_CONTROL_PORT,
34
+ CUSTODIAN_HEALTH_PORT,
35
+ CUSTODIAN_PORT,
36
+ HELD_CONFIGMAP,
37
+ HELD_SECRETS_SECRET,
38
+ HELD_TLS_SECRET,
39
+ } from "./names.ts";
40
+
41
+ /**
42
+ * The Custodian's image: Envoy's upstream distroless build, pinned by the digest of its multi-arch
43
+ * index. Deployed, never built (ADR-0038) — jr2 writes Envoy's configuration, not Envoy. The tag
44
+ * rides along for a human reading a pod spec; the digest is what a node pulls. `kitRegistry`
45
+ * re-homes it like a Kit image, and `jr2 kit push` mirrors it (ADR-0044).
46
+ */
47
+ export const CUSTODIAN_IMAGE = {
48
+ repo: "envoyproxy/envoy",
49
+ tag: "distroless-v1.39.1",
50
+ digest: "sha256:eb2c01c13125d1629637cb4e4cce7207009fb7cc2c8027f9742758549d15b6f4",
51
+ } as const;
52
+
53
+ /** The Custodian's ref, at its canonical home or re-homed onto a `kitRegistry` mirror — the same
54
+ * two answers `publishedKitRefs` gives a Kit image (ADR-0044). */
55
+ export function custodianRef(kitRegistry?: string): string {
56
+ const { repo, tag, digest } = CUSTODIAN_IMAGE;
57
+ const home = kitRegistry ? `${kitRegistry.replace(/\/+$/, "")}/${repo.split("/").pop()}` : `docker.io/${repo}`;
58
+ return `${home}:${tag}@${digest}`;
59
+ }
60
+
61
+ /** Where the Custodian reads what it holds, and how it is told what to do with it. */
62
+ export const CUSTODIAN_VALUES = "/etc/jr2/custodian/values";
63
+ export const CUSTODIAN_TLS = "/etc/jr2/custodian/tls";
64
+ export const CUSTODIAN_CONFIG = "/etc/jr2/custodian/config";
65
+ /** The two keys of the `jr2-held` ConfigMap the Custodian reads (beside `held.json`). */
66
+ export const CUSTODIAN_BOOTSTRAP_KEY = "envoy.json";
67
+ export const CUSTODIAN_SCRIPT_KEY = "custodian.lua";
68
+
69
+ /** Where the Harness and the Custodian see the `jr2-ca` ConfigMap (ADR-0020). */
70
+ export const CA_MOUNT = "/etc/jr2/ca";
71
+
72
+ /** Envoy's command line: the rendered bootstrap, ONE worker (a pod-local sidecar needs no more,
73
+ * and the start-up line then prints once), and no hot restart — a pod replaces a container, it
74
+ * never hands sockets from one Envoy to the next. */
75
+ export const CUSTODIAN_ARGS: readonly string[] = [
76
+ "-c",
77
+ `${CUSTODIAN_CONFIG}/${CUSTODIAN_BOOTSTRAP_KEY}`,
78
+ "--concurrency",
79
+ "1",
80
+ "--disable-hot-restart",
81
+ // The access log is the `jr2.custodian` contract; flushed twice a second, not Envoy's ten.
82
+ "--file-flush-interval-msec",
83
+ "500",
84
+ "--log-level",
85
+ "warning",
86
+ ];
87
+
88
+ /** The address the Harness reaches the Custodian's control listener at: its Menu and the ask. */
89
+ export const CUSTODIAN_URL = `http://127.0.0.1:${CUSTODIAN_CONTROL_PORT}`;
90
+
91
+ /** Where the Orchestrator's Service is, from inside a Harness pod. */
92
+ export type OrchestratorAddress = { host: string; port: number };
93
+
94
+ // ---- the bootstrap ------------------------------------------------------------------------------
95
+
96
+ type Json = Record<string, unknown>;
97
+
98
+ const HCM = "type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager";
99
+ const LUA_FILTER = "envoy.filters.http.lua";
100
+
101
+ /** A stdout access log in the `jr2.custodian key=value` contract (ADR-0059: the `@kind` steps match
102
+ * on it). Names only: never a header value, a body, a path, a query or a Stand-in. `logged` limits it
103
+ * to the requests the script marked, so a CONNECT handed to an intercept listener logs there, once. */
104
+ function accessLog(target: string): Json[] {
105
+ return [
106
+ {
107
+ name: "envoy.access_loggers.stdout",
108
+ filter: {
109
+ metadata_filter: {
110
+ matcher: { filter: "jr2", path: [{ key: "log" }], value: { bool_match: true } },
111
+ match_if_key_not_found: false,
112
+ },
113
+ },
114
+ typed_config: {
115
+ "@type": "type.googleapis.com/envoy.extensions.access_loggers.stream.v3.StdoutAccessLog",
116
+ log_format: {
117
+ text_format_source: {
118
+ inline_string:
119
+ `jr2.custodian action=%DYNAMIC_METADATA(jr2:action)% target=${target} ` +
120
+ "secret=%DYNAMIC_METADATA(jr2:secret)% reason=%DYNAMIC_METADATA(jr2:reason)% " +
121
+ "method=%REQ(:METHOD)% status=%RESPONSE_CODE%\n",
122
+ },
123
+ },
124
+ },
125
+ },
126
+ ];
127
+ }
128
+
129
+ /** One HTTP connection manager. Every one of them adds nothing to a request and nothing to a
130
+ * response: no `server: envoy`, no `x-request-id`, no `x-forwarded-*`, no `x-envoy-*` (§5.4). */
131
+ function hcm(
132
+ prefix: string,
133
+ opts: {
134
+ strictPath?: boolean;
135
+ target: string;
136
+ routes: Json;
137
+ idleSeconds: number;
138
+ /** How long a connection may hold no request, and take to send one's headers. */
139
+ connectionIdleSeconds?: number;
140
+ lua: boolean;
141
+ upgrade?: boolean;
142
+ local?: Json;
143
+ },
144
+ ): Json {
145
+ return {
146
+ name: "envoy.filters.network.http_connection_manager",
147
+ typed_config: {
148
+ "@type": HCM,
149
+ stat_prefix: prefix,
150
+ codec_type: "HTTP1",
151
+ server_header_transformation: "PASS_THROUGH",
152
+ generate_request_id: false,
153
+ skip_xff_append: true,
154
+ normalize_path: true,
155
+ merge_slashes: false,
156
+ // Toward a bound host an encoded `/` or `\` is refused (§5.4): a `paths` prefix must not be
157
+ // steppable-around. The control listener keeps it — an Instance ID is one encoded segment.
158
+ // `normalize_path` reads a bare `\` as `/` and `..;` as `..`, so the upstream gets the path
159
+ // the prefix was checked against, whatever IIS or Tomcat would read.
160
+ path_with_escaped_slashes_action: opts.strictPath ? "REJECT_REQUEST" : "KEEP_UNCHANGED",
161
+ request_headers_timeout: `${Math.min(60, opts.connectionIdleSeconds ?? 60)}s`,
162
+ stream_idle_timeout: `${opts.idleSeconds}s`,
163
+ // `x_api_key` is not `x-api-key` to the strip, but some servers read it as one.
164
+ common_http_protocol_options: {
165
+ idle_timeout: `${opts.connectionIdleSeconds ?? 3600}s`,
166
+ headers_with_underscores_action: "REJECT_REQUEST",
167
+ },
168
+ ...(opts.upgrade ? { upgrade_configs: [{ upgrade_type: "CONNECT" }] } : {}),
169
+ access_log: accessLog(opts.target),
170
+ ...(opts.local ? { local_reply_config: opts.local } : {}),
171
+ http_filters: [
172
+ ...(opts.lua
173
+ ? [
174
+ {
175
+ name: LUA_FILTER,
176
+ typed_config: {
177
+ "@type": "type.googleapis.com/envoy.extensions.filters.http.lua.v3.Lua",
178
+ default_source_code: { filename: `${CUSTODIAN_CONFIG}/${CUSTODIAN_SCRIPT_KEY}` },
179
+ // The route the request arrived on is the route it leaves by: the ask's rewrite
180
+ // to this pod's Sandbox must not be routed again (it would find no route). The
181
+ // one re-route the script wants — a normalized CONNECT target — it asks for.
182
+ clear_route_cache: false,
183
+ },
184
+ },
185
+ ]
186
+ : []),
187
+ {
188
+ name: "envoy.filters.http.router",
189
+ typed_config: {
190
+ "@type": "type.googleapis.com/envoy.extensions.filters.http.router.v3.Router",
191
+ suppress_envoy_headers: true,
192
+ },
193
+ },
194
+ ],
195
+ route_config: {
196
+ request_headers_to_remove: ["x-forwarded-proto", "x-forwarded-for", "x-request-id", "x-envoy-internal"],
197
+ response_headers_to_remove: ["x-envoy-upstream-service-time"],
198
+ ...opts.routes,
199
+ },
200
+ },
201
+ };
202
+ }
203
+
204
+ /** Route metadata the script reads its role and target from. */
205
+ function meta(fields: Record<string, string>): Json {
206
+ return { filter_metadata: { [LUA_FILTER]: fields } };
207
+ }
208
+
209
+ /** An upstream that never answered, said as the Custodian's own 502 (§5.4) — the host and Envoy's
210
+ * reason, never a retry without verification. */
211
+ function unreachable(target: string): Json {
212
+ return {
213
+ mappers: [
214
+ {
215
+ filter: { response_flag_filter: { flags: ["UF", "URX", "UH", "UC", "DF", "DPE", "UMSDR"] } },
216
+ status_code: 502,
217
+ body_format_override: {
218
+ text_format_source: {
219
+ inline_string: `jr2 custodian: ${target} could not be reached: %RESPONSE_CODE_DETAILS% %UPSTREAM_TRANSPORT_FAILURE_REASON%\n`,
220
+ },
221
+ },
222
+ },
223
+ ],
224
+ };
225
+ }
226
+
227
+ /** The Orchestrator unreachable, said so the Harness can tell it from an answer and retry the
228
+ * surface read the way ADR-0042 asks — the one route on this pod that retries. */
229
+ const ORCHESTRATOR_UNREACHABLE: Json = {
230
+ mappers: [
231
+ {
232
+ filter: { response_flag_filter: { flags: ["UF", "URX", "UH", "UC", "DF", "DPE", "UMSDR"] } },
233
+ status_code: 502,
234
+ headers_to_add: [
235
+ { header: { key: "x-jr2-custodian", value: "unreachable" }, append_action: "OVERWRITE_IF_EXISTS_OR_ADD" },
236
+ ],
237
+ body_format_override: {
238
+ json_format: {
239
+ error: "the Orchestrator never answered through the Custodian",
240
+ detail: "%RESPONSE_CODE_DETAILS% %UPSTREAM_TRANSPORT_FAILURE_REASON%",
241
+ flags: "%RESPONSE_FLAGS%",
242
+ },
243
+ },
244
+ },
245
+ ],
246
+ };
247
+
248
+ /**
249
+ * The addresses a tunnel never reaches (ADR-0059's dial guard): loopback, unspecified, link-local
250
+ * (the cloud metadata address among it, and AWS's IPv6 one), and the IPv4-mapped spelling of each.
251
+ * The script refuses a target that SPELLS one; this list refuses a name that RESOLVES to one.
252
+ */
253
+ export const GUARDED_RANGES: ReadonlyArray<readonly [string, number]> = [
254
+ ["127.0.0.0", 8],
255
+ ["0.0.0.0", 8],
256
+ ["169.254.0.0", 16],
257
+ ["::1", 128],
258
+ ["::", 128],
259
+ ["fe80::", 10],
260
+ ["fd00:ec2::254", 128],
261
+ ["::ffff:127.0.0.0", 104],
262
+ ["::ffff:0.0.0.0", 104],
263
+ ["::ffff:169.254.0.0", 112],
264
+ ];
265
+
266
+ /** The dial guard, by resolved address: an RBAC filter after the dynamic forward proxy, on the
267
+ * address that filter saved. A bound target never resolves here, so it never matches. */
268
+ function resolvedGuard(): Json {
269
+ return {
270
+ name: "envoy.filters.http.rbac",
271
+ typed_config: {
272
+ "@type": "type.googleapis.com/envoy.extensions.filters.http.rbac.v3.RBAC",
273
+ rules: {
274
+ action: "DENY",
275
+ policies: {
276
+ guard: {
277
+ permissions: GUARDED_RANGES.map(([address_prefix, prefix_len]) => ({
278
+ matcher: {
279
+ name: "envoy.rbac.matchers.upstream_ip_port",
280
+ typed_config: {
281
+ "@type":
282
+ "type.googleapis.com/envoy.extensions.rbac.matchers.upstream_ip_port.v3.UpstreamIpPortMatcher",
283
+ upstream_ip: { address_prefix, prefix_len },
284
+ },
285
+ },
286
+ })),
287
+ principals: [{ any: true }],
288
+ },
289
+ },
290
+ },
291
+ },
292
+ };
293
+ }
294
+
295
+ /** The egress listener's own replies. The guard's refusal by resolved address is the one 403 of
296
+ * a request the script let through as a tunnel, so it is said the way the script says a refusal by
297
+ * spelling; the script's own refusals carry their own bodies and are left alone. */
298
+ function egressReplies(): Json {
299
+ return {
300
+ mappers: [
301
+ {
302
+ filter: {
303
+ and_filter: {
304
+ filters: [
305
+ {
306
+ status_code_filter: {
307
+ comparison: { op: "EQ", value: { default_value: 403, runtime_key: "jr2.guard" } },
308
+ },
309
+ },
310
+ {
311
+ metadata_filter: {
312
+ matcher: { filter: "jr2", path: [{ key: "action" }], value: { string_match: { exact: "tunnel" } } },
313
+ },
314
+ },
315
+ ],
316
+ },
317
+ },
318
+ body_format_override: {
319
+ text_format_source: {
320
+ inline_string:
321
+ "jr2 custodian: %REQ(:AUTHORITY)% resolves to a loopback, link-local or unspecified address\n",
322
+ },
323
+ },
324
+ },
325
+ ...(unreachable("%REQ(:AUTHORITY)%").mappers as Json[]),
326
+ ],
327
+ };
328
+ }
329
+
330
+ /** Every bound target, in manifest order, with the secrets bound to it. */
331
+ function targetsOf(
332
+ manifest: HeldManifest,
333
+ ): Array<{ key: string; host: string; port: number; leaf: string; secrets: HeldBinding[] }> {
334
+ const byKey = new Map<string, { key: string; host: string; port: number; leaf: string; secrets: HeldBinding[] }>();
335
+ for (const secret of manifest.secrets) {
336
+ for (const h of secret.hosts) {
337
+ const key = `${authorityHost(h.host)}:${h.port}`;
338
+ const entry = byKey.get(key) ?? { key, host: h.host, port: h.port, leaf: h.leaf, secrets: [] };
339
+ entry.secrets.push(secret);
340
+ byKey.set(key, entry);
341
+ }
342
+ }
343
+ return [...byKey.values()];
344
+ }
345
+
346
+ /** A host as a CONNECT target spells it: an IPv6 literal in brackets. */
347
+ function authorityHost(host: string): string {
348
+ return host.includes(":") ? `[${host}]` : host;
349
+ }
350
+
351
+ function isIpLiteral(host: string): boolean {
352
+ return /^\d+\.\d+\.\d+\.\d+$/.test(host) || host.includes(":");
353
+ }
354
+
355
+ /**
356
+ * The Custodian's static bootstrap (ADR-0059): Envoy's whole configuration, rendered once per
357
+ * converge by `jr2 up`, identical for every Harness pod of the Instance.
358
+ *
359
+ * control 127.0.0.1:8081 the Menu and the ask, plain HTTP, to the Orchestrator's Service —
360
+ * three routes and nothing else, each swapping the Sandbox token's
361
+ * Stand-in for the token (ADR-0013, ADR-0053)
362
+ * health 0.0.0.0:15021 GET /healthz, for the kubelet, which cannot reach loopback
363
+ * egress 127.0.0.1:15001 CONNECT only — present when a held secret exists; a bound target
364
+ * goes to its intercept listener, every other one is tunneled
365
+ * intercept (internal) one per bound target: TLS ended with that target's leaf, the swap,
366
+ * then Envoy's own verified TLS to the target
367
+ */
368
+ export function custodianBootstrap(manifest: HeldManifest, orchestrator: OrchestratorAddress): Json {
369
+ const targets = targetsOf(manifest);
370
+ const orchestratorTarget = `${orchestrator.host}:${orchestrator.port}`;
371
+ const route = (name: string, method: string, regex: string, extra: Json = {}): Json => ({
372
+ name,
373
+ match: { safe_regex: { regex }, headers: [{ name: ":method", string_match: { exact: method } }] },
374
+ route: { cluster: "orchestrator", timeout: "0s", auto_host_rewrite: true, ...extra },
375
+ metadata: meta({ role: "control", route: name }),
376
+ });
377
+
378
+ const listeners: Json[] = [
379
+ {
380
+ name: "control",
381
+ address: { socket_address: { address: "127.0.0.1", port_value: CUSTODIAN_CONTROL_PORT } },
382
+ filter_chains: [
383
+ {
384
+ filters: [
385
+ hcm("control", {
386
+ target: orchestratorTarget,
387
+ idleSeconds: 3600,
388
+ lua: true,
389
+ local: ORCHESTRATOR_UNREACHABLE,
390
+ routes: {
391
+ virtual_hosts: [
392
+ {
393
+ name: "orchestrator",
394
+ domains: ["*"],
395
+ routes: [
396
+ // A read retries any reset. A pick or an ask retries only a reset before
397
+ // it was sent: one the Orchestrator may have read is not sent twice.
398
+ route("surface", "GET", "/agents/[^/]+/surface", {
399
+ retry_policy: { retry_on: "reset,connect-failure", num_retries: 2 },
400
+ }),
401
+ route("events", "POST", "/agents/[^/]+/events", {
402
+ retry_policy: { retry_on: "reset-before-request,connect-failure", num_retries: 2 },
403
+ }),
404
+ route("fetch", "POST", "/fetch", {
405
+ retry_policy: { retry_on: "reset-before-request,connect-failure", num_retries: 2 },
406
+ }),
407
+ {
408
+ name: "deny",
409
+ match: { prefix: "/" },
410
+ direct_response: { status: 404 },
411
+ metadata: meta({ role: "deny" }),
412
+ },
413
+ ],
414
+ },
415
+ ],
416
+ },
417
+ }),
418
+ ],
419
+ },
420
+ ],
421
+ },
422
+ {
423
+ name: "health",
424
+ address: { socket_address: { address: "0.0.0.0", port_value: CUSTODIAN_HEALTH_PORT } },
425
+ // A peer can reach this listener; the kubelet probes one request at a time. So a peer's idle
426
+ // connections cannot fill Envoy's global limit and stop the Menu and the egress: this listener
427
+ // is outside that limit, holds 16 connections at most, and closes one idle for 5 s.
428
+ ignore_global_conn_limit: true,
429
+ filter_chains: [
430
+ {
431
+ filters: [
432
+ {
433
+ name: "envoy.filters.network.connection_limit",
434
+ typed_config: {
435
+ "@type": "type.googleapis.com/envoy.extensions.filters.network.connection_limit.v3.ConnectionLimit",
436
+ stat_prefix: "health",
437
+ max_connections: 16,
438
+ },
439
+ },
440
+ hcm("health", {
441
+ target: "health",
442
+ idleSeconds: 5,
443
+ connectionIdleSeconds: 5,
444
+ lua: true,
445
+ routes: {
446
+ virtual_hosts: [
447
+ {
448
+ name: "health",
449
+ domains: ["*"],
450
+ routes: [
451
+ {
452
+ match: { path: "/healthz", headers: [{ name: ":method", string_match: { exact: "GET" } }] },
453
+ direct_response: { status: 404 },
454
+ metadata: meta({ role: "health" }),
455
+ },
456
+ { match: { prefix: "/" }, direct_response: { status: 404 } },
457
+ ],
458
+ },
459
+ ],
460
+ },
461
+ }),
462
+ ],
463
+ },
464
+ ],
465
+ },
466
+ ];
467
+
468
+ const clusters: Json[] = [
469
+ {
470
+ name: "orchestrator",
471
+ type: "LOGICAL_DNS",
472
+ dns_lookup_family: "V4_PREFERRED",
473
+ connect_timeout: "5s",
474
+ // Node closes a keep-alive connection idle for 5 s (the Orchestrator's default). A pooled
475
+ // connection is dropped first, so a request is not written onto one Node is closing.
476
+ typed_extension_protocol_options: {
477
+ "envoy.extensions.upstreams.http.v3.HttpProtocolOptions": {
478
+ "@type": "type.googleapis.com/envoy.extensions.upstreams.http.v3.HttpProtocolOptions",
479
+ common_http_protocol_options: { idle_timeout: "4s" },
480
+ explicit_http_config: { http_protocol_options: {} },
481
+ },
482
+ },
483
+ load_assignment: {
484
+ cluster_name: "orchestrator",
485
+ endpoints: [
486
+ {
487
+ lb_endpoints: [
488
+ {
489
+ endpoint: {
490
+ address: { socket_address: { address: orchestrator.host, port_value: orchestrator.port } },
491
+ },
492
+ },
493
+ ],
494
+ },
495
+ ],
496
+ },
497
+ },
498
+ ];
499
+
500
+ if (targets.length > 0) {
501
+ listeners.push({
502
+ name: "egress",
503
+ address: { socket_address: { address: "127.0.0.1", port_value: CUSTODIAN_PORT } },
504
+ filter_chains: [
505
+ {
506
+ filters: [
507
+ hcm("egress", {
508
+ target: "%REQ(:AUTHORITY)%",
509
+ idleSeconds: 3600,
510
+ lua: true,
511
+ upgrade: true,
512
+ local: egressReplies(),
513
+ routes: {
514
+ virtual_hosts: [
515
+ ...targets.map((t) => ({
516
+ name: `intercept-${t.leaf}`,
517
+ domains: [t.key],
518
+ routes: [
519
+ {
520
+ match: { connect_matcher: {} },
521
+ route: {
522
+ cluster: `intercept-${t.leaf}`,
523
+ timeout: "0s",
524
+ upgrade_configs: [{ upgrade_type: "CONNECT", connect_config: {} }],
525
+ },
526
+ metadata: meta({ role: "egress" }),
527
+ },
528
+ { match: { prefix: "/" }, direct_response: { status: 405 }, metadata: meta({ role: "egress" }) },
529
+ ],
530
+ })),
531
+ {
532
+ name: "tunnel",
533
+ domains: ["*"],
534
+ routes: [
535
+ {
536
+ match: { connect_matcher: {} },
537
+ route: {
538
+ cluster: "tunnel",
539
+ timeout: "0s",
540
+ upgrade_configs: [{ upgrade_type: "CONNECT", connect_config: {} }],
541
+ },
542
+ metadata: meta({ role: "egress" }),
543
+ },
544
+ { match: { prefix: "/" }, direct_response: { status: 405 }, metadata: meta({ role: "egress" }) },
545
+ ],
546
+ },
547
+ ],
548
+ },
549
+ }),
550
+ ],
551
+ },
552
+ ],
553
+ });
554
+ for (const t of targets) {
555
+ listeners.push({
556
+ name: `intercept-${t.leaf}`,
557
+ internal_listener: {},
558
+ filter_chains: [
559
+ {
560
+ transport_socket: {
561
+ name: "envoy.transport_sockets.tls",
562
+ typed_config: {
563
+ "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.DownstreamTlsContext",
564
+ common_tls_context: {
565
+ // HTTP/1.1 alone, in every branch: SSE rides it, and one downstream protocol
566
+ // keeps the behaviour the same whatever engine carries it.
567
+ alpn_protocols: ["http/1.1"],
568
+ tls_certificates: [
569
+ {
570
+ certificate_chain: { filename: `${CUSTODIAN_TLS}/${t.leaf}.crt` },
571
+ private_key: { filename: `${CUSTODIAN_TLS}/${t.leaf}.key` },
572
+ },
573
+ ],
574
+ },
575
+ },
576
+ },
577
+ filters: [
578
+ hcm(`intercept-${t.leaf}`, {
579
+ strictPath: true,
580
+ target: t.key,
581
+ idleSeconds: 3600,
582
+ lua: true,
583
+ local: unreachable(t.key),
584
+ routes: {
585
+ virtual_hosts: [
586
+ {
587
+ name: t.leaf,
588
+ domains: ["*"],
589
+ routes: [
590
+ {
591
+ match: { prefix: "/" },
592
+ route: { cluster: `upstream-${t.leaf}`, timeout: "0s" },
593
+ metadata: meta({ role: "intercept", target: t.key }),
594
+ },
595
+ ],
596
+ },
597
+ ],
598
+ },
599
+ }),
600
+ ],
601
+ },
602
+ ],
603
+ });
604
+ clusters.push(
605
+ {
606
+ name: `intercept-${t.leaf}`,
607
+ load_assignment: {
608
+ cluster_name: `intercept-${t.leaf}`,
609
+ endpoints: [
610
+ {
611
+ lb_endpoints: [
612
+ {
613
+ endpoint: { address: { envoy_internal_address: { server_listener_name: `intercept-${t.leaf}` } } },
614
+ },
615
+ ],
616
+ },
617
+ ],
618
+ },
619
+ },
620
+ {
621
+ name: `upstream-${t.leaf}`,
622
+ type: isIpLiteral(t.host) ? "STATIC" : "LOGICAL_DNS",
623
+ dns_lookup_family: "V4_PREFERRED",
624
+ connect_timeout: "30s",
625
+ load_assignment: {
626
+ cluster_name: `upstream-${t.leaf}`,
627
+ endpoints: [
628
+ {
629
+ lb_endpoints: [{ endpoint: { address: { socket_address: { address: t.host, port_value: t.port } } } }],
630
+ },
631
+ ],
632
+ },
633
+ transport_socket: {
634
+ name: "envoy.transport_sockets.tls",
635
+ typed_config: {
636
+ "@type": "type.googleapis.com/envoy.extensions.transport_sockets.tls.v3.UpstreamTlsContext",
637
+ ...(isIpLiteral(t.host) ? {} : { sni: t.host }),
638
+ common_tls_context: {
639
+ validation_context: {
640
+ // Roots + the user's caBundle, and NEVER the jr2 CA: the Custodian must not accept
641
+ // one of its own leaves from upstream (§4.4).
642
+ trusted_ca: { filename: `${CA_MOUNT}/upstream.crt` },
643
+ match_typed_subject_alt_names: [
644
+ { san_type: isIpLiteral(t.host) ? "IP_ADDRESS" : "DNS", matcher: { exact: t.host } },
645
+ ],
646
+ },
647
+ },
648
+ },
649
+ },
650
+ },
651
+ );
652
+ }
653
+ clusters.push({
654
+ name: "tunnel",
655
+ lb_policy: "CLUSTER_PROVIDED",
656
+ connect_timeout: "30s",
657
+ cluster_type: {
658
+ name: "envoy.clusters.dynamic_forward_proxy",
659
+ typed_config: {
660
+ "@type": "type.googleapis.com/envoy.extensions.clusters.dynamic_forward_proxy.v3.ClusterConfig",
661
+ dns_cache_config: { name: "tunnel", dns_lookup_family: "V4_PREFERRED" },
662
+ },
663
+ },
664
+ });
665
+ // The dynamic forward proxy filter resolves the CONNECT target for the tunnel cluster; it sits
666
+ // after the script so a refused target is never resolved at all. It saves the address it
667
+ // resolved, and the dial guard's second half judges THAT address, so a name that resolves to a
668
+ // guarded address is refused like the literal (ADR-0059).
669
+ const egress = listeners.find((l) => l.name === "egress") as {
670
+ filter_chains: Array<{ filters: Array<{ typed_config: { http_filters: Json[] } }> }>;
671
+ };
672
+ const filters = egress.filter_chains[0]!.filters[0]!.typed_config.http_filters;
673
+ filters.splice(
674
+ filters.length - 1,
675
+ 0,
676
+ {
677
+ name: "envoy.filters.http.dynamic_forward_proxy",
678
+ typed_config: {
679
+ "@type": "type.googleapis.com/envoy.extensions.filters.http.dynamic_forward_proxy.v3.FilterConfig",
680
+ dns_cache_config: { name: "tunnel", dns_lookup_family: "V4_PREFERRED" },
681
+ save_upstream_address: true,
682
+ },
683
+ },
684
+ resolvedGuard(),
685
+ );
686
+ }
687
+
688
+ return {
689
+ // No admin listener and no xDS: nothing in the pod can reconfigure this, or read it back.
690
+ node: { id: "jr2-custodian", cluster: "jr2-custodian" },
691
+ bootstrap_extensions: [
692
+ {
693
+ name: "envoy.bootstrap.internal_listener",
694
+ typed_config: {
695
+ "@type": "type.googleapis.com/envoy.extensions.bootstrap.internal_listener.v3.InternalListener",
696
+ },
697
+ },
698
+ ],
699
+ static_resources: { listeners, clusters },
700
+ // A single-writer sidecar: one worker, so the start-up line prints once.
701
+ overload_manager: {
702
+ resource_monitors: [
703
+ {
704
+ name: "envoy.resource_monitors.global_downstream_max_connections",
705
+ typed_config: {
706
+ "@type":
707
+ "type.googleapis.com/envoy.extensions.resource_monitors.downstream_connections.v3.DownstreamConnectionsConfig",
708
+ max_active_downstream_connections: 4096,
709
+ },
710
+ },
711
+ ],
712
+ },
713
+ };
714
+ }
715
+
716
+ // ---- the script ---------------------------------------------------------------------------------
717
+
718
+ /** `custodian.lua` beside this module — the fixed half of the script. */
719
+ const SCRIPT = readFileSync(new URL("./custodian.lua", import.meta.url), "utf8");
720
+
721
+ /** A Lua string literal. Every string that reaches here passed `heldSecretsOf`'s rules (names,
722
+ * header tokens, printable paths), so escaping the two characters Lua gives meaning to is enough. */
723
+ function lua(value: string): string {
724
+ return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
725
+ }
726
+
727
+ function luaList(values: readonly string[]): string {
728
+ return `{ ${values.map(lua).join(", ")} }`;
729
+ }
730
+
731
+ /**
732
+ * The script the Custodian runs: a generated `HELD` table in front of `custodian.lua`. The table
733
+ * names every secret this pod holds — the Sandbox token first, then the manifest's — with the
734
+ * headers and paths each may ride and the targets each is bound to. It never holds a value: the
735
+ * script reads those from the mounted files at start.
736
+ */
737
+ export function custodianScript(manifest: HeldManifest): string {
738
+ const lines = [
739
+ "-- Generated by `jr2 up` from held.json (ADR-0059). Names, headers, paths, targets; never a value.",
740
+ `HELD = { secrets = {}, targets = {}, hosts = ${targetsOf(manifest).length} }`,
741
+ `HELD.credential = ${luaList(CREDENTIAL_HEADERS)}`,
742
+ `HELD.secrets[1] = { name = ${lua(SANDBOX_TOKEN_NAME)}, headers = { "authorization" } }`,
743
+ "HELD.control = { HELD.secrets[1] }",
744
+ ];
745
+ manifest.secrets.forEach((s, i) => {
746
+ const paths = s.paths ? `, paths = ${luaList(s.paths)}` : "";
747
+ lines.push(`HELD.secrets[${i + 2}] = { name = ${lua(s.name)}, headers = ${luaList(s.headers)}${paths} }`);
748
+ });
749
+ for (const t of targetsOf(manifest)) {
750
+ const refs = t.secrets.map((s) => `HELD.secrets[${manifest.secrets.indexOf(s) + 2}]`);
751
+ lines.push(`HELD.targets[${lua(t.key)}] = { ${refs.join(", ")} }`);
752
+ }
753
+ return `${lines.join("\n")}\n\n${SCRIPT}`;
754
+ }
755
+
756
+ // ---- the composition ----------------------------------------------------------------------------
757
+
758
+ export type CustodianCompositionOptions = {
759
+ /** The Custodian's resolved ref (`custodianRef`, from the image map). */
760
+ image: string;
761
+ /** Where the Sandbox token lives: a Sandbox's `<name>-token` Secret, or the Instance Harness's key
762
+ * of `jr2-instance`. Mounted into the Custodian alone (ADR-0013). */
763
+ token: { secret: string; key: string };
764
+ /** This pod's Sandbox — the name the ask is addressed by (ADR-0053). Absent on the Instance
765
+ * Harness, which mounts no Repo and answers an ask by saying so. */
766
+ sandbox?: string;
767
+ /** The Instance ships a `harness.caBundle` (ADR-0020). */
768
+ caBundle: boolean;
769
+ };
770
+
771
+ type Container = Record<string, unknown>;
772
+ type Volume = Record<string, unknown>;
773
+ type VolumeMount = { name: string; mountPath: string; readOnly?: boolean };
774
+
775
+ /** What one Harness pod gains from the Custodian. */
776
+ export type CustodianComposition = {
777
+ /** The Harness container's env — in `env`, after `harness.env`, before the wire's gate (§5.1). In
778
+ * Kubernetes `env` wins over `envFrom`, so a Stand-in overrides a same-named key of any `envFrom`
779
+ * Secret, even one that changes after the converge. */
780
+ harnessEnv: HarnessEnvVar[];
781
+ harnessMounts: VolumeMount[];
782
+ custodianContainer: Container;
783
+ volumes: Volume[];
784
+ };
785
+
786
+ /** The isolation baseline, stated by the Custodian itself rather than left to the operator's
787
+ * default: the image is not jr2's, and a read-only root is a claim only the seat can make. */
788
+ const CUSTODIAN_SECURITY = {
789
+ runAsNonRoot: true,
790
+ readOnlyRootFilesystem: true,
791
+ allowPrivilegeEscalation: false,
792
+ capabilities: { drop: ["ALL"] },
793
+ seccompProfile: { type: "RuntimeDefault" },
794
+ };
795
+
796
+ /**
797
+ * The Custodian in one Harness pod, and what the Harness container gets from it (ADR-0059). Called
798
+ * by BOTH placements, so a Sandbox and the Instance Harness cannot differ in what they hold.
799
+ *
800
+ * The Custodian is ALWAYS here: it carries the Menu and the ask, which every Harness pod needs
801
+ * (ADR-0013). The egress half — `HTTPS_PROXY`, the trust bundles, the leaves — joins it only when
802
+ * the manifest holds a secret, and with none the Harness's HTTPS goes direct, as it always did.
803
+ */
804
+ export function custodianComposition(manifest: HeldManifest, opts: CustodianCompositionOptions): CustodianComposition {
805
+ const held = manifest.secrets.length > 0;
806
+ const literal = manifest.secrets.filter((s) => s.source.kind === "literal").map((s) => s.name);
807
+
808
+ const harnessEnv: HarnessEnvVar[] = [
809
+ { name: "JR2_CUSTODIAN_URL", value: CUSTODIAN_URL },
810
+ { name: SANDBOX_TOKEN_NAME, value: standIn(SANDBOX_TOKEN_NAME) },
811
+ ];
812
+ if (held) {
813
+ const proxy = `http://127.0.0.1:${CUSTODIAN_PORT}`;
814
+ const noProxy = "localhost,127.0.0.1,::1";
815
+ harnessEnv.push(
816
+ ...manifest.secrets.map((s) => ({ name: s.name, value: standIn(s.name) })),
817
+ { name: "HTTPS_PROXY", value: proxy },
818
+ { name: "https_proxy", value: proxy },
819
+ { name: "NO_PROXY", value: noProxy },
820
+ { name: "no_proxy", value: noProxy },
821
+ { name: "NODE_USE_ENV_PROXY", value: "1" },
822
+ { name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/extra.crt` },
823
+ { name: "SSL_CERT_FILE", value: `${CA_MOUNT}/bundle.crt` },
824
+ { name: "REQUESTS_CA_BUNDLE", value: `${CA_MOUNT}/bundle.crt` },
825
+ { name: "GIT_SSL_CAINFO", value: `${CA_MOUNT}/bundle.crt` },
826
+ );
827
+ } else if (opts.caBundle) {
828
+ harnessEnv.push({ name: "NODE_EXTRA_CA_CERTS", value: `${CA_MOUNT}/ca.crt` });
829
+ }
830
+
831
+ const trust = held || opts.caBundle;
832
+ const volumes: Volume[] = [
833
+ {
834
+ name: "custodian-values",
835
+ projected: {
836
+ // Group-readable, for the pod's fsGroup: the Custodian runs as Envoy's own uid.
837
+ defaultMode: 0o440,
838
+ sources: [
839
+ { secret: { name: opts.token.secret, items: [{ key: opts.token.key, path: SANDBOX_TOKEN_NAME }] } },
840
+ ...(literal.length
841
+ ? [{ secret: { name: HELD_SECRETS_SECRET, items: literal.map((n) => ({ key: n, path: n })) } }]
842
+ : []),
843
+ ...manifest.secrets.flatMap((s) =>
844
+ s.source.kind === "secret"
845
+ ? [{ secret: { name: s.source.secret, items: [{ key: s.source.key, path: s.name }] } }]
846
+ : [],
847
+ ),
848
+ ],
849
+ },
850
+ },
851
+ { name: "custodian-config", configMap: { name: HELD_CONFIGMAP } },
852
+ ...(held
853
+ ? [
854
+ { name: "custodian-tls", secret: { secretName: HELD_TLS_SECRET, defaultMode: 0o440 } },
855
+ {
856
+ name: "custodian-trust",
857
+ configMap: { name: CA_CONFIGMAP, items: [{ key: "upstream.crt", path: "upstream.crt" }] },
858
+ },
859
+ ]
860
+ : []),
861
+ ...(trust ? [{ name: "ca", configMap: { name: CA_CONFIGMAP } }] : []),
862
+ ];
863
+
864
+ const custodianContainer: Container = {
865
+ name: "custodian",
866
+ image: opts.image,
867
+ imagePullPolicy: "IfNotPresent",
868
+ args: CUSTODIAN_ARGS,
869
+ // Non-secret settings only: never a value in env.
870
+ ...(opts.sandbox !== undefined ? { env: [{ name: "JR2_SANDBOX", value: opts.sandbox }] } : {}),
871
+ volumeMounts: [
872
+ { name: "custodian-values", mountPath: CUSTODIAN_VALUES, readOnly: true },
873
+ { name: "custodian-config", mountPath: CUSTODIAN_CONFIG, readOnly: true },
874
+ ...(held
875
+ ? [
876
+ { name: "custodian-tls", mountPath: CUSTODIAN_TLS, readOnly: true },
877
+ { name: "custodian-trust", mountPath: CA_MOUNT, readOnly: true },
878
+ ]
879
+ : []),
880
+ ],
881
+ // The kubelet probes the pod IP and cannot reach a loopback listener, and a distroless image
882
+ // has no shell for an exec probe — so the health listener exists. Pod Ready needs it, and the
883
+ // operator holds the Sandbox's Ready on pod Ready: no Turn is admitted before this serves.
884
+ readinessProbe: {
885
+ httpGet: { path: "/healthz", port: CUSTODIAN_HEALTH_PORT },
886
+ initialDelaySeconds: 1,
887
+ periodSeconds: 2,
888
+ failureThreshold: 15,
889
+ },
890
+ securityContext: CUSTODIAN_SECURITY,
891
+ };
892
+
893
+ return {
894
+ harnessEnv,
895
+ harnessMounts: trust ? [{ name: "ca", mountPath: CA_MOUNT, readOnly: true }] : [],
896
+ custodianContainer,
897
+ volumes,
898
+ };
899
+ }