mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +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,
|
||||
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.
|
||||
- [ ] **IN PROGRESS:** 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. 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
|
||||
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.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.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 |
|
||||
|
||||
#### 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