Skip to content

Commit b5ed66b

Browse files
nicktrnTrigger.dev RepoOps
authored andcommitted
fix(cli): keep custom base images' own trust store
Fixes `trigger deploy` failing on self-hosted instances that set custom base images with `DEPLOY_BASE_IMAGES` or `DEPLOY_BUILD_BASE_IMAGES` (#5054). The deploy step that distrusts compromised emSign certificate authorities assumed the published root Debian images, so it broke on non-root or non-Debian bases such as Chainguard's. Stages built on a custom image now skip that step and keep the certificate settings the image ships with. Stages on the published images are unchanged. The self-hosting docs now state that a custom base image owns its own security hardening. Mono-RevId: 5de0cdda76e9b57d7b076d52c6ab8c763934e2d3
1 parent a7f0f90 commit b5ed66b

4 files changed

Lines changed: 107 additions & 10 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+
Fix `trigger deploy` failing on self-hosted instances with custom base images. Stages built on a custom image now keep the trust store that image ships with.

‎docs/self-hosting/overview.mdx‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,8 @@ You own a custom base image. It must provide:
121121

122122
A `DEPLOY_BUILD_BASE_IMAGES` image needs everything the base image provides, plus `python3`, `make` and `g++`. Build extensions that add image instructions are replayed on it. Without an entry, the build stage uses the published build image, except for projects whose build extensions add image instructions: those build from your base image and install the toolchain with `apt-get`, so that base must be Debian-based. To avoid the published images entirely, set both variables.
123123

124+
A custom image is yours to harden. Deploys on the published images remove trust in publicly compromised certificate authorities and point Node.js and Python at the OS trust store. Stages built on a custom image get neither, and run with the certificate settings the image ships with. Keeping a custom image patched, trust store included, is your responsibility.
125+
124126
<Warning>
125127
`image.pkgs` and build extensions that run `apt-get` (such as `aptGet` and `playwright`) assume a
126128
Debian base. On other distributions, install those packages in your base image instead.

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

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -492,4 +492,78 @@ describe("generateContainerfile", () => {
492492
expect(containerfile).toContain("FROM base AS build");
493493
expect(containerfile).toContain(TOOLCHAIN_PACKAGES);
494494
});
495+
496+
it.each(["node", "bun"] as BuildRuntime[])(
497+
"leaves the trust store of configured base and build images alone on %s",
498+
async (runtime) => {
499+
const containerfile = await generateContainerfile({
500+
runtime,
501+
build: {},
502+
image: {
503+
base: "acme/node-fips:26@sha256:abc",
504+
buildBase: "acme/node:26-dev@sha256:def",
505+
},
506+
indexScript: "index.js",
507+
entrypoint: "entrypoint.js",
508+
});
509+
510+
expect(distrustSteps(containerfile)).toHaveLength(0);
511+
expect(rejectSteps(containerfile)).toHaveLength(0);
512+
expect(containerfile).not.toContain(EMSIGN_REJECT_DIR);
513+
expect(containerfile).not.toContain("--use-openssl-ca");
514+
expect(containerfile).not.toMatch(osTrustStoreEnv);
515+
}
516+
);
517+
518+
it("hardens only the published build image when just the base is configured", async () => {
519+
const containerfile = await generateContainerfile({
520+
runtime: "node-26",
521+
build: {},
522+
image: { base: "acme/node-fips:26@sha256:abc" },
523+
indexScript: "index.js",
524+
entrypoint: "entrypoint.js",
525+
});
526+
527+
const buildStart = containerfile.indexOf(`FROM ${BUILD_IMAGE["node-26"]} AS build`);
528+
const steps = Array.from(containerfile.matchAll(/^ {2}update-ca-certificates --fresh$/gm));
529+
530+
expect(steps).toHaveLength(1);
531+
expect(steps[0]!.index).toBeGreaterThan(buildStart);
532+
expect(containerfile).toContain('NODE_OPTIONS="--max_old_space_size=8192 --use-openssl-ca"');
533+
});
534+
535+
it("hardens only the published base image when just the build image is configured", async () => {
536+
const containerfile = await generateContainerfile({
537+
runtime: "node-26",
538+
build: {},
539+
image: { buildBase: "acme/node:26-dev@sha256:def" },
540+
indexScript: "index.js",
541+
entrypoint: "entrypoint.js",
542+
});
543+
544+
const buildStart = containerfile.indexOf("FROM acme/node:26-dev@sha256:def AS build");
545+
const steps = Array.from(containerfile.matchAll(/^ {2}update-ca-certificates --fresh$/gm));
546+
547+
expect(steps).toHaveLength(1);
548+
expect(steps[0]!.index).toBeLessThan(buildStart);
549+
expect(containerfile).toContain('NODE_OPTIONS="--max_old_space_size=8192"');
550+
});
551+
552+
it("leaves the trust store alone when the build stage derives from a configured base", async () => {
553+
const containerfile = await generateContainerfile({
554+
runtime: "node-26",
555+
build: {},
556+
image: {
557+
base: "acme/node-fips:26@sha256:abc",
558+
instructions: ["RUN echo custom > /etc/marker"],
559+
},
560+
indexScript: "index.js",
561+
entrypoint: "entrypoint.js",
562+
});
563+
564+
expect(containerfile).toContain("FROM base AS build");
565+
expect(distrustSteps(containerfile)).toHaveLength(0);
566+
expect(rejectSteps(containerfile)).toHaveLength(0);
567+
expect(containerfile).not.toContain("--use-openssl-ca");
568+
});
495569
});

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -761,6 +761,10 @@ RUN sed -i -E 's#^(mozilla/(${EMSIGN_COMPROMISE_DISTRUSTED_ROOTS.join(
761761
ENV NODE_OPTIONS=--use-openssl-ca
762762
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}}`;
763763

764+
// A custom base image owns its own hardening, trust store included
765+
const trustStoreHardening = (customImage: string | undefined) =>
766+
customImage ? "" : distrustCompromisedRoots;
767+
764768
export async function generateContainerfile(options: GenerateContainerfileOptions) {
765769
switch (options.runtime) {
766770
case "node":
@@ -829,23 +833,28 @@ const parseGenerateOptions = (options: GenerateContainerfileOptions) => {
829833

830834
// Instructions run once (FROM base) since their downloads are unbounded;
831835
// package-only projects keep the prebuilt toolchain and repeat the small install
832-
const buildStage =
833-
baseInstructions && !options.image?.buildBase
834-
? `FROM base AS build
836+
const buildFromBase = Boolean(baseInstructions) && !options.image?.buildBase;
837+
const buildStageCustomImage = buildFromBase ? options.image?.base : options.image?.buildBase;
838+
839+
const buildStage = buildFromBase
840+
? `FROM base AS build
835841
836842
RUN apt-get update && \\
837843
apt-get install -y --no-install-recommends ${TOOLCHAIN_PACKAGES} && \\
838844
apt-get clean && \\
839845
rm -rf /var/lib/apt/lists/*`
840-
: `FROM ${options.image?.buildBase ?? BUILD_IMAGE[options.runtime]} AS build
846+
: `FROM ${options.image?.buildBase ?? BUILD_IMAGE[options.runtime]} AS build
841847
842848
ENV DEBIAN_FRONTEND=noninteractive${customization ? `\n\n${customization}` : ""}
843849
844-
${distrustCompromisedRoots}`;
850+
${trustStoreHardening(options.image?.buildBase)}`;
845851

846852
return {
847853
baseImage: options.image?.base ?? BASE_IMAGE[options.runtime],
848854
buildStage,
855+
indexerNodeOptions: buildStageCustomImage
856+
? "--max_old_space_size=8192"
857+
: "--max_old_space_size=8192 --use-openssl-ca",
849858
customization,
850859
buildArgs,
851860
buildEnvVars,
@@ -865,7 +874,7 @@ ENV DEBIAN_FRONTEND=noninteractive
865874
866875
${customization}
867876
868-
${distrustCompromisedRoots}
877+
${trustStoreHardening(options.image?.base)}
869878
870879
${buildStage}
871880
@@ -957,8 +966,15 @@ CMD []
957966
}
958967

959968
async function generateNodeContainerfile(options: GenerateContainerfileOptions) {
960-
const { baseImage, buildStage, buildArgs, buildEnvVars, postInstallCommands, customization } =
961-
parseGenerateOptions(options);
969+
const {
970+
baseImage,
971+
buildStage,
972+
indexerNodeOptions,
973+
buildArgs,
974+
buildEnvVars,
975+
postInstallCommands,
976+
customization,
977+
} = parseGenerateOptions(options);
962978

963979
return `# syntax=docker/dockerfile:1
964980
# check=skip=SecretsUsedInArgOrEnv
@@ -968,7 +984,7 @@ ENV DEBIAN_FRONTEND=noninteractive
968984
969985
${customization}
970986
971-
${distrustCompromisedRoots}
987+
${trustStoreHardening(options.image?.base)}
972988
973989
${buildStage}
974990
@@ -1027,7 +1043,7 @@ ENV TRIGGER_PROJECT_ID=\${TRIGGER_PROJECT_ID} \
10271043
TRIGGER_LOG_LEVEL=debug \
10281044
NODE_EXTRA_CA_CERTS=\${NODE_EXTRA_CA_CERTS} \
10291045
NODE_ENV=production \
1030-
NODE_OPTIONS="--max_old_space_size=8192 --use-openssl-ca"
1046+
NODE_OPTIONS="${indexerNodeOptions}"
10311047
10321048
ARG TARGETPLATFORM
10331049
ARG BUILDPLATFORM

0 commit comments

Comments
 (0)