Merge PR #1837: Pin the worker to the patched immutable Node24 base (#470)

This commit is contained in:
jason.woltje committed 2026-11-02 12:31:19 -06:00
commit a7a433e271
15 files changed
+259 -12

No files matched your search

+9 -2
View File
@@ -60,8 +60,15 @@ scripts/testing/gate-lane.sh prepare-image --lane a --candidate FULL_SHA \
Preparation holds both lanes and the exclusive image lock. Its build log,
receipt, count and wall time are separate from warm gate timings. The current
Containerfile uses a mutable FROM tag and apt indexes. A missing base digest
refuses provenance publication. No credential file is copied or printed.
Containerfile uses one literal digest-pinned FROM reference; apt indexes remain
mutable. Preparation records that exact declared reference as `baseDigest`, and
verification refuses a different receipt reference. It never borrows the digest
of a cached mutable tag. Mutable, indirect and multistage FROM inputs refuse
before build dispatch. This binds source/build inputs, not an independently
measured base binary or a layer-level reconstruction. Admitted image validation
must still check the built image's Node version (`v24.22.2`) and provenance.
Retained registry config metadata and its limits are in
`docs/security/q284-node-base/`. No credential file is copied or printed.
Run records are immutable JSON files published by link after fsync. Every step
has a log, UTC/monotonic times, exit code, pass/fail/skip counts and log hash.
+11 -2
View File
@@ -3,6 +3,14 @@ import { readFileSync, readdirSync, lstatSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { execFileSync } from 'node:child_process';
import { refuse } from './lane-state.mjs';
// This is the declared build input, not an observation of a running binary.
function pinnedBase(checkout){
const lines=readFileSync(join(checkout,'Containerfile'),'utf8').split('\n').filter(line=>/^\s*FROM(?:\s|$)/i.test(line));
if(lines.length!==1)refuse('image requires exactly one pinned FROM');
const fields=lines[0].trim().split(/\s+/);
if(fields.length!==2||!/^[A-Za-z0-9][A-Za-z0-9._:/-]*@sha256:[a-f0-9]{64}$/.test(fields[1]))refuse('image requires one literal digest-pinned FROM');
return fields[1];
}
export function imageFingerprint(checkout){
const dockerfile=readFileSync(join(checkout,'Containerfile'),'utf8');
const copies=dockerfile.split('\n').filter(l=>/^\s*(COPY|ADD)\s/i.test(l)).map(l=>l.trim());
@@ -18,6 +26,7 @@ export function inspectImage(tag,docker=process.env.MOSAIC_GATE_REAL_DOCKER??'do
}
export function verifyImage(receipt,checkout,inspect=inspectImage){
if(!receipt||!/^sha256:[a-f0-9]{64}$/.test(receipt.imageId??'')||!/^.+@sha256:[a-f0-9]{64}$/.test(receipt.baseDigest??'')||!/^linux\/[a-z0-9_]+$/.test(receipt.platform??'')||!/^mosaic-poc-agent:[A-Za-z0-9_.-]+$/.test(receipt.tag??''))refuse('image provenance receipt invalid');
if(receipt.baseDigest!==pinnedBase(checkout))refuse('image base differs from pinned Containerfile');
if(receipt.inputDigest!==imageFingerprint(checkout))refuse('image inputs differ');
const actual=inspect(receipt.tag);
if(actual.Id!==receipt.imageId)refuse('pinned image ID differs');
@@ -29,6 +38,7 @@ export function verifyImage(receipt,checkout,inspect=inspectImage){
// Called by the CLI only while both lanes and the image lock are exclusive.
export function prepareImage(checkout,{grant,tempRoot,out},invoke){
if(!grant)refuse('missing image preparation grant');
const baseDigest=pinnedBase(checkout);
const inputDigest=imageFingerprint(checkout);
const pkg=JSON.parse(readFileSync(join(checkout,'package.json'),'utf8'));
const release=readFileSync(join(checkout,'RELEASE'),'utf8').trim();
@@ -38,7 +48,6 @@ export function prepareImage(checkout,{grant,tempRoot,out},invoke){
const start=performance.now();let buildOutput;try{buildOutput=run(['compose','build']);}catch(e){if(out)writeFileSync(join(out,'image-build.log'),String(e.stdout??'')+String(e.stderr??''),{mode:0o600,flag:'wx'});throw e;}if(out)writeFileSync(join(out,'image-build.log'),buildOutput,{mode:0o600,flag:'wx'});const buildWallMs=performance.now()-start;
if(inputDigest!==imageFingerprint(checkout))refuse('image inputs changed while building');
const actual=JSON.parse(run(['image','inspect',tag,'--format','{{json .}}']));
const base=JSON.parse(run(['image','inspect','node:24-bookworm-slim','--format','{{json .}}']));
const receipt={inputDigest,imageId:actual.Id,tag,baseDigest:base.RepoDigests?.find(x=>/@sha256:[a-f0-9]{64}$/.test(x)),platform:`${actual.Os}/${actual.Architecture}`,grant,builds:1,buildWallMs,at:new Date().toISOString()};
const receipt={inputDigest,imageId:actual.Id,tag,baseDigest,platform:`${actual.Os}/${actual.Architecture}`,grant,builds:1,buildWallMs,at:new Date().toISOString()};
verifyImage(receipt,checkout,()=>actual);return receipt;
}