Files
ModelForge/docs/security/SUPPLY_CHAIN_RELEASE_GATE.md

4.4 KiB

Supply-chain release gate

What is inventoried

Python packages    read from the built images, not from a manifest
Node packages      read from the lock file, which is what `npm ci` installs
base images        pinned per component
CI actions         pinned by commit SHA
runtime images     pinned by digest

Reading the Python inventory from the image rather than the manifest matters: a manifest states intent, an image states what shipped. A build that silently resolved a different transitive version is exactly the case an SBOM exists to catch.

SBOM

scripts/m16_sbom.py produces CycloneDX 1.5 at docs/security/sbom/modelforge-cyclonedx.json. It lists components and versions with package URLs, not dependency contents: a release inventory must be readable and diffable, so no binaries are stored.

Each component carries the image it belongs to, so a finding can be traced to what actually ships it rather than to the repository as a whole.

Image provenance

docs/security/sbom/image-provenance.json binds each built image to its image id, repository digest where one exists, creation timestamp and the exact source commit. Provenance is what makes the SBOM verifiable rather than merely present: without it, an inventory describes an image nobody can identify again.

CVE triage

Findings are classified, and the classification is evidence rather than a label:

EXPLOITABLE           reachable from an untrusted input in this deployment
NOT_REACHABLE         the vulnerable path is not executed by ModelForge
DEV_ONLY              present only in a development or test dependency
FALSE_POSITIVE        the advisory does not apply to this version or configuration
ACCEPTED_TEMPORARY    accepted with an owner, a reason and a review date
MUST_FIX              blocks the release

"Zero CVEs" is never claimed without the scan output that supports it, and an aggregate count never replaces the per-finding record for a HIGH or CRITICAL.

Dependency pinning

Base images are pinned to a major or minor tag so they continue to receive security patches on a compatible line; the runtime worker is pinned by digest because its CUDA and PyTorch versions are part of the reproducibility contract for a benchmark. CI actions are pinned by commit SHA.

No dependency is declared as latest. A lock file makes a build reproducible in practice, but a floating specifier means the next npm install can pull a different major version — or a compromised release — without anyone deciding to.

Container posture

non-root            api, node-agent, web; postgres and redis drop to their own users
no privileged mode  none
no Docker socket    none
capabilities        dropped entirely on api, node-agent and web
no-new-privileges   api, node-agent, web
read-only rootfs    node-agent, web; api writes only to its mounted data volumes
healthchecks        api, postgres, redis, web

Network boundaries

PostgreSQL   loopback only by default
Redis        loopback only by default
console      loopback only by default
API          published deliberately; every admin route is operator-authenticated
runtime      no public port; reached only through the gateway
Node Agent   outbound only; no inbound listener

PostgreSQL and Redis defaulting to every interface was the most serious finding of the M16 review: the control-plane database, holding provenance, credential hashes and the audit trail, was reachable from the LAN behind a development password. Both now bind to loopback, overridable per deployment.

Secrets

gitleaks runs over the full history, not the working tree. A finding is investigated rather than suppressed; the single allowlist entry names one exact string and says why it is not a credential.

Generated artefacts are checked too: logs, backup manifests, audit exports and the OpenAPI document must contain no operator key, encryption key, node credential or database password.

Cryptography

Backup encryption is AES-256-GCM from cryptography. ModelForge designs no primitive of its own; BackupCipher is an adapter boundary so a KMS or HSM can replace the local key.

The M16 review verified, and static tests now enforce, that a fresh nonce is drawn inside the chunk loop rather than once per file, that each chunk is authenticated with associated data binding it to its key id and index, and that a decryption failure removes both the partial file and the destination so no readable plaintext survives a wrong key.