@jr2/orchestrator 0.2.1 → 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.
- package/package.json +3 -4
- package/src/actor.ts +5 -5
- package/src/agent.ts +2 -2
- package/src/config.ts +119 -14
- package/src/custodian.lua +312 -0
- package/src/custodian.ts +899 -0
- package/src/harness-client.ts +2 -2
- package/src/held-secrets.ts +587 -0
- package/src/http.ts +19 -16
- package/src/images.ts +11 -10
- package/src/index.ts +6 -2
- package/src/instance.ts +1 -1
- package/src/model-keys.ts +92 -0
- package/src/names.ts +30 -6
- package/src/registration.ts +1 -1
- package/src/repo-fetch.ts +1 -1
- package/src/run-host.ts +15 -13
- package/src/sandbox-kubectl.ts +71 -107
- package/src/server.ts +4 -5
- package/src/tokens.ts +9 -8
- package/src/workspace.ts +1 -1
package/src/custodian.ts
ADDED
|
@@ -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
|
+
}
|