# Security The operator-facing summary. The reasoning behind each control is in [THREAT_MODEL.md](security/THREAT_MODEL.md); the supply-chain rules are in [MODEL_SUPPLY_CHAIN_POLICY.md](security/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](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 `