221 lines
13 KiB
YAML
221 lines
13 KiB
YAML
---
|
|
# Crafty Controller pod = TWO containers:
|
|
# 1. crafty — the panel + every Minecraft server it creates (Java + Bedrock) run as
|
|
# SUBPROCESSES inside THIS container. Serves its web UI over HTTPS (self-signed)
|
|
# on :8443. Never exposed directly — only the sidecar and the game-port LB reach it.
|
|
# 2. oauth2-proxy — a per-pod REVERSE-PROXY (Option B, platform/oauth2-proxy/README.md). The shared
|
|
# Gateway routes crafty.home.keithjackson.dev -> this sidecar (:4180); the sidecar
|
|
# authenticates the browser against Keycloak (homelab realm, group platform-admins)
|
|
# and only THEN proxies to the co-located Crafty UI at https://127.0.0.1:8443. This
|
|
# is how Crafty's UI (which has NO native OIDC) is Keycloak-gated without ext_authz
|
|
# on the shared Gateway (impossible on Cilium 1.16.5 — see the oauth2-proxy README).
|
|
#
|
|
# Rendered by its OWN non-gating post-platform Flux Kustomization (clusters/base/crafty.yaml,
|
|
# dependsOn platform-config, wait:false) — a leaf UI must NEVER sit in the gating `platform` tier
|
|
# (DECISIONS-LOG: "Headlamp gating stall").
|
|
apiVersion: apps/v1
|
|
kind: Deployment
|
|
metadata:
|
|
name: crafty
|
|
namespace: crafty
|
|
labels:
|
|
app.kubernetes.io/name: crafty
|
|
spec:
|
|
replicas: 1
|
|
# Crafty owns a single RWO Longhorn volume with live SQLite + running server subprocesses; a second
|
|
# replica would double-mount/corrupt it. Recreate (not RollingUpdate) so the old pod releases the RWO
|
|
# PVC before the new one attaches.
|
|
strategy:
|
|
type: Recreate
|
|
revisionHistoryLimit: 3
|
|
selector:
|
|
matchLabels:
|
|
app.kubernetes.io/name: crafty
|
|
template:
|
|
metadata:
|
|
labels:
|
|
app.kubernetes.io/name: crafty
|
|
spec:
|
|
automountServiceAccountToken: false
|
|
# ── IDENTITY / STORAGE PERMISSIONS (the seerr fsGroup lesson, applied) ───────────────────────
|
|
# The Crafty image runs as the non-root user `crafty` (UID 1000) whose PRIMARY GROUP is root
|
|
# (GID 0): its Dockerfile does `useradd -g root -M crafty` + `chown -R crafty:root /crafty` and
|
|
# makes /crafty group-writable — the OpenShift/K8s "arbitrary-UID, root-group-perms" pattern the
|
|
# Crafty docs call out. A fresh Longhorn PVC is root:root and NOT group-writable, so the crafty
|
|
# process (GID 0) could not create /crafty/servers/* etc. -> CrashLoop (same failure seerr hit as
|
|
# UID 1000 on a root:root PVC). fsGroup=0 makes the kubelet group-own the mounted volume to GID 0,
|
|
# mark it group-writable + SGID, and add GID 0 as a supplemental group, so the crafty user can
|
|
# write — Longhorn's CSIDriver fsGroupPolicy: ReadWriteOnceWithFSType applies fsGroup to this RWO
|
|
# filesystem volume. runAsUser/runAsGroup pin the image's own crafty:root identity; runAsNonRoot
|
|
# asserts it. (HUMAN runtime check at bring-up: confirm the pod writes the DB and starts a server;
|
|
# if the image's crafty UID is not 1000, adjust runAsUser — see README.)
|
|
securityContext:
|
|
runAsNonRoot: true
|
|
runAsUser: 1000
|
|
runAsGroup: 0
|
|
fsGroup: 0
|
|
seccompProfile:
|
|
type: RuntimeDefault
|
|
containers:
|
|
# ─────────────────────────────── Crafty panel + MC servers ──────────────────────────────────
|
|
- name: crafty
|
|
# registry.gitlab.com/crafty-controller/crafty-4 — official image. Pinned to the current
|
|
# stable release (4.10.8, 2026-07-29; tags are bare semver, NO `v` prefix). Renovate maintains.
|
|
image: registry.gitlab.com/crafty-controller/crafty-4:4.11.0
|
|
imagePullPolicy: IfNotPresent
|
|
env:
|
|
- name: TZ
|
|
value: "America/Chicago"
|
|
ports:
|
|
- { name: https-ui, containerPort: 8443, protocol: TCP }
|
|
# Starter game ports the operator assigns to servers created in the panel. Declared here for
|
|
# documentation + to back the game-port LoadBalancer (service-l4.yaml). Add more as needed.
|
|
- { name: mc-java-0, containerPort: 25565, protocol: TCP }
|
|
- { name: mc-java-1, containerPort: 25566, protocol: TCP }
|
|
- { name: mc-java-2, containerPort: 25567, protocol: TCP }
|
|
- { name: mc-bedr-0, containerPort: 19132, protocol: UDP }
|
|
- { name: mc-bedr-1, containerPort: 19133, protocol: UDP }
|
|
- { name: mc-bedr-2, containerPort: 19134, protocol: UDP }
|
|
resources:
|
|
requests:
|
|
cpu: 250m
|
|
# Raised 1Gi -> 3Gi when the first Minecraft server ("homelab", Paper 26.2) was created:
|
|
# that JVM runs with -Xms2000M -Xmx2000M, so the container's steady-state floor is now the
|
|
# panel (~100Mi) PLUS a permanently-committed 2 GiB heap plus JVM overhead. Measured
|
|
# cgroup memory.current with the server idle-but-running: 2.86Gi. A 1Gi request understated
|
|
# that by ~3x, which both misleads the scheduler and makes this Burstable pod an early
|
|
# eviction candidate under node memory pressure (kubelet ranks by usage-over-request).
|
|
memory: 3Gi
|
|
# Minecraft JVMs are memory-hungry; cap generously for a starter set of small servers.
|
|
# Headroom check for the 4Gi cap: 2 GiB heap + ~0.5 GiB JVM non-heap (metaspace, code cache,
|
|
# GC structures, thread stacks, mmap'd region files) + ~0.1 GiB panel = ~2.9Gi observed,
|
|
# leaving ~1.2Gi. A 3 GiB heap would have landed near 4Gi and risked an OOMKill, which is
|
|
# why the server was sized at a 2 GiB heap rather than larger. Raise BOTH this limit and the
|
|
# server's own memory in Crafty together if a bigger heap is ever wanted.
|
|
limits:
|
|
memory: 4Gi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
# readOnlyRootFilesystem intentionally UNSET: Crafty's launcher writes runtime state under
|
|
# /crafty (app cache, per-server working dirs) beyond the mounted subPaths; hardening it
|
|
# needs writable emptyDirs and is a deferred, runtime-verified follow-up (as headlamp notes).
|
|
volumeMounts:
|
|
# ONE Longhorn PVC mounted five times by subPath — exactly the dirs Crafty's docker-compose
|
|
# maps. Everything else under /crafty is read-only image content.
|
|
- { name: crafty-data, mountPath: /crafty/servers, subPath: servers }
|
|
- { name: crafty-data, mountPath: /crafty/backups, subPath: backups }
|
|
- { name: crafty-data, mountPath: /crafty/logs, subPath: logs }
|
|
- { name: crafty-data, mountPath: /crafty/import, subPath: import }
|
|
- { name: crafty-data, mountPath: /crafty/app/config, subPath: config }
|
|
# Crafty inits its SQLite DB + Tornado server on first boot (can take ~30-60s). TCP-probe the
|
|
# HTTPS UI port (an HTTPS GET would fight the self-signed cert). Generous startup budget.
|
|
startupProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 10
|
|
failureThreshold: 30
|
|
livenessProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 20
|
|
failureThreshold: 6
|
|
readinessProbe:
|
|
tcpSocket:
|
|
port: https-ui
|
|
periodSeconds: 15
|
|
# ─────────────────────────── oauth2-proxy reverse-proxy sidecar ─────────────────────────────
|
|
- name: oauth2-proxy
|
|
# quay.io/oauth2-proxy/oauth2-proxy — same authenticator family as platform/oauth2-proxy, but
|
|
# here in per-app REVERSE-PROXY mode (--upstream is the co-located Crafty UI, not static://202).
|
|
image: quay.io/oauth2-proxy/oauth2-proxy:v7.8.1
|
|
imagePullPolicy: IfNotPresent
|
|
args:
|
|
- --http-address=0.0.0.0:4180
|
|
- --reverse-proxy=true
|
|
- --provider=oidc
|
|
- --oidc-issuer-url=https://keycloak.${BASE_DOMAIN}/realms/homelab
|
|
# Gate the co-located Crafty UI. Crafty serves HTTPS with a SELF-SIGNED cert, so skip verify
|
|
# on this in-pod (127.0.0.1) hop; the browser-facing TLS is the trusted wildcard at the Gateway.
|
|
- --upstream=https://127.0.0.1:8443
|
|
- --ssl-upstream-insecure-skip-verify=true
|
|
- --email-domain=*
|
|
- --scope=openid email profile
|
|
- --oidc-groups-claim=groups
|
|
# R-IDP-06: a valid Keycloak session is NOT enough — the user must ALSO be in platform-admins,
|
|
# else /oauth2 returns 403. (Matches the platform oauth2-proxy floor.)
|
|
- --allowed-group=platform-admins
|
|
# Single provider => 302 straight to Keycloak, no oauth2-proxy interstitial button.
|
|
- --skip-provider-button=true
|
|
- --redirect-url=https://crafty.${BASE_DOMAIN}/oauth2/callback
|
|
# ── COOKIE ISOLATION (bug fix 2026-09-29) ────────────────────────────────────────────
|
|
# This sidecar and the SHARED forward-auth oauth2-proxy (platform/oauth2-proxy) are
|
|
# INDEPENDENT instances holding DIFFERENT cookie-secrets (verified in-cluster: the two
|
|
# `cookie-secret` values differ, so neither can decrypt the other's cookie). Both used
|
|
# to emit oauth2-proxy's DEFAULT cookie names (`_oauth2_proxy` and `_oauth2_proxy_csrf`)
|
|
# scoped to `.${BASE_DOMAIN}`, so the browser kept ONE pair of cookies for BOTH proxies:
|
|
# whichever authenticated last overwrote the other's session, and the loser then logged
|
|
# "No valid authentication in request" and bounced to Keycloak -- which, thanks to the
|
|
# live Keycloak SSO session, silently redirected back and overwrote the cookie again.
|
|
# That mutual eviction presents EXACTLY as the reported symptom: "I signed in to
|
|
# Keycloak but I am still not signed in to the app."
|
|
# Two independent fixes, both applied here (only this side changes -- renaming the
|
|
# SHARED proxy's cookie would sign every other gated app out):
|
|
# 1. --cookie-name: a unique name, so the two sessions cannot alias at all.
|
|
# 2. --cookie-domain: HOST-scoped. A per-app reverse proxy has no reason to broadcast
|
|
# its session to every other host in the zone; the shared proxy keeps the wide
|
|
# scope because cross-host forward-auth is precisely its job.
|
|
# --whitelist-domain deliberately stays zone-wide: it governs permitted post-login
|
|
# REDIRECT targets, not cookie scope.
|
|
- --cookie-name=_oauth2_proxy_crafty
|
|
- --cookie-domain=crafty.${BASE_DOMAIN}
|
|
- --whitelist-domain=.${BASE_DOMAIN}
|
|
- --cookie-secure=true
|
|
- --pass-access-token=true
|
|
- --set-xauthrequest=true
|
|
env:
|
|
# client-id / client-secret MUST match the Keycloak `crafty` client
|
|
# (keycloak-realm-clients secret, crafty-client-secret). cookie-secret is a random 32-byte
|
|
# value. All three from the SOPS-encrypted crafty-oauth2-proxy Secret, by name only (R-IDP-04).
|
|
- name: OAUTH2_PROXY_CLIENT_ID
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: client-id }
|
|
- name: OAUTH2_PROXY_CLIENT_SECRET
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: client-secret }
|
|
- name: OAUTH2_PROXY_COOKIE_SECRET
|
|
valueFrom:
|
|
secretKeyRef: { name: crafty-oauth2-proxy, key: cookie-secret }
|
|
ports:
|
|
- { name: http, containerPort: 4180, protocol: TCP }
|
|
resources:
|
|
requests:
|
|
cpu: 25m
|
|
memory: 64Mi
|
|
limits:
|
|
memory: 128Mi
|
|
securityContext:
|
|
allowPrivilegeEscalation: false
|
|
readOnlyRootFilesystem: true
|
|
capabilities:
|
|
drop:
|
|
- ALL
|
|
livenessProbe:
|
|
httpGet:
|
|
path: /ping
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 15
|
|
readinessProbe:
|
|
httpGet:
|
|
path: /ping
|
|
port: http
|
|
initialDelaySeconds: 5
|
|
periodSeconds: 15
|
|
volumes:
|
|
- name: crafty-data
|
|
persistentVolumeClaim:
|
|
claimName: crafty-data
|