Skip to content

Commit fee21c3

Browse files
nicktrnTrigger.dev RepoOps
authored andcommitted
fix(cli): point python at the OS trust store in deploy images
Mono-RevId: f8534eae039f05e66719e51b5b42b3ea26bbcc80
1 parent 1fb4c76 commit fee21c3

8 files changed

Lines changed: 155 additions & 6 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
`NODE_EXTRA_CA_CERTS` set as an environment variable now reaches the task process at startup instead of being overwritten by the image's empty default.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"trigger.dev": patch
3+
---
4+
5+
Python tasks and `pip` in deploy images now verify TLS against the image's trust store instead of a pinned certifi bundle, so they also refuse the compromised eMudhra intermediates.

‎packages/cli-v3/src/deploy/buildImage.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,50 @@ describe("generateContainerfile", () => {
158158
);
159159
});
160160

161+
const osTrustStoreEnv =
162+
/^ENV SSL_CERT_FILE=\$\{SSL_CERT_FILE:-\/etc\/ssl\/certs\/ca-certificates\.crt\} REQUESTS_CA_BUNDLE=\$\{REQUESTS_CA_BUNDLE:-\/etc\/ssl\/certs\/ca-certificates\.crt\} PIP_CERT=\$\{PIP_CERT:-\/etc\/ssl\/certs\/ca-certificates\.crt\}$/gm;
163+
164+
it("keeps a trust bundle set by image instructions", async () => {
165+
const containerfile = await generateContainerfile({
166+
runtime: "node",
167+
build: {},
168+
image: { instructions: ["ENV REQUESTS_CA_BUNDLE=/app/certs/corp.pem"] },
169+
indexScript: "index.js",
170+
entrypoint: "entrypoint.js",
171+
});
172+
173+
const instruction = containerfile.indexOf("ENV REQUESTS_CA_BUNDLE=/app/certs/corp.pem");
174+
expect(instruction).toBeGreaterThan(-1);
175+
expect(instruction).toBeLessThan(containerfile.search(osTrustStoreEnv));
176+
});
177+
178+
it.each(["node", "bun"] as BuildRuntime[])(
179+
"points certifi users at the OS trust store in the base and prebuilt build stages on %s",
180+
async (runtime) => {
181+
const containerfile = await generateContainerfile({
182+
runtime,
183+
build: {},
184+
image: undefined,
185+
indexScript: "index.js",
186+
entrypoint: "entrypoint.js",
187+
});
188+
189+
expect(containerfile.match(osTrustStoreEnv)).toHaveLength(2);
190+
}
191+
);
192+
193+
it("points certifi users at the OS trust store once when the build stage derives from base", async () => {
194+
const containerfile = await generateContainerfile({
195+
runtime: "node",
196+
build: {},
197+
image: { instructions: ["RUN echo hi"] },
198+
indexScript: "index.js",
199+
entrypoint: "entrypoint.js",
200+
});
201+
202+
expect(containerfile.match(osTrustStoreEnv)).toHaveLength(1);
203+
});
204+
161205
const rejectSteps = (containerfile: string) =>
162206
Array.from(
163207
containerfile.matchAll(

‎packages/cli-v3/src/deploy/buildImage.ts‎

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,8 @@ export const EMSIGN_COMPROMISE_DISTRUSTED_ROOTS = [
743743

744744
export const EMSIGN_REJECT_DIR = "/usr/local/share/ca-certificates/emsign-block";
745745

746+
const OS_TRUST_STORE = "/etc/ssl/certs/ca-certificates.crt";
747+
746748
const rejectCompromisedIntermediates = `RUN mkdir -p ${EMSIGN_REJECT_DIR} && \\
747749
${EMSIGN_COMPROMISE_REJECTED_INTERMEDIATES.map(
748750
({ file, pemLines }) =>
@@ -756,7 +758,8 @@ RUN sed -i -E 's#^(mozilla/(${EMSIGN_COMPROMISE_DISTRUSTED_ROOTS.join(
756758
"|"
757759
)})\\.crt)$#!\\1#' /etc/ca-certificates.conf && \\
758760
update-ca-certificates --fresh
759-
ENV NODE_OPTIONS=--use-openssl-ca`;
761+
ENV NODE_OPTIONS=--use-openssl-ca
762+
ENV SSL_CERT_FILE=\${SSL_CERT_FILE:-${OS_TRUST_STORE}} REQUESTS_CA_BUNDLE=\${REQUESTS_CA_BUNDLE:-${OS_TRUST_STORE}} PIP_CERT=\${PIP_CERT:-${OS_TRUST_STORE}}`;
760763

761764
export async function generateContainerfile(options: GenerateContainerfileOptions) {
762765
switch (options.runtime) {
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, it, expect } from "vitest";
2+
import { RunnerEnv } from "./env.js";
3+
4+
const requiredEnv = {
5+
TRIGGER_CONTENT_HASH: "hash",
6+
TRIGGER_PROJECT_ID: "proj_1",
7+
TRIGGER_PROJECT_REF: "proj_ref",
8+
TRIGGER_DEPLOYMENT_ID: "deploy_1",
9+
TRIGGER_DEPLOYMENT_VERSION: "20261020.1",
10+
TRIGGER_ENV_ID: "env_1",
11+
OTEL_EXPORTER_OTLP_ENDPOINT: "https://example.com/otel",
12+
TRIGGER_RUNNER_ID: "runner_1",
13+
TRIGGER_POD_SCHEDULED_AT_MS: "1",
14+
TRIGGER_DEQUEUED_AT_MS: "1",
15+
TRIGGER_SUPERVISOR_API_PROTOCOL: "http",
16+
TRIGGER_SUPERVISOR_API_DOMAIN: "localhost",
17+
TRIGGER_SUPERVISOR_API_PORT: "8000",
18+
TRIGGER_WORKER_INSTANCE_NAME: "worker",
19+
};
20+
21+
describe("RunnerEnv.gatherProcessEnv", () => {
22+
it.each(["", "/app/certs/corp-root.pem"])(
23+
"leaves NODE_EXTRA_CA_CERTS=%j to the task run env",
24+
(value) => {
25+
const env = new RunnerEnv({ ...requiredEnv, NODE_EXTRA_CA_CERTS: value });
26+
27+
expect(env.gatherProcessEnv()).not.toHaveProperty("NODE_EXTRA_CA_CERTS");
28+
}
29+
);
30+
});

‎packages/cli-v3/src/entryPoints/managed/env.ts‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,9 +86,6 @@ export class RunnerEnv {
8686
get NODE_ENV() {
8787
return this.env.NODE_ENV;
8888
}
89-
get NODE_EXTRA_CA_CERTS() {
90-
return this.env.NODE_EXTRA_CA_CERTS;
91-
}
9289
get UV_USE_IO_URING() {
9390
return this.env.UV_USE_IO_URING;
9491
}
@@ -243,7 +240,6 @@ export class RunnerEnv {
243240
gatherProcessEnv(): Record<string, string> {
244241
const $env = {
245242
NODE_ENV: this.NODE_ENV,
246-
NODE_EXTRA_CA_CERTS: this.NODE_EXTRA_CA_CERTS,
247243
OTEL_EXPORTER_OTLP_ENDPOINT: this.OTEL_EXPORTER_OTLP_ENDPOINT,
248244
TRIGGER_OTEL_EXPORTER_OTLP_ENDPOINT: this.OTEL_EXPORTER_OTLP_ENDPOINT,
249245
UV_USE_IO_URING: this.UV_USE_IO_URING,

‎packages/cli-v3/src/executions/taskRunProcess.test.ts‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { TaskRunProcess, type TaskRunProcessOptions } from "./taskRunProcess.js";
1+
import { TaskRunProcess, trustStoreEnv, type TaskRunProcessOptions } from "./taskRunProcess.js";
22
import { describe, it, expect, vi } from "vitest";
33
import { UncaughtExceptionError, UnexpectedExitError } from "@trigger.dev/core/v3/errors";
44
import type {
@@ -58,6 +58,50 @@ function createExecution(runId: string, attemptNumber: number): TaskRunExecution
5858
} as unknown as TaskRunExecution;
5959
}
6060

61+
describe("trustStoreEnv", () => {
62+
const controllerEnv = {
63+
SSL_CERT_FILE: "/etc/ssl/certs/ca-certificates.crt",
64+
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt",
65+
PIP_CERT: "/etc/ssl/certs/ca-certificates.crt",
66+
PATH: "/usr/bin",
67+
};
68+
69+
it("forwards the controller's trust store variables to the task process", () => {
70+
expect(trustStoreEnv(controllerEnv, {})).toEqual({
71+
SSL_CERT_FILE: "/etc/ssl/certs/ca-certificates.crt",
72+
REQUESTS_CA_BUNDLE: "/etc/ssl/certs/ca-certificates.crt",
73+
PIP_CERT: "/etc/ssl/certs/ca-certificates.crt",
74+
});
75+
});
76+
77+
it("lets the run's own variables win", () => {
78+
expect(trustStoreEnv(controllerEnv, { REQUESTS_CA_BUNDLE: "/app/corp.pem" })).toEqual({
79+
SSL_CERT_FILE: "/etc/ssl/certs/ca-certificates.crt",
80+
REQUESTS_CA_BUNDLE: "/app/corp.pem",
81+
PIP_CERT: "/etc/ssl/certs/ca-certificates.crt",
82+
});
83+
});
84+
85+
it("sets nothing when neither side defines them", () => {
86+
expect(trustStoreEnv({ PATH: "/usr/bin" }, {})).toEqual({});
87+
});
88+
89+
it("lets the run's NODE_EXTRA_CA_CERTS win over the image's empty value", () => {
90+
expect(
91+
trustStoreEnv(
92+
{ NODE_EXTRA_CA_CERTS: "" },
93+
{ NODE_EXTRA_CA_CERTS: "/app/certs/corp-root.pem" }
94+
)
95+
).toEqual({ NODE_EXTRA_CA_CERTS: "/app/certs/corp-root.pem" });
96+
});
97+
98+
it("forwards NODE_EXTRA_CA_CERTS baked into the image when the run sets none", () => {
99+
expect(trustStoreEnv({ NODE_EXTRA_CA_CERTS: "/app/certs/corp-root.pem" }, {})).toEqual({
100+
NODE_EXTRA_CA_CERTS: "/app/certs/corp-root.pem",
101+
});
102+
});
103+
});
104+
61105
describe("TaskRunProcess", () => {
62106
describe("execute() on a dead child process", () => {
63107
it("should reject when child process has already exited and IPC send is skipped", async () => {

‎packages/cli-v3/src/executions/taskRunProcess.ts‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,7 @@ export class TaskRunProcess {
141141

142142
const fullEnv = {
143143
...$env,
144+
...trustStoreEnv(process.env, $env),
144145
OTEL_IMPORT_HOOK_INCLUDES: workerManifest.otelImportHook?.include?.join(","),
145146
// TODO: this will probably need to use something different for bun (maybe --preload?)
146147
NODE_OPTIONS: execOptionsForRuntime(workerManifest.runtime, workerManifest, maxOldSpaceSize),
@@ -606,6 +607,27 @@ export class TaskRunProcess {
606607
}
607608
}
608609

610+
const TRUST_STORE_ENV_KEYS = [
611+
"NODE_EXTRA_CA_CERTS",
612+
"SSL_CERT_FILE",
613+
"REQUESTS_CA_BUNDLE",
614+
"PIP_CERT",
615+
] as const;
616+
617+
export function trustStoreEnv(
618+
controllerEnv: NodeJS.ProcessEnv,
619+
runEnv: Record<string, string | undefined>
620+
): Record<string, string> {
621+
const env: Record<string, string> = {};
622+
for (const key of TRUST_STORE_ENV_KEYS) {
623+
const value = runEnv[key] ?? controllerEnv[key];
624+
if (value) {
625+
env[key] = value;
626+
}
627+
}
628+
return env;
629+
}
630+
609631
function executorArgs(workerManifest: WorkerManifest): string[] {
610632
return [];
611633
}

0 commit comments

Comments
 (0)