Files

4.1 KiB

Security

The operator-facing summary. The reasoning behind each control is in THREAT_MODEL.md; the supply-chain rules are in MODEL_SUPPLY_CHAIN_POLICY.md.

Trust boundaries

Boundary Rule
Operator ↔ control plane Every admin route requires the operator API key
Application ↔ control plane A service credential scoped to specific capabilities
Control plane ↔ compute node The node authenticates outbound; the control plane never dials a node
Control plane ↔ upstream model source Acquisition only, into quarantine, verified before use

An operator credential and a capability credential are different things and cannot substitute for each other — capability_clients_are_not_operators is asserted as a platform invariant.

Model code is never executed

trust_remote_code is false and production refuses to start if it is anything else. A model repository cannot ship Python that ModelForge will run. Artifacts are verified per file by SHA-256 against the recorded upstream identity before leaving quarantine, and an artifact that fails stays there.

Secrets

Secret Stored as Notes
Operator API key configuration only never persisted by the platform
Service credential SHA-256 hash plus a short prefix cannot be read back; rotate if lost
Node credential SHA-256 hash single-use enrolment, atomically claimed
Backup encryption key configuration only store it outside this deployment
Hugging Face token configuration only acquisition only; never passed to a runtime

ModelForge never generates its own credentials. A platform that mints its own admin secret has no way to tell you it did. Generation procedures are in CONFIGURATION.md.

Revocation is permanent, expiry is enforced, a disabled client cannot serve with a valid secret, and rotation leaves exactly one usable secret.

Network exposure

By default only the API is published. PostgreSQL, Redis and the console bind to loopback, and a test fails if any Compose projection publishes a datastore more widely — including the disaster-recovery projection, which was found doing exactly that during the v1 gate.

Container hardening

No privileged containers. No container mounts the Docker socket. No host network or PID namespace. The API, Node Agent and console drop all capabilities and set no-new-privileges; the Node Agent and console run read-only root filesystems. The console serves a static build from an unprivileged nginx — never a development server.

Console security headers

Content-Security-Policy with default-src 'none', script-src 'self' and no unsafe-inline or unsafe-eval; inline style is permitted only as an attribute via style-src-attr, which covers the dynamic width bars the console uses while still blocking an injected <style> element. connect-src is derived at build time from the same API base URL compiled into the bundle, so the policy cannot drift from the origin the bundle calls.

Also: X-Content-Type-Options, X-Frame-Options, Referrer-Policy, Cross-Origin-Opener-Policy, Cross-Origin-Resource-Policy and Permissions-Policy — on the entry document and on hashed assets.

Errors

Every error response is exactly code, message and correlation_id. A 500 body is exactly Internal Server Error: no DSN, no driver name, no SQL, no path, no traceback.

Backups

AES-256-GCM from a reviewed library, a fresh nonce per chunk, associated data binding each chunk to its key id and index, and fail-closed decryption that removes both the partial file and the destination. No encryption key appears in any manifest or audit event.

Supply chain

Dependencies are locked; no floating specifier is permitted and a test enforces it. Python dependencies are audited inside the built images rather than against a manifest, and the frontend with npm audit. Each release publishes a CycloneDX SBOM bound to the image digests and the source commit.

Reporting a vulnerability

Report privately to the repository owner. Do not open a public issue.