Files
jakman85andClaude Opus 4.8 748e9ca55d fix(oauth2-proxy): --allowed-groups → --allowed-group (correct oauth2-proxy v7.8.1 flag)
The plural --allowed-groups does not exist in oauth2-proxy v7.8.1; the shared
oauth2-proxy new pod and the crafty auth sidecar both crashlooped on 'unknown flag'.
The flag is the singular, repeatable --allowed-group.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-28 15:57:55 -05:00

190 lines
9.8 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.10.8
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
memory: 1Gi
# Minecraft JVMs are memory-hungry; cap generously for a starter set of small servers.
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.home.keithjackson.dev/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.home.keithjackson.dev/oauth2/callback
- --cookie-domain=.home.keithjackson.dev
- --whitelist-domain=.home.keithjackson.dev
- --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