8.6 KiB
Build and supply-chain gate
Local blocking controls
pnpm install --frozen-lockfileand a real manifest/lock mismatch fixture- all direct and transitive lockfile rows with package SHA-512 integrity
- production/development, direct/transitive and platform-optional classification
- package-manifest license allow/deny policy
- approved inventory baseline digest and actual add/remove/change/upgrade diff
- independent review for new direct production dependencies
- CycloneDX 1.6 SBOM and inventory component/edge coherence
- source/lock/SBOM/dist-linked local provenance statement
- source, opt-in recipes, scripts, tests, tracked config/schema, public, built asset and generated release metadata secret scan
- two-build
SOURCE_DATE_EPOCHreproducibility check
The canonical commands are:
corepack pnpm verify:lockfile
corepack pnpm verify:reproducible-build
corepack pnpm build:release-candidate
corepack pnpm verify:local-evidence
corepack pnpm check:supply-chain:fixtures
config/security/dependency-baseline.json is the approved local baseline.
Changing it requires DEPENDENCY_BASELINE_OWNER and
DEPENDENCY_BASELINE_REASON; editing the digest or hardcoding an empty diff is
rejected.
External promotion controls
Promotion reads the provider files named by VULNERABILITY_REPORT_PATH and
PROVENANCE_ATTESTATION_PATH. The vulnerability report must bind both the
exact lockfile digest and candidate distSha256; the provenance attestation
must name dist with that same digest. Both documents use strict schemas and
Ed25519 signatures verified with separately configured trusted public keys and
key IDs (VULNERABILITY_PUBLIC_KEY_PATH, VULNERABILITY_KEY_ID,
PROVENANCE_PUBLIC_KEY_PATH, and PROVENANCE_KEY_ID). Keys of another curve,
including Ed448, are rejected even if a document labels its algorithm
Ed25519.
immutable_build archives the raw pnpm-lock.yaml, dist (including hidden
.vite files), the build manifest, module inventory, release verification,
secret-scan result, and local supply-chain evidence once. The candidate
manifest hashes the raw lockfile bytes and requires that digest to equal the
dependency inventory's lockfileSha256. Before upload, the producer validates
the manifest-bound exact archive member set and every member digest, then
publishes the archive SHA-256 as an immutable job output. The two provider jobs
download this same archive separately, compare that output digest, validate the
exact member set before extracting only into isolated roots, and receive
CANDIDATE_LOCKFILE_PATH and CANDIDATE_DIST_SHA256; configured
VULNERABILITY_PROVIDER_COMMAND and PROVENANCE_PROVIDER_COMMAND must emit the
signed reports. After each external command returns, provider upload validation
rechecks the unchanged archive and extracted candidate, parses the provider JSON
with its strict schema, and binds its dist and lockfile digests before upload.
If either provider input is absent, local verification remains meaningful but
artifacts/security/supply-chain-verification.json records
promotionStatus: FAIL_UNVERIFIED. verify:provider-evidence and
verify:promotion then exit non-zero. Promotion recomputes the candidate file
set and digests, then read-only revalidates the archived executable schemas,
raw lockfile, module inventory, build outputs, release coherence, SBOM,
provenance, security scan and supply-chain coherence. It never rebuilds or
rewrites candidate evidence. Promotion uploads the already verified archive
itself with the two provider reports and verification records; it does not
create a replacement archive from extracted files. Scanner or signing outages
are not converted to an empty PASS.
The generated workflow is also a supply-chain control. config/ci/gates.json
is its sole typed authority. Run corepack pnpm generate:ci-workflow after a
contract change and corepack pnpm check:ci-workflow (or the encompassing
corepack pnpm check:ci) to reject byte drift in the checked-in Gitea adapter.
Action resolution is separately closed over one typed, runtime-frozen registry
in scripts/contracts/ci-gates.ts. Every generated uses: value is an absolute
upstream URL pinned to a full commit SHA:
https://github.com/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5(v4.3.1)https://github.com/actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020(v4.4.0)https://github.com/ChristopherHX/gitea-upload-artifact@81f940d004763f986ba3582c007fd842dd5cb0d7(patchedv4branch)https://github.com/ChristopherHX/gitea-download-artifact@75635f32b4c1c41c4b3d64e8f85210112ed4c9c7(patchedv4branch)
Unknown actions, relative repositories, tags/branches and short SHAs are
rejected. Gitea 1.22's official Actions documentation recommends the
ChristopherHX patched artifact forks for v4 compatibility; the supported
deployment baseline is nevertheless Gitea 1.26.4+ with Gitea Runner 1.0.0+.
A real end-to-end provider smoke on the staging Gitea instance remains
mandatory before any generated job becomes a required check.
External provider supervision is fail-closed and requires a Linux runner with
an executable /usr/bin/bwrap. Bubblewrap mounts the repository workspace
read-only, hides .git, and read-only binds the trusted process.execPath
inside the sandbox at /tmp/node. It exposes only the sibling untrusted raw-evidence
directory as writable. Provider commands run non-interactively through
/bin/sh -eu -c, receive a minimized environment plus only their own
provider-prefixed credentials, and have a 30-minute limit. They must consume
the supplied candidate paths and digests, write exactly the configured raw
report, and must not depend on workspace mutation, host home/toolcache access,
or direct access to the sealed evidence path. Missing sandbox support, stale or
misplaced outputs, command failure/timeout, and post-command candidate drift
all stop publication.
Provider documents are strict schema v2. Their Ed25519 signature covers the supervisor-supplied evidence type, validity window, run ID/attempt, independent 32-byte invocation nonce, archived source identity, and all four candidate digests. Each provider job exposes its supervisor-generated nonce as a job output; promotion treats those outputs as the independent expected values and never lets a report define its own expected nonce. A report from another attempt, source, archive, nonce, or key fingerprint is fail-closed even when it has been correctly re-signed.
The immutable archive contains a strict producer-local assessment. Promotion
revalidates it from captured archive members without reopening checkout policy
or source paths. The finalizer captures the archive, both reports, and both
public keys once, generates both verification v3 records in memory, and writes
exactly five mode-0400 files beneath a random mode-0700 directory in
RUNNER_TEMP. The exact five are the captured archive, captured vulnerability
report, captured provenance attestation, generated provider-verification v3,
and generated promotion-verification v3. The promotion record binds the exact
provider-record hash, local-assessment hash, both report hashes, run/source/
candidate identities, both nonces, both key IDs/fingerprints, and canonical
trust-policy hash. It never creates or reuses .release/promoted-staging.
The final promotion verification/staging step must be immediately adjacent to
the promoted-release upload, and that upload must not use always(). This
reduces the post-verification mutation window but does not seal a pathname
across two action steps. The runner is therefore required to be trusted,
exclusive and single-tenant, with no provider command or other same-UID process
surviving from staging into the immediately following upload. The artifact
service and transfer actions also remain outside the candidate's cryptographic
identity: every downstream consumer must revalidate the downloaded archive,
manifest member digests and signed provider evidence. Producer-side adjacency
does not provide consumer-side digest revalidation.
The immediately following upload action still reopens pathnames. The
descriptor-relative staging and cleanup code does not claim an atomic
renameat2 handoff or close a malicious same-UID Gitea upload adapter; the
staging Gitea smoke/native platform adapter remains the required closure for
that boundary. That smoke must exercise exact-five upload and download plus
cleanup on success, validation failure, upload failure, and cancellation. No
native uploader or renameat2 guarantee exists in this repository today.
Approved vulnerability exceptions require vulnerability/package identity, owner, a different reviewer, reason and expiry. Expired or self-approved exceptions are blocking.