# Supply-chain release gate ## What is inventoried ```text 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: ```text 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 ```text 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 ```text 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.