Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ jobs:
# request brings in, with rules kept in the repository. The image is pinned
# by digest; it marks every directory git-safe itself, so the runner's uid
# owning the checkout is not a problem.
# --verbose prints each finding's file, line, rule and commit (the value stays
# REDACTED), so a red run says where to look; accepted false positives are
# listed by fingerprint in .gitleaksignore.
gitleaks:
runs-on: ubuntu-latest
timeout-minutes: 10
Expand All @@ -231,7 +234,7 @@ jobs:
run: >-
docker run --rm -v "$PWD":/repo:ro
zricethezav/gitleaks:v8.30.1@sha256:c00b6bd0aeb3071cbcb79009cb16a60dd9e0a7c60e2be9ab65d25e6bc8abbb7f
git --redact --no-banner --exit-code 1 /repo
git --redact --verbose --no-banner --exit-code 1 /repo

# The third-party images the manifests and docker-compose run (NATS, Redis,
# Postgres, monitoring, tracing). The image scan above covers only the
Expand Down Expand Up @@ -310,6 +313,9 @@ jobs:
curl -fsSLo kubeconform.tar.gz "https://github.com/yannh/kubeconform/releases/download/${KUBECONFORM_VERSION}/kubeconform-linux-amd64.tar.gz"
echo "${KUBECONFORM_SHA256} kubeconform.tar.gz" | sha256sum -c -
tar xzf kubeconform.tar.gz kubeconform
# The local overlay reads its secrets from a gitignored file; generate
# throwaway values so it renders here too.
scripts/k8s/local-secrets.sh
# Against the Kubernetes version k3s runs, overlay by overlay.
for overlay in local prod; do
echo "== overlays/${overlay}"
Expand Down
8 changes: 8 additions & 0 deletions .gitleaksignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Accepted gitleaks findings, by fingerprint (commit:file:rule:line). gitleaks
# scans every commit, so a false positive in history stays until it is listed
# here; rewriting pushed history is not an option.
#
# Test fixture, not a secret: strongSecret in pkg/config/config_test.go was the
# literal "0123456789abcdef" repeated four times, which the generic-api-key rule
# flags for its name and entropy. The test now builds the value at run time.
c255f6a685bc737b0be8ab8b85892314d7fd32e3:pkg/config/config_test.go:generic-api-key:103
7 changes: 5 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
.PHONY: help build test test-integration lint cover up down k8s-build k8s-context k8s-apply k8s-delete k6-leaderboard k6-matchmaking k6-websocket
.PHONY: help build test test-integration lint cover up down k8s-build k8s-secrets k8s-context k8s-apply k8s-delete k6-leaderboard k6-matchmaking k6-websocket

# Every directory under cmd/, as CI's image matrix: the hand-written list had
# lost outbox-relay, so k8s-build skipped an image the cluster needs.
Expand Down Expand Up @@ -48,7 +48,10 @@ k8s-context:
@test "$$(kubectl config current-context)" = "$(K3D_CONTEXT)" || \
{ echo "kubectl context is '$$(kubectl config current-context)', not $(K3D_CONTEXT); refusing" >&2; exit 1; }

k8s-apply: k8s-context ## Deploy the local overlay to the k3d cluster
k8s-secrets: ## Generate the local overlay's secrets (.env.local, gitignored)
@scripts/k8s/local-secrets.sh

k8s-apply: k8s-context k8s-secrets ## Deploy the local overlay to the k3d cluster
kubectl apply -k deployments/k8s/overlays/local

k8s-delete: k8s-context ## Tear down the local overlay
Expand Down
5 changes: 5 additions & 0 deletions cmd/gateway/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedJWT, config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/leaderboard/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/matchmaking/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/outbox-relay/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedPostgres); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/player/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedJWT, config.NeedPostgres, config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/presence/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
5 changes: 5 additions & 0 deletions cmd/websocket/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,11 @@ func run() int {
Thereafter: cfg.LogSampleThereafter,
})
defer func() { _ = log.Sync() }()
// In production, refuse to start with a secret that is unset, short or a
// known default rather than run with one anyone can read in the repository.
if err := cfg.Validate(config.NeedJWT, config.NeedRedis); err != nil {
log.Fatal("refusing to start with weak secrets", zap.Error(err))
}

shutdownTracing := tracing.MustInit(context.Background(), tracing.Config{
Enabled: cfg.OTelEnabled,
Expand Down
14 changes: 13 additions & 1 deletion deployments/k8s/base/redis.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,19 @@ spec:
image: redis:7.4.11-alpine@sha256:858f009f9709ce576febc734aa78b8f6d624b82571f9ddb6bda4377c833b3499
# AOF: every write is appended and fsynced once a second, so a restart
# loses at most about a second of writes instead of everything.
args: ["redis-server", "--appendonly", "yes"]
# requirepass: without it any pod in the cluster read and changed
# every session, queue and leaderboard (measured from the gateway
# pod: PING +PONG, DBSIZE answered). The value comes from the Secret;
# the services already send it (REDIS_PASSWORD through envFrom).
args: ["redis-server", "--appendonly", "yes", "--requirepass", "$(REDIS_PASSWORD)"]
env:
- name: REDIS_PASSWORD
valueFrom:
secretKeyRef: { name: gamemesh-secrets, key: REDIS_PASSWORD }
# redis-cli in the probes authenticates with it.
- name: REDISCLI_AUTH
valueFrom:
secretKeyRef: { name: gamemesh-secrets, key: REDIS_PASSWORD }
ports:
- containerPort: 6379
startupProbe:
Expand Down
5 changes: 4 additions & 1 deletion deployments/k8s/components/monitoring/monitoring.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -234,8 +234,11 @@ spec:
env:
- name: GF_SECURITY_ADMIN_USER
value: admin
# From the overlay's Secret, not written here: with value admin
# anyone who reached Grafana logged in as admin with admin/admin.
- name: GF_SECURITY_ADMIN_PASSWORD
value: admin
valueFrom:
secretKeyRef: { name: gamemesh-secrets, key: GRAFANA_ADMIN_PASSWORD }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
Expand Down
9 changes: 6 additions & 3 deletions deployments/k8s/overlays/local/kustomization.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# The local k3d cluster: images built and imported on this machine, dev
# secrets, monitoring included, the base's replica counts (two websocket
# The local k3d cluster: images built and imported on this machine, secrets
# generated on this machine, monitoring included, the base's replica counts (two websocket
# replicas exercise the event fan-out).
#
# kubectl apply -k deployments/k8s/overlays/local
Expand Down Expand Up @@ -30,10 +30,13 @@ images:
newTag: dev
- name: gamemesh/presence
newTag: dev
# Values from .env.local, which is gitignored and written by
# make k8s-secrets (random, openssl rand -hex 32): no secret value is in the
# repository, not even a development one. A render fails until it exists.
secretGenerator:
- name: gamemesh-secrets
envs:
- secrets.env
- .env.local
patches:
# Traefik's pods are the gateway's only direct clients; k3s's default pod
# CIDR. Without it the gateway takes Traefik's address for the client's and
Expand Down
8 changes: 0 additions & 8 deletions deployments/k8s/overlays/local/secrets.env

This file was deleted.

13 changes: 9 additions & 4 deletions deployments/k8s/overlays/prod/kustomization.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,15 @@
#
# kubectl apply -k deployments/k8s/overlays/prod (only from CI, step D)
#
# No Secret is generated here: the cluster must already hold gamemesh-secrets
# (JWT_SECRET, POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB), created there
# with values from openssl rand -hex 32 (A9). Monitoring stays out (finding
# #68).
# No Secret is generated here: the cluster must already hold gamemesh-secrets,
# created there and never committed (docs/deployment.md, "Production
# secrets"), with these keys:
# JWT_SECRET, POSTGRES_PASSWORD, REDIS_PASSWORD openssl rand -hex 32 each
# POSTGRES_USER, POSTGRES_DB gamemesh
# The services refuse to start in production when one they use is missing,
# shorter than 32 characters or a known default (config.Validate), so a
# missing or weak Secret stalls the rollout instead of running. Monitoring
# stays out (finding #68).
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: gamemesh
Expand Down
36 changes: 32 additions & 4 deletions docs/deployment-phase1-plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -1039,6 +1039,34 @@ Her adım şu yapıda:
pod (ele geçirilmiş biri de) Redis'e tam erişir. Compose'daki gibi
(`redis-server --requirepass`, A2a) şifre prod Secret'ından gelmeli; servisler
`REDIS_PASSWORD`'ü zaten okuyor.
- **Kararlar (2026-10-19):** local secret'lar repo dışında ve rastgele (`make k8s-secrets`);
fail fast servis başına ihtiyaçla (`Validate(NeedJWT, NeedPostgres, NeedRedis)`); k8s'te
Redis `requirepass`; Grafana şifresi Secret'tan; prod Secret'ının cluster'da oluşturulması
B/D'de (değerler kullanıcının makinesinde `openssl` ile, hiçbir zaman bana ya da loglara
gelmeden).
- **Yapılanlar (`feat/production-secrets`, 2026-10-19), CI kanıtı bekleniyor:**
1. `overlays/local/secrets.env` (commit'li dev değerleri) kalktı. Yerine gitignore'daki
`.env.local`, `scripts/k8s/local-secrets.sh` ile: eksik her anahtar `openssl rand -hex
32` (64 karakter), izin 600, değer hiç basılmıyor, var olanlar korunuyor. Dosya yoksa
render hata veriyor; CI `manifests` job'u atılacak değerler üretiyor. k3d: yeni
Postgres şifresi eski volume'la `password authentication failed` (28P01) verdi, yeni
pod'lar takıldı, eski pod'lar trafiği almaya devam etti; volume yenilenince 19/19.
2. Redis `--requirepass` (#56): önce gateway pod'undan şifresiz `PING` → `+PONG`,
`DBSIZE` yanıt verdi; sonra `-NOAUTH`, yanlış şifre `-WRONGPASS`; kayıt, oturum ve
kuyruk çalışıyor. Şifre süreç listesinde görünmüyor (Redis başlığı `redis-server
*:6379` olarak yeniden yazıyor).
3. Grafana (#67): önce `admin/admin` 200; sonra 401, üretilen şifre 200.
4. Fail fast (#40): `Config.Validate`; production'da eksik, 32 karakterden kısa ya da
bilinen varsayılan (koddaki ve `.env.example`'daki JWT) değerleri, ayarın adıyla ve
değeri yazmadan reddediyor. gateway imajı: varsayılanlarla iki hatayı birlikte
raporlayıp çıkıyor, güçlü değerlerle açılıyor. k3d: zayıf JWT ile gateway deploy'u →
yeni pod `CrashLoopBackOff`, eski 2 pod `1/1`, `/health` 200, rollout `undo` ile geri.
5. Prod Secret sözleşmesi prod overlay'inde ve `docs/deployment.md`'de: `JWT_SECRET`,
`POSTGRES_PASSWORD`, `REDIS_PASSWORD` (her biri `openssl rand -hex 32`),
`POSTGRES_USER`, `POSTGRES_DB`.
- Not: eski dev değerleri git geçmişinde duruyor; hiçbir zaman local dışında kullanılmadı.
- **Bitti sayılır:** `git grep -i jwt_secret deployments/` sadece anahtarın adını veren bir
yorum satırı buluyor; `deployments/` altında hiçbir dosya bir secret'a değer atamıyor.

### A10. Prova: yerel k3s (k3d) üzerinde prod'a en yakın deploy

Expand Down Expand Up @@ -1467,8 +1495,8 @@ manifest okunarak tespit edildi, henüz çalışan bir sistemde test edilmedi.
| 36 | Toplam requests 1800m CPU, 2 vCPU'ya sığmıyor | M5 | Ölçüldü (hesap) | A8 (prod 10 pod, 950m/960Mi), B1 |
| 37 | ConfigMap değişikliği pod'ları yeniden başlatmıyor | M5 | Ölçüldü (Docker analog) | A8 (çözüldü: `configMapGenerator`, hash'li ad) |
| 38 | Ingress nginx'e özel; k3s'te sessizce yok sayılır | M5 | Kod | A8 (çözüldü: `traefik` sınıfı, k3d'de `/health` 200), C4 |
| 39 | Secret'lar repoda (`01-secrets.yaml`) | M5, M8 | Kod | A9 |
| 40 | **Uygulama production'da güvensiz secret ile sessizce açılıyor** | M8 | Kod | A9 |
| 39 | Secret'lar repoda (`01-secrets.yaml`) | M5, M8 | Kod | A9 (çözüldü: local `.env.local` gitignore'da ve rastgele; prod Secret repoda yok) |
| 40 | **Uygulama production'da güvensiz secret ile sessizce açılıyor** | M8 | Kod | A9 (çözüldü: `Config.Validate`, k3d'de zayıf JWT ile rollout takıldı) |
| 41 | Otomatik secret taraması yok | M8 | Kod | A9, A5a (gitleaks; GitHub secret scanning ve push protection zaten açıktı, API ile doğrulandı) |
| 42 | `kubeconform` anlamsal hataları yakalayamıyor | M5, M7 | Kod | A10 |
| 43 | Docker/k3s `ufw`'yi atlıyor | M2 | Bilgi | B2 |
Expand All @@ -1484,7 +1512,7 @@ manifest okunarak tespit edildi, henüz çalışan bir sistemde test edilmedi.
| 53 | `ReverseProxy.Director` Go 1.26'da deprecated; `Rewrite`'a geçiş XFF ve `Host` davranışını değiştiriyor | M3 | Ölçüldü (staticcheck SA1019) | `fix/gateway-trusted-proxies` (çözüldü) |
| 54 | GO-2026-6505 (otel otlptrace v1.44.0, erişilebilir); Trivy kırmızıyken govulncheck adımı hiç çalışmadığı için görünmüyordu | M7 | Ölçüldü (CI run 38416862319, yerel govulncheck) | `fix/deps-x-net` (çözüldü), maskeleme A5a (çözüldü: ayrı job'lar) |
| 55 | Presence (8086) ve websocket (8084) compose'da `0.0.0.0`'a yayınlı; presence kimliksiz bir iç API (LAN'dan herhangi bir oyuncunun durumu değiştirilebilir) | M2 | Ölçüldü (token'sız `POST /presence/connect` → 200) | A2a (çözüldü) |
| 56 | k8s'teki Redis şifresiz; NetworkPolicy olmadan cluster'daki her pod erişebiliyor | M5, M8 | Kod | A9 |
| 56 | k8s'teki Redis şifresiz; NetworkPolicy olmadan cluster'daki her pod erişebiliyor | M5, M8 | Kod | A9 (çözüldü: `requirepass`, şifresiz erişim `NOAUTH`) |
| 57 | Postgres DSN'inde `connect_timeout`/`statement_timeout` yok; ağ bölünmesinde sorgular ve açılış asılı kalabilir | M4 | Kod | A3a (çözüldü: 75 sn → 5 sn, açılışta retry) |
| 58 | Docker Desktop kapanırken container'ları "elle durduruldu" işaretliyor; `unless-stopped` stack'i geri getirmiyor (A2a iddiasının düzeltmesi) | M2 | Ölçüldü (`HasBeenManuallyStopped: true`, 14/14) | Karar: `restart: always` (Docker Desktop yeniden açılışında doğrulanacak) |
| 59 | Açık WS bağlantısı JWT'nin süresi dolduktan sonra da açık kalıyor (token sadece el sıkışmada kontrol ediliyordu) | M8 | Kod, test ile doğrulandı | A3b (çözüldü: tarama `1008 "token expired"`) |
Expand All @@ -1495,7 +1523,7 @@ manifest okunarak tespit edildi, henüz çalışan bir sistemde test edilmedi.
| 64 | Event bus bağlantısında açılış retry'ı yok; bağımlılık geç kalkınca servis `fatal` verip yeniden başlıyor | M4 | Ölçüldü (k3d: websocket 2 restart, `event bridge failed`) | A7 (çözüldü: 30 sn'lik bağlantı retry'ı; k3d'de 0 restart) |
| 65 | Tracing kapanışı sınırsız (`shutdownTracing(context.Background())`, 7 servis); collector yokken her pod kapanışı ~18 sn uzuyor | M4 | Ölçüldü (k3d: ~24 sn → `OTEL_ENABLED=false` ile ~6 sn) | PR #44 (çözüldü: 10,01 → 3,00 sn; k3d'de 24 → 9 sn) |
| 66 | Üçüncü taraf imajlar (NATS, Redis, Postgres, monitoring, tracing) hiç taranmıyordu; yerelde 7'den 6'sı düşüyor, ikisi EOL Alpine üzerinde | M7 | Ölçüldü (Trivy 0.70.0, yerel) | A7-2 (prod imajları bloklayan, `.trivyignore.yaml` süreli) |
| 67 | Grafana admin şifresi manifest'te düz metin (`admin`/`admin`) | M8 | Kod | A9 |
| 67 | Grafana admin şifresi manifest'te düz metin (`admin`/`admin`) | M8 | Kod | A9 (çözüldü: Secret'tan; `admin/admin` 401) |
| 68 | Monitoring ve tracing imajları major sürüm geride (Prometheus v2.54.1, Grafana 11.2.0, Jaeger 1.62.0, collector 0.111.0); CI'da rapor modunda | M7 | Ölçüldü (#66 taraması) | Faz 1 sonrası (minor'lar 2026-10-19'da yükseltildi: Grafana 11.6.16 ve Jaeger 1.76 artık EOL Alpine üzerinde değil, 141→74 / 76→73; major'lar bekliyor); A8'de monitoring prod overlay'inin dışında |
| 69 | Dependabot k8s ve compose imajlarını ayrı PR'larda güncelliyor; "ortak imajlar aynı olmalı" kontrolü zorunlu `ci-ok` altında her PR'ı tek başına kırmızı yapıyor, hiçbiri merge edilemiyor (#46'nın tasarım hatası) | M7 | Ölçüldü (#48, #50, #52, #53: `lint` fail, `grafana differs`) | `ci/dependabot-image-group` (multi-ecosystem group) |
| 70 | CI job'larında `timeout-minutes` yok (varsayılan 360 dk); asılı bir job `main` kuyruğunu ve deploy'u saatlerce bekletebilir | M7 | Ölçüldü (`main`'de websocket build'i 785 sn, PR'da aynı kod 103/105 sn) | `ci/job-timeouts` |
Expand Down
Loading
Loading