mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-12 15:53:42 +00:00
feat: add Kubernetes multiplayer security baseline
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: control-plane
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
spec:
|
||||||
|
replicas: 2
|
||||||
|
selector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
labels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
spec:
|
||||||
|
serviceAccountName: control-plane
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
securityContext:
|
||||||
|
runAsNonRoot: true
|
||||||
|
runAsUser: 10001
|
||||||
|
runAsGroup: 10001
|
||||||
|
seccompProfile:
|
||||||
|
type: RuntimeDefault
|
||||||
|
containers:
|
||||||
|
- name: control-plane
|
||||||
|
image: ghcr.io/cosmic-clash/control-plane@sha256:0000000000000000000000000000000000000000000000000000000000000000
|
||||||
|
ports:
|
||||||
|
- name: http
|
||||||
|
containerPort: 8080
|
||||||
|
securityContext:
|
||||||
|
allowPrivilegeEscalation: false
|
||||||
|
readOnlyRootFilesystem: true
|
||||||
|
capabilities:
|
||||||
|
drop: [ALL]
|
||||||
|
resources:
|
||||||
|
requests:
|
||||||
|
cpu: 100m
|
||||||
|
memory: 128Mi
|
||||||
|
limits:
|
||||||
|
cpu: 1
|
||||||
|
memory: 512Mi
|
||||||
|
env:
|
||||||
|
- name: DATABASE_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cosmic-clash-database
|
||||||
|
key: password
|
||||||
|
- name: REDIS_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cosmic-clash-redis
|
||||||
|
key: password
|
||||||
|
- name: STEAM_PUBLISHER_KEY
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: cosmic-clash-steam
|
||||||
|
key: publisher-key
|
||||||
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
namespace: cosmic-clash
|
||||||
|
resources:
|
||||||
|
- namespace.yaml
|
||||||
|
- service-accounts.yaml
|
||||||
|
- rbac.yaml
|
||||||
|
- network-policies.yaml
|
||||||
|
- control-plane-deployment.yaml
|
||||||
|
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: cosmic-clash
|
||||||
|
labels:
|
||||||
|
pod-security.kubernetes.io/enforce: restricted
|
||||||
|
pod-security.kubernetes.io/audit: restricted
|
||||||
|
pod-security.kubernetes.io/warn: restricted
|
||||||
|
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: default-deny-ingress-egress
|
||||||
|
spec:
|
||||||
|
podSelector: {}
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
---
|
||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: NetworkPolicy
|
||||||
|
metadata:
|
||||||
|
name: control-plane-allowed-flows
|
||||||
|
spec:
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: control-plane
|
||||||
|
policyTypes: [Ingress, Egress]
|
||||||
|
ingress:
|
||||||
|
- from:
|
||||||
|
- namespaceSelector: {}
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: edge-gateway
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 8080
|
||||||
|
egress:
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: data
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: postgres
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 5432
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: data
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
app.kubernetes.io/name: redis
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 6379
|
||||||
|
- to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: agones-system
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 443
|
||||||
|
- ports:
|
||||||
|
- protocol: UDP
|
||||||
|
port: 53
|
||||||
|
- protocol: TCP
|
||||||
|
port: 53
|
||||||
|
to:
|
||||||
|
- namespaceSelector:
|
||||||
|
matchLabels:
|
||||||
|
kubernetes.io/metadata.name: kube-system
|
||||||
|
podSelector:
|
||||||
|
matchLabels:
|
||||||
|
k8s-app: kube-dns
|
||||||
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: Role
|
||||||
|
metadata:
|
||||||
|
name: control-plane-agones-allocator
|
||||||
|
namespace: agones-system
|
||||||
|
rules:
|
||||||
|
- apiGroups: ["allocation.agones.dev"]
|
||||||
|
resources: ["gameserverallocations"]
|
||||||
|
verbs: ["create"]
|
||||||
|
---
|
||||||
|
apiVersion: rbac.authorization.k8s.io/v1
|
||||||
|
kind: RoleBinding
|
||||||
|
metadata:
|
||||||
|
name: cosmic-clash-control-plane-agones-allocator
|
||||||
|
namespace: agones-system
|
||||||
|
subjects:
|
||||||
|
- kind: ServiceAccount
|
||||||
|
name: control-plane
|
||||||
|
namespace: cosmic-clash
|
||||||
|
roleRef:
|
||||||
|
kind: Role
|
||||||
|
name: control-plane-agones-allocator
|
||||||
|
apiGroup: rbac.authorization.k8s.io
|
||||||
|
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: control-plane
|
||||||
|
namespace: cosmic-clash
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
---
|
||||||
|
apiVersion: v1
|
||||||
|
kind: ServiceAccount
|
||||||
|
metadata:
|
||||||
|
name: match-server
|
||||||
|
namespace: cosmic-clash
|
||||||
|
automountServiceAccountToken: false
|
||||||
|
|
||||||
+5
-3
@@ -57,9 +57,11 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
|
|||||||
- [x] Complete the threat model for forgery, replay, queue/flood/bot abuse,
|
- [x] Complete the threat model for forgery, replay, queue/flood/bot abuse,
|
||||||
workload/insider compromise, DDoS, supply chain and denial-of-wallet
|
workload/insider compromise, DDoS, supply chain and denial-of-wallet
|
||||||
([THREAT-MODEL.md](docs/THREAT-MODEL.md)).
|
([THREAT-MODEL.md](docs/THREAT-MODEL.md)).
|
||||||
- [ ] Enforce restricted workloads/RBAC/networks/private stores/backups/secrets;
|
- [ ] **IN PROGRESS:** Enforce restricted workloads/RBAC/networks/private
|
||||||
isolate SDR signing behind an audited non-exportable signer and add
|
stores/backups/secrets; isolate SDR signing behind an audited non-exportable
|
||||||
volumetric edge defense, WebSocket limits and overload shedding.
|
signer and add volumetric edge defense, WebSocket limits and overload
|
||||||
|
shedding. A provider-neutral restricted Kubernetes baseline and structural
|
||||||
|
policy tests now exist; live edge/data-plane controls remain.
|
||||||
- [ ] Pin, scan, SBOM and sign artifacts; verify signatures at admission and
|
- [ ] Pin, scan, SBOM and sign artifacts; verify signatures at admission and
|
||||||
document the critical vulnerability SLA.
|
document the critical vulnerability SLA.
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1184,7 +1184,7 @@ the local/CI/community transport, not a silent production fallback.
|
|||||||
| 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.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]` | **IN PROGRESS.** Dependency-free workload credential policy validates an adapter-verified signature, issuer/audience/time bounds, namespace/service account, pod UID, GameServer UID, allocation ID, match ID and server ID before result submission | `server/domain/workload.go` and adversarial tests reject every binding mutation, missing/unverified signature and time boundary; `server/testkit/pipeline_test.go` carries allocation identity through the offline result path; projected-token/JWT adapter, trusted-cluster verification and live duplicate/conflict alerting remain |
|
| 8.10 `[D:8.5,8.31]` | **IN PROGRESS.** Dependency-free workload credential policy validates an adapter-verified signature, issuer/audience/time bounds, namespace/service account, pod UID, GameServer UID, allocation ID, match ID and server ID before result submission | `server/domain/workload.go` and adversarial tests reject every binding mutation, missing/unverified signature and time boundary; `server/testkit/pipeline_test.go` carries allocation identity through the offline result path; projected-token/JWT adapter, trusted-cluster verification and live duplicate/conflict alerting remain |
|
||||||
| 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.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.12 `[D:8.11]` | **IN PROGRESS.** Provider-neutral Kubernetes baseline enforces restricted namespace admission, non-root/read-only/no-capability workloads, separate service accounts, allocator-only RBAC, default-deny networking and explicit edge/data/DNS/Agones flows; application manifests consume externally populated Secret objects | `deploy/k8s/base/` plus `server/security/test_kubernetes_policies.py` cover the static hardening and secret-reference invariants; private-store provisioning, edge DDoS/WAF/origin shielding, WebSocket limits, overload shedding, encrypted backups and live policy/load tests remain |
|
||||||
| 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 |
|
| 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 |
|
||||||
|
|
||||||
#### 8C — Queueing, matchmaking, playlists and rating
|
#### 8C — Queueing, matchmaking, playlists and rating
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
from pathlib import Path
|
||||||
|
import re
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
|
||||||
|
BASE = Path(__file__).parents[2] / "deploy" / "k8s" / "base"
|
||||||
|
|
||||||
|
|
||||||
|
class KubernetesPolicyTest(unittest.TestCase):
|
||||||
|
def read(self, name):
|
||||||
|
return (BASE / name).read_text()
|
||||||
|
|
||||||
|
def test_namespace_enforces_restricted_pod_security(self):
|
||||||
|
namespace = self.read("namespace.yaml")
|
||||||
|
for key in ("enforce", "audit", "warn"):
|
||||||
|
self.assertIn(f"pod-security.kubernetes.io/{key}: restricted", namespace)
|
||||||
|
|
||||||
|
def test_workload_is_non_root_immutable_and_unprivileged(self):
|
||||||
|
deployment = self.read("control-plane-deployment.yaml")
|
||||||
|
for required in (
|
||||||
|
"runAsNonRoot: true", "type: RuntimeDefault", "allowPrivilegeEscalation: false",
|
||||||
|
"readOnlyRootFilesystem: true", "drop: [ALL]", "resources:",
|
||||||
|
"image: ghcr.io/cosmic-clash/control-plane@sha256:",
|
||||||
|
):
|
||||||
|
self.assertIn(required, deployment)
|
||||||
|
self.assertNotRegex(deployment, r"(?im)^\s*(password|token|private.?key):\s*[^\n]+$")
|
||||||
|
self.assertIn("secretKeyRef:", deployment)
|
||||||
|
|
||||||
|
def test_rbac_is_scoped_to_allocator_create(self):
|
||||||
|
rbac = self.read("rbac.yaml")
|
||||||
|
self.assertIn("namespace: agones-system", rbac)
|
||||||
|
self.assertIn('resources: ["gameserverallocations"]', rbac)
|
||||||
|
self.assertIn('verbs: ["create"]', rbac)
|
||||||
|
self.assertNotRegex(rbac, r"verbs:.*\b(get|list|watch|update|patch|delete|\*)\b")
|
||||||
|
self.assertNotIn('resources: ["*"]', rbac)
|
||||||
|
|
||||||
|
def test_default_deny_and_only_declared_data_dns_edge_flows_exist(self):
|
||||||
|
policies = self.read("network-policies.yaml")
|
||||||
|
self.assertIn("name: default-deny-ingress-egress", policies)
|
||||||
|
self.assertIn("policyTypes: [Ingress, Egress]", policies)
|
||||||
|
for port in ("port: 8080", "port: 5432", "port: 6379", "port: 443", "port: 53"):
|
||||||
|
self.assertIn(port, policies)
|
||||||
|
self.assertNotIn("ipBlock:", policies)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user