docs: complete multiplayer threat model

This commit is contained in:
Josh Creek
2026-08-31 20:58:11 +01:00
parent 217ae263cd
commit bbabd259b6
4 changed files with 63 additions and 3 deletions
+36
View File
@@ -0,0 +1,36 @@
# Cosmic Clash multiplayer threat model
This is the launch threat model for the control plane, dedicated servers and
clients. It records the security boundary and the verification owner for each
class of failure; it does not treat a trusted workload class as a trusted
individual pod.
| Threat | Prevention | Detection / response | Owner | Residual risk |
|---|---|---|---|---|
| Forged Steam identity or ticket | Backend calls Steam validation for the expected App ID; player ID comes from the verified SteamID mapping, never request JSON | Ticket rejection metrics, replay alerts, ban/revoke identity | Identity/API | Valve/Steam outage pauses new authenticated sessions |
| Ticket/session replay | Single-use ticket nonce; opaque short-lived session token; store token digest and revocation in PostgreSQL | Duplicate-ticket and revoked-session counters; incident revoke all sessions for identity | Identity/API | Stolen live session remains usable until expiry/revocation propagation |
| Queue/proposal flooding or duplicate claims | Body/rate limits, one active ticket partial unique index, idempotency keys, serializable participant fence | Per-identity/IP rate alerts, queue-depth and conflict dashboards, overload shedding | API/matcher | Distributed abusive identities can consume bounded capacity until automated bans act |
| Latency-evidence forgery | Opaque location, nonce/freshness checks, server-computed RTT, discrepancy quarantine; evidence affects placement only | Three-bad/five-clean counters and regional RTT SLO alerts | Matcher/networking | Colluding endpoints can bias placement within the accepted evidence window |
| Join-authorisation theft or slot hijack | Signed match-scoped authorisation binds verified SteamID/match/server/team/slot/protocol/expiry; server-owned generation fences old peers | Rejected-binding/generation metrics and audit events; revoke assignment | Allocator/game-server | A stolen valid authorisation remains usable until expiry unless the server revokes it |
| Forged or replayed match result | Pod/GameServer-bound projected identity or one-match credential; issuer/audience/namespace/SA/pod/GameServer/allocator binding; canonical digest | Receipt conflict is inert and pages; duplicate is idempotent; result lag alerts at 5/30 minutes | Result/maintenance | A compromised authoritative pod can submit before compromise is detected |
| Workload/insider compromise | Per-workload service accounts, least RBAC, private stores, default-deny network, no publisher/root key in game pods | Credential-use audit, pod identity anomaly alerts, immediate workload drain/revoke | Platform/security | Cluster-admin or KMS compromise is outside application controls |
| Gameplay/API DDoS and flood | Connection/body/WebSocket limits, token buckets, overload shedding, edge WAF/DDoS service, live-result priority | Saturation, 5xx, tick-backlog and dropped-work dashboards; shed new queue/allocation work first | SRE/platform | Volumetric attack may require provider mitigation capacity |
| SDR signing-key theft | Offline CA separated from online signer; non-exportable KMS/HSM key; signer allowlist and short-lived tickets | Signer audit and anomaly alerts; rotate/revoke certificates and tickets | Security/networking | Provider/Valve trust or HSM compromise requires external response |
| Dependency/image supply chain | Pin image/dependency digests, SBOM, vulnerability scan, artifact signature and admission verification | CI/admission failures and provenance inventory; critical-fix SLA | Release/security | Unknown zero-days remain possible until detection or patch |
| Denial of wallet / autoscaling abuse | Allocation quotas, budgets, warm-capacity limits, per-identity/IP controls and scale ceilings | Cost-per-match, allocation-rate and quota alerts; disable region/playlist safely | SRE/finance | Legitimate launch spikes can trigger conservative limits |
| Data loss or cache inconsistency | PostgreSQL backups/RPO <=5m, serializable transactions, transactional outbox; Redis is rebuildable only | Restore/failover rehearsal, cache-repair metrics, result reconciliation | Data/SRE | Recovery can pause new work; valid live matches must continue |
## Trust boundaries
- Clients are untrusted and cannot submit ratings, outcomes, penalties,
allocation state or exemptions.
- Game servers are authoritative for simulation but are not trusted for
identity, allocation ownership, or unrestricted result submission.
- PostgreSQL is the durable authority. Redis, Agones annotations and local
spool files are recoverable transport/cache state.
- The offline SDR CA and online leaf signer are separate; API, matcher,
allocator and game-server workloads cannot read signer keys.
Every accepted residual risk above has an owner and a planned detection path.
Security incidents fail closed for identity/result ownership and degrade open
only for recoverable result delivery, where the signed spool is reconciled.
+3 -2
View File
@@ -53,8 +53,9 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
identity; make identical duplicates idempotent and conflicting results
inert/alerting. Pure Go binding, hashing, reconciliation, and SQL boundaries exist;
production credential validation remains.
- [ ] Complete the threat model for forgery, replay, queue/flood/bot abuse,
workload/insider compromise, DDoS, supply chain and denial-of-wallet.
- [x] Complete the threat model for forgery, replay, queue/flood/bot abuse,
workload/insider compromise, DDoS, supply chain and denial-of-wallet
([THREAT-MODEL.md](docs/THREAT-MODEL.md)).
- [ ] Enforce restricted workloads/RBAC/networks/private stores/backups/secrets;
isolate SDR signing behind an audited non-exportable signer and add
volumetric edge defense, WebSocket limits and overload shedding.
+1 -1
View File
@@ -1183,7 +1183,7 @@ the local/CI/community transport, not a silent production fallback.
| 8.8 `[D:8.7]` | **IN PROGRESS.** Pure Go session policy issues opaque short-lived tokens, stores only digests, authenticates by verified player identity and supports revocation | `server/domain/auth.go` covers wrong-token/expiry/revocation behavior; distributed revocation, account/IP limits, request limits and production session persistence remain |
| 8.9 `[D:8.4,8.7]` | **IN PROGRESS.** Pure Go join policy binds SteamID/player/match/server/team/slot/protocol/expiry, rejects duplicate roster slots, permits same-identity reclaim through backend loss and fences prior server-owned generations | `server/domain/reconnect.go` covers SteamID/server/slot binding, expiry, repeated reclaim, grace boundary and old-generation fencing; signed token issuance/verification, persistent leases, Godot `hello` transport and production integration remain |
| 8.10 `[D:8.5,8.31]` | Authenticate results with pod-bound projected identity or one-match attested credential; validate issuer/audience/expiry, namespace/SA, pod UID, GameServer UID and allocator match binding | Another pod sharing a workload class cannot submit for the allocation; identical duplicates are idempotent; conflicting results are inert and alerting across all trusted clusters |
| 8.11 `[D:8.1]` | Write the threat model: forged clients/replay/queues/results, floods/bots, pod/insider compromise, gameplay and API DDoS, SDR signing-key theft, dependencies and denial-of-wallet | Every threat has prevention/detection/owner/verification; accepted residual risks are explicit; offline CA and online signer trust boundaries are separate |
| 8.11 `[D:8.1]` | **DONE.** Write the threat model: forged clients/replay/queues/results, floods/bots, pod/insider compromise, gameplay and API DDoS, SDR signing-key theft, dependencies and denial-of-wallet | [`docs/THREAT-MODEL.md`](docs/THREAT-MODEL.md) records prevention, detection/response, owner and residual risk for every threat; it separates offline CA/online signer, client/game-server/PostgreSQL/Redis trust boundaries and recovery behavior |
| 8.12 `[D:8.11]` | Harden workloads and edge: restricted containers, least RBAC, private DB/Redis, default-deny networks, backups/secrets, volumetric DDoS/WAF/origin shielding, WebSocket limits and overload shedding | Policy/network tests enforce declared flows; edge load test preserves result ingress/live matches while rejecting new work; no credential appears in Git/images/args/telemetry |
| 8.13 `[D:8.12]` | Pin images by digest; generate SBOMs, scan dependencies/images, sign artifacts, verify signatures at admission and document a critical-fix SLA | CI blocks a vulnerable/disallowed or unsigned release artifact and records the exact provenance deployed |
+23
View File
@@ -0,0 +1,23 @@
from pathlib import Path
import unittest
MODEL = (Path(__file__).parents[2] / "docs" / "THREAT-MODEL.md").read_text()
class ThreatModelTest(unittest.TestCase):
def test_required_threat_classes_have_controls_and_owners(self):
for term in (
"Forged Steam identity", "Ticket/session replay", "Queue/proposal",
"Latency-evidence forgery", "Join-authorisation", "Forged or replayed match result",
"Workload/insider compromise", "DDoS", "SDR signing-key theft",
"supply chain", "Denial of wallet / autoscaling abuse", "Data loss",
):
self.assertIn(term, MODEL)
self.assertIn("| Owner |", MODEL)
self.assertIn("Residual risk", MODEL)
self.assertIn("PostgreSQL is the durable authority", MODEL)
if __name__ == "__main__":
unittest.main()