Files
ModelForge/docs/security/SUPPLY_CHAIN_RELEASE_GATE.md

103 lines
4.4 KiB
Markdown

# 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.