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.