mirror of
https://github.com/jcreek/CosmicClash.git
synced 2026-09-10 16:04:04 +00:00
feat: enforce supply chain policy
This commit is contained in:
@@ -0,0 +1,18 @@
|
||||
name: Supply Chain Policy
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
repository-policy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Verify immutable image references and secret hygiene
|
||||
run: make verify-supply-chain
|
||||
- name: Verify release process is documented
|
||||
run: test -s docs/SUPPLY-CHAIN.md
|
||||
+4
-2
@@ -1,6 +1,7 @@
|
||||
# Local-only dedicated-server build and verification image. Pin the Godot
|
||||
# release family used by project.godot; no image is pushed by this repository.
|
||||
FROM --platform=linux/amd64 barichello/godot-ci:4.7.1 AS project-imported
|
||||
# barichello/godot-ci:4.7.1 (linux/amd64), resolved 2026-08-29.
|
||||
FROM --platform=linux/amd64 barichello/godot-ci@sha256:622e5ca81b54cd8038ecf7de5d157b47efc800d7cf635af2eec18a6aee4bab7e AS project-imported
|
||||
WORKDIR /workspace
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends libfontconfig1 \
|
||||
@@ -28,7 +29,8 @@ RUN sed -i 's|^run/main_scene=.*$|run/main_scene="res://scenes/server_boot.tscn"
|
||||
&& mkdir -p /opt/cosmic-clash \
|
||||
&& godot --headless --path Game --export-release "Linux Dedicated Server" /opt/cosmic-clash/CosmicClashServer.x86_64
|
||||
|
||||
FROM --platform=linux/amd64 ubuntu:24.04 AS server
|
||||
# ubuntu:24.04 multi-architecture index, resolved 2026-08-29.
|
||||
FROM --platform=linux/amd64 ubuntu@sha256:571c2ab10651ab3a703fcfcb1b06545f5b53085872dcdf68bed17dd7ef4d72db AS server
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends libfontconfig1 libgl1 libstdc++6 && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=exporter /opt/cosmic-clash/ /opt/cosmic-clash/
|
||||
COPY deploy/cosmic-clash-server /opt/cosmic-clash/cosmic-clash-server
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
.PHONY: verify-phase6 verify-enet-integration verify-steam-templates
|
||||
.PHONY: verify-phase6 verify-enet-integration verify-steam-templates verify-supply-chain
|
||||
|
||||
verify-phase6:
|
||||
bash scripts/verify_phase6.sh
|
||||
@@ -8,3 +8,6 @@ verify-enet-integration:
|
||||
|
||||
verify-steam-templates:
|
||||
bash scripts/verify_steam_templates.sh
|
||||
|
||||
verify-supply-chain:
|
||||
python3 scripts/verify_supply_chain.py
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# Multiplayer artifact supply chain
|
||||
|
||||
Container references in the repository are immutable `@sha256:` digests. The
|
||||
base manifests may contain a zero digest only as a deployment template; a
|
||||
release overlay must replace it with a registry-resolved digest and run the
|
||||
checker with `--require-concrete`.
|
||||
|
||||
The release pipeline must, for every image and exported server artifact:
|
||||
|
||||
1. generate and retain an SBOM tied to the exact digest;
|
||||
2. scan OS and application dependencies and fail on a critical or disallowed
|
||||
vulnerability;
|
||||
3. sign the image and provenance with the offline release authority, and
|
||||
verify both at cluster admission; and
|
||||
4. publish the digest, SBOM, scan result, signature and provenance as one
|
||||
immutable release record.
|
||||
|
||||
Critical vulnerability fixes are triaged immediately and a patched release is
|
||||
cut within 24 hours of confirmation. A release with an unaccepted critical
|
||||
finding or unverifiable signature is not eligible for admission.
|
||||
|
||||
`python3 scripts/verify_supply_chain.py` is the dependency-free repository
|
||||
guard. Registry signing/scanning and admission require the release environment
|
||||
and are intentionally not simulated by this local check.
|
||||
+5
-2
@@ -62,8 +62,11 @@ product policy are in [`docs/MATCHMAKING.md`](docs/MATCHMAKING.md).
|
||||
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.
|
||||
- [ ] **IN PROGRESS:** Pin, scan, SBOM and sign artifacts; verify signatures at
|
||||
admission and document the critical vulnerability SLA. Repository image
|
||||
references are now digest-pinned with a static secret-hygiene guard and a
|
||||
24-hour critical-fix policy; registry execution and concrete release
|
||||
provenance remain.
|
||||
|
||||
## Phase 8 — queues, playlists and rating
|
||||
|
||||
|
||||
+1
-1
@@ -1185,7 +1185,7 @@ the local/CI/community transport, not a silent production fallback.
|
||||
| 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]` | **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]` | **IN PROGRESS.** Docker/Kubernetes references are digest-pinned, a dependency-free checker rejects mutable tags/plaintext credentials and concrete release overlays can reject template digests; release documentation defines SBOM, dependency/image scanning, signing, admission verification and a 24-hour critical-fix SLA | `scripts/verify_supply_chain.py`, `server/security/test_supply_chain.py`, `docs/SUPPLY-CHAIN.md` and `.github/workflows/supply-chain.yml` cover repository policy and provenance requirements; registry SBOM/scan/sign/admission execution and a concrete production overlay remain |
|
||||
|
||||
#### 8C — Queueing, matchmaking, playlists and rating
|
||||
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Reject mutable container references and checked-in credential values."""
|
||||
|
||||
from pathlib import Path
|
||||
import argparse
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
DIGEST = re.compile(r"^[^\s@]+@sha256:[0-9a-f]{64}$")
|
||||
FROM = re.compile(r"^\s*FROM(?:\s+--platform=\S+)?\s+(\S+)")
|
||||
IMAGE = re.compile(r"^\s*image:\s*(\S+)\s*$")
|
||||
SECRET_VALUE = re.compile(r"^\s*(?:password|token|private[-_ ]?key|publisher[-_ ]?key):\s*\S+", re.I)
|
||||
|
||||
|
||||
def check_text(path: Path, text: str, concrete: bool) -> list[str]:
|
||||
errors = []
|
||||
for line_number, line in enumerate(text.splitlines(), 1):
|
||||
from_match = FROM.match(line)
|
||||
image_match = IMAGE.match(line)
|
||||
reference = from_match.group(1) if from_match else image_match.group(1) if image_match else None
|
||||
if from_match and reference:
|
||||
reference = reference.split(" AS ", 1)[0].split(" as ", 1)[0]
|
||||
# A bare name in a later Docker stage is an internal stage alias, not
|
||||
# an independently fetched image and therefore needs no digest.
|
||||
internal_stage = bool(from_match and reference and "/" not in reference and "@" not in reference and ":" not in reference)
|
||||
if reference and not internal_stage and not DIGEST.fullmatch(reference):
|
||||
errors.append(f"{path}:{line_number}: image is not digest-pinned: {reference}")
|
||||
if concrete and reference and "@sha256:" in reference:
|
||||
digest = reference.rsplit("@sha256:", 1)[1]
|
||||
if set(digest) == {"0"}:
|
||||
errors.append(f"{path}:{line_number}: template digest is not a release artifact")
|
||||
if SECRET_VALUE.match(line):
|
||||
errors.append(f"{path}:{line_number}: possible plaintext credential")
|
||||
return errors
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--dockerfile", type=Path, default=Path("Dockerfile"))
|
||||
parser.add_argument("--manifest-dir", type=Path, default=Path("deploy/k8s"))
|
||||
parser.add_argument("--require-concrete", action="store_true")
|
||||
args = parser.parse_args()
|
||||
errors = check_text(args.dockerfile, args.dockerfile.read_text(), args.require_concrete)
|
||||
for path in sorted(args.manifest_dir.rglob("*.y*ml")):
|
||||
errors.extend(check_text(path, path.read_text(), args.require_concrete))
|
||||
for error in errors:
|
||||
print(error, file=sys.stderr)
|
||||
return 1 if errors else 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
@@ -0,0 +1,36 @@
|
||||
from pathlib import Path
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
|
||||
|
||||
ROOT = Path(__file__).parents[2]
|
||||
CHECKER = ROOT / "scripts" / "verify_supply_chain.py"
|
||||
|
||||
|
||||
class SupplyChainTest(unittest.TestCase):
|
||||
def run_checker(self, *args):
|
||||
return subprocess.run([sys.executable, str(CHECKER), *args], cwd=ROOT, text=True, capture_output=True)
|
||||
|
||||
def test_checked_in_references_are_digest_pinned(self):
|
||||
result = self.run_checker()
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
|
||||
def test_checker_rejects_tags_plaintext_secrets_and_template_release(self):
|
||||
with tempfile.TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
dockerfile = root / "Dockerfile"
|
||||
manifests = root / "manifests"
|
||||
manifests.mkdir()
|
||||
dockerfile.write_text("FROM example.invalid/game:latest\n")
|
||||
(manifests / "bad.yaml").write_text("image: example.invalid/game@sha256:" + "0" * 64 + "\npassword: leaked\n")
|
||||
result = self.run_checker("--dockerfile", str(dockerfile), "--manifest-dir", str(manifests), "--require-concrete")
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("not digest-pinned", result.stderr)
|
||||
self.assertIn("plaintext credential", result.stderr)
|
||||
self.assertIn("not a release artifact", result.stderr)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user