88 lines
4.1 KiB
Markdown
88 lines
4.1 KiB
Markdown
# 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 `<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.
|