Initial public ModelForge release
This commit is contained in:
@@ -0,0 +1,69 @@
|
||||
# Credential threat tests
|
||||
|
||||
## The three credential planes
|
||||
|
||||
ModelForge issues three kinds of secret and they never substitute for one another.
|
||||
|
||||
```text
|
||||
operator API key X-ModelForge-Admin-Token admin routes, /metrics, recovery, lifecycle
|
||||
node credential Authorization: Bearer agent publication only
|
||||
capability secret Authorization: Bearer one named capability, through the gateway
|
||||
```
|
||||
|
||||
A capability secret on an operator route returns 401. An operator route with no key configured
|
||||
returns 503 rather than falling open. The invariant `capability_clients_are_not_operators` asserts
|
||||
no client ever carries an operator or node scope in its capability list.
|
||||
|
||||
## Storage
|
||||
|
||||
Only hashes are stored. `ServiceCredential` has no column that could hold a plaintext secret, and
|
||||
the stored hash is verified to be the SHA-256 of the issued value. A prefix is retained for operator
|
||||
identification and is bounded so it cannot reconstruct the secret.
|
||||
|
||||
Live checks confirmed no operator key, backup encryption key, node credential, `PGPASSWORD` or
|
||||
database password appears in the API logs, in any audit event, in the OpenAPI document, or in any
|
||||
recovery API response.
|
||||
|
||||
## Revocation
|
||||
|
||||
Revocation is permanent and immediate. A revoked credential is refused on every subsequent attempt,
|
||||
including after the node re-enrols with a new one — enrolment revokes rather than deletes, so the
|
||||
revocation survives in history. The invariant `revoked_credentials_stay_revoked` asserts no revoked
|
||||
credential records a use later than its revocation.
|
||||
|
||||
## Rotation
|
||||
|
||||
Rotation leaves exactly one usable secret. Two simultaneously valid secrets is an unbounded window
|
||||
rather than a graceful cutover, so the old one is revoked as the new one is issued and the partial
|
||||
unique index enforces it.
|
||||
|
||||
## Races
|
||||
|
||||
Revocation racing authentication was tested with sixteen concurrent threads. Whichever order the
|
||||
race resolves in the revoked credential is never accepted, and re-checking afterwards still refuses
|
||||
it.
|
||||
|
||||
A single-use enrolment token was stormed with sixty concurrent attempts. Exactly one succeeded,
|
||||
fifty-nine were refused, and one node identity with one active credential exists afterwards. This
|
||||
covers a real M15 defect where the `used_at` check was read-then-write and two agent threads could
|
||||
burn one token on two identities for the same hardware.
|
||||
|
||||
## Burst behaviour
|
||||
|
||||
Sixty invalid credentials produced sixty identical 401s with no amplification, and the valid
|
||||
credential still worked afterwards. Refusal is cheap and constant: there is no lockout to trigger
|
||||
and no differential response to measure. Mean refusal latency was 12.75 ms with a 58.09 ms maximum.
|
||||
|
||||
## Node identity
|
||||
|
||||
A recovered node keeps its persisted identity, so losing a credential does not lose its
|
||||
accelerators, storage roots or history. A node whose identity file is also gone enrols as a new
|
||||
node — ModelForge genuinely cannot distinguish it from different hardware — and the operator
|
||||
disables the superseded row rather than letting two nodes claim the same machine.
|
||||
|
||||
## Diagnostics
|
||||
|
||||
A failed publication now names the status, method and path, never the response body or the
|
||||
credential. Before M16 the agent logged only `control-plane publication failed; retrying`, which
|
||||
cannot distinguish a revoked credential from a DNS failure — and had been doing so for two days
|
||||
while pointed at a placeholder URL.
|
||||
Reference in New Issue
Block a user