# Deployment + Service — Keycloak (namespace: sso) # # Prerequisites (apply in order): # 1. pvc.yaml — keycloak-data PVC # 2. middleware.yaml — Traefik middlewares # 3. create-secrets.sh — keycloak-config Secret (KC_DB_URL, KC_DB_PASSWORD, # KC_BOOTSTRAP_ADMIN_PASSWORD) # 4. Edit PROVIDER_JAR_URL in the init container below (CP-NK-005 — see CONFIG.md) # 5. This file # # After first pod starts successfully: # 6. bootstrap-realm.sh — configure master realm, create net-kingdom realm # # privacyIDEA Keycloak Provider (init container): # The init container downloads the provider JAR to /opt/keycloak/providers/ # before Keycloak starts. Keycloak detects providers and rebuilds automatically # (no --optimized flag). Build output is cached in the keycloak-data PVC so # subsequent restarts skip the full rebuild. # # For production, prefer building a custom image with the provider pre-baked: # FROM quay.io/keycloak/keycloak:VERSION # COPY keycloak-provider-VERSION.jar /opt/keycloak/providers/ # RUN /opt/keycloak/bin/kc.sh build # A custom image avoids the internet dependency and ensures a reproducible build. # See README.md "Custom image" section for details. # # Container ports: # 8080 — HTTP (Traefik ingress; TLS terminated at Traefik) # 9000 — Management (health/live, health/ready, health/started — kubelet only) apiVersion: apps/v1 kind: Deployment metadata: name: keycloak namespace: sso labels: app.kubernetes.io/name: keycloak app.kubernetes.io/part-of: net-kingdom-sso-mfa net-kingdom/component: sso spec: replicas: 1 selector: matchLabels: app.kubernetes.io/name: keycloak strategy: type: Recreate # single replica — avoid two pods racing on the build-cache PVC template: metadata: labels: app.kubernetes.io/name: keycloak app.kubernetes.io/part-of: net-kingdom-sso-mfa net-kingdom/component: sso spec: securityContext: runAsNonRoot: true runAsUser: 1000 # keycloak user inside the official image fsGroup: 1000 # ── Init: download privacyIDEA Keycloak Provider JAR ───────────────── # The JAR is placed in /opt/keycloak/providers/ via an emptyDir volume. # Keycloak detects the new provider on startup and rebuilds automatically. # # NOTE: This requires outbound HTTPS from the cluster to GitHub. # If your cluster has no egress internet access, pre-stage the JAR in an # internal registry or use a custom Keycloak image (see comment above). initContainers: - name: install-privacyidea-provider # Pin curl image version alongside the Keycloak image. image: curlimages/curl:8.10.1 securityContext: runAsNonRoot: true runAsUser: 65534 # nobody — curl image default env: - name: PROVIDER_JAR_URL # CP-NK-005 — EDIT this value before applying. # Find the correct release at: # https://github.com/privacyIDEA/keycloak-provider/releases # Choose a version compatible with your Keycloak image version above. # Example: # https://github.com/privacyIDEA/keycloak-provider/releases/download/v0.9/keycloak-provider-0.9.jar value: "EDIT_BEFORE_APPLY" command: - sh - -c - | if [ "$PROVIDER_JAR_URL" = "EDIT_BEFORE_APPLY" ]; then echo "ERROR: PROVIDER_JAR_URL not set." >&2 echo "Edit deployment.yaml and replace PROVIDER_JAR_URL with the real JAR URL." >&2 echo "See CONFIG.md CP-NK-005 and README.md." >&2 exit 1 fi echo "Downloading privacyIDEA Keycloak Provider from: $PROVIDER_JAR_URL" curl -fsSL -o /providers/keycloak-provider.jar "$PROVIDER_JAR_URL" BYTES=$(wc -c < /providers/keycloak-provider.jar) echo "Downloaded: ${BYTES} bytes -> /providers/keycloak-provider.jar" volumeMounts: - name: providers mountPath: /providers containers: - name: keycloak # Pin to a specific release; update via image update policy. # Check https://quay.io/repository/keycloak/keycloak for latest stable. image: quay.io/keycloak/keycloak:26.0 imagePullPolicy: IfNotPresent # kc.sh start — no --optimized flag so Keycloak rebuilds when providers change. # After the first successful start, subsequent starts use the cached build # in the keycloak-data PVC and restart within ~30 seconds. args: ["start"] ports: - name: http containerPort: 8080 protocol: TCP - name: management containerPort: 9000 protocol: TCP # ── Environment — sensitive values from Secret ────────────────── env: # Database - name: KC_DB value: postgres - name: KC_DB_URL valueFrom: secretKeyRef: name: keycloak-config key: KC_DB_URL - name: KC_DB_USERNAME value: keycloak - name: KC_DB_PASSWORD valueFrom: secretKeyRef: name: keycloak-config key: KC_DB_PASSWORD # Bootstrap admin (used only on first start to create the admin user) - name: KC_BOOTSTRAP_ADMIN_USERNAME value: admin - name: KC_BOOTSTRAP_ADMIN_PASSWORD valueFrom: secretKeyRef: name: keycloak-config key: KC_BOOTSTRAP_ADMIN_PASSWORD # Hostname & proxy # CP-NK-004 — update CONFIG.md if you change this value. - name: KC_HOSTNAME value: kc.coulomb.social # Traefik passes X-Forwarded-For and X-Forwarded-Proto headers. - name: KC_PROXY_HEADERS value: xforwarded # TLS is terminated at Traefik; Keycloak serves HTTP inside the cluster. - name: KC_HTTP_ENABLED value: "true" # Observability - name: KC_HEALTH_ENABLED value: "true" - name: KC_METRICS_ENABLED value: "true" - name: KC_LOG_LEVEL value: INFO # Caching — local = in-JVM Infinispan; switch to ispn for multi-replica HA. - name: KC_CACHE value: local # ── Volume mounts ─────────────────────────────────────────────── volumeMounts: # providers emptyDir: populated by init container before Keycloak starts - name: providers mountPath: /opt/keycloak/providers readOnly: true # data PVC: Keycloak build cache (data/generated/) and runtime data - name: data mountPath: /opt/keycloak/data # ── Probes ────────────────────────────────────────────────────── # Keycloak 24+: health endpoints on management port 9000. # /health/started — true once the application has completed startup. # /health/live — true unless the application is in an unrecoverable state. # /health/ready — true once Keycloak can serve requests. # # Startup: allow up to 5 min for DB migrations + provider build on first boot. startupProbe: httpGet: path: /health/started port: 9000 initialDelaySeconds: 20 periodSeconds: 10 failureThreshold: 30 # 30 × 10s = 5 min livenessProbe: httpGet: path: /health/live port: 9000 initialDelaySeconds: 0 periodSeconds: 15 failureThreshold: 3 readinessProbe: httpGet: path: /health/ready port: 9000 initialDelaySeconds: 0 periodSeconds: 10 failureThreshold: 3 # ── Resources ─────────────────────────────────────────────────── # Keycloak is JVM-based; the initial provider build spikes CPU briefly. # Raise limits for production. resources: requests: cpu: "250m" memory: "512Mi" limits: cpu: "1000m" memory: "1Gi" # ── Volumes ───────────────────────────────────────────────────────── volumes: # providers emptyDir: re-populated by init container on every pod start. # Keycloak detects the JAR and checks whether a rebuild is needed. # If the JAR hash matches the cached build, startup is fast (~30s). - name: providers emptyDir: {} - name: data persistentVolumeClaim: claimName: keycloak-data --- # Service — ClusterIP; Traefik reaches Keycloak via port 8080. # Port 9000 (management) is NOT exposed — kubelet probes reach it directly on the pod. apiVersion: v1 kind: Service metadata: name: keycloak namespace: sso labels: app.kubernetes.io/name: keycloak app.kubernetes.io/part-of: net-kingdom-sso-mfa net-kingdom/component: sso spec: type: ClusterIP selector: app.kubernetes.io/name: keycloak ports: - name: http port: 8080 targetPort: 8080 protocol: TCP