Initial public ModelForge release

This commit is contained in:
Jens
2026-09-01 21:30:16 +02:00
commit 7082ab955a
490 changed files with 104252 additions and 0 deletions
+87
View File
@@ -0,0 +1,87 @@
# 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.