feat: initial scaffold from helm-template
Validate login GitOps repo / validate (push) Successful in 9s

- Keycloak Operator v26 deployed to keycloak-system namespace
- Keycloak HA instance (2 replicas, jdbc-ping cluster discovery)
- Dedicated CNPG cluster (local-postgres storage, 2 instances)
- KeycloakRealmImport: home-lab realm with groups and traefik-oidc client
- Traefik IngressRoute: login.olb42.com (CF Access bypass)
- Admin credentials placeholder (seal before first deploy)
- ArgoCD ApplicationSets: login-operator + login
This commit is contained in:
2026-05-10 22:32:49 +01:00
commit 6f29be2987
21 changed files with 750 additions and 0 deletions
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- runtime
- state
+7
View File
@@ -0,0 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Runtime objects are managed by the Keycloak Operator via the Keycloak CR.
# Nothing to define here — the operator creates Deployments, Services, and
# ConfigMaps from the spec in manifest/overlays/production/keycloak-instance.yaml.
resources: []
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Persistent state is managed by the CNPG Cluster CR defined in the production
# overlay. CNPG handles PVC lifecycle; nothing additional is needed here.
resources: []
+17
View File
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
# Keycloak Operator v26 — official Kubernetes manifests.
# Pin to a specific release tag; bump here when upgrading Keycloak.
# Full release list: https://github.com/keycloak/keycloak-k8s-resources/releases
#
# These resources include:
# - keycloaks.k8s.keycloak.org CRD
# - keycloakrealmimports.k8s.keycloak.org CRD
# - keycloak-operator Deployment + RBAC (deployed to keycloak-system namespace
# as defined in the upstream manifest; operator watches all namespaces)
resources:
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloaks.k8s.keycloak.org-v1.yml
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml
- https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/26.2.5/kubernetes/kubernetes.yml
@@ -0,0 +1,22 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: login
# Dormant mode: CNPG cluster stays alive to preserve data.
# Keycloak instance is scaled to zero by patching the Keycloak CR.
resources:
- ../../base/state
patches:
- patch: |
- op: replace
path: /spec/instances
value: 0
target:
kind: Keycloak
name: keycloak
commonLabels:
app.kubernetes.io/part-of: login
app.kubernetes.io/environment: dormant
@@ -0,0 +1,25 @@
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: login-admin
namespace: login
annotations:
sealedsecrets.bitnami.com/managed: "true"
spec:
encryptedData:
# Seal from admin.secret.plain.yaml:
# kubeseal --context homelab-argocd \
# --secret-file manifest/overlays/production/admin.secret.plain.yaml \
# --sealed-secret-file manifest/overlays/production/admin.sealed.secret.yaml
username: REPLACE_WITH_KUBESEAL_OUTPUT
password: REPLACE_WITH_KUBESEAL_OUTPUT
template:
metadata:
name: login-admin
namespace: login
annotations:
sealedsecrets.bitnami.com/managed: "true"
labels:
app.kubernetes.io/part-of: login
app.kubernetes.io/environment: production
type: Opaque
@@ -0,0 +1,48 @@
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: login-postgres
namespace: login
spec:
# ── HA: 1 primary + 1 standby replica ────────────────────────────────────
instances: 2
# ── Storage: local-postgres class (fast local NVMe, no Longhorn overhead) ─
storage:
storageClass: local-postgres
size: 8Gi
# ── Spread replicas across nodes ──────────────────────────────────────────
affinity:
enablePodAntiAffinity: true
topologyKey: kubernetes.io/hostname
# ── Bootstrap: create the keycloak database and owner role ───────────────
bootstrap:
initdb:
database: keycloak
owner: keycloak
# ── WAL archival via barman-cloud → MinIO ─────────────────────────────────
plugins:
- name: barman-cloud.cloudnative-pg.io
isWALArchiver: true
parameters:
barmanObjectName: minio-store
# ── PostgreSQL tuning for Keycloak workload ───────────────────────────────
postgresql:
parameters:
max_connections: "200"
shared_buffers: "256MB"
work_mem: "8MB"
maintenance_work_mem: "64MB"
# ── Resources ─────────────────────────────────────────────────────────────
resources:
requests:
memory: 256Mi
cpu: 100m
limits:
memory: 512Mi
cpu: 500m
@@ -0,0 +1,36 @@
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: login
namespace: login
annotations:
# Cloudflare tunnel: public access, bypass CF Access
# Keycloak IS the auth provider — it must be reachable before any OIDC
# flow can begin, so CF Access must not gate it.
dns.public: "true"
dns.public.access.policy: bypass
dns.public.hostname: login.olb42.com
# Homepage service discovery
gethomepage.dev/enabled: "true"
gethomepage.dev/name: "Keycloak"
gethomepage.dev/group: "Security"
gethomepage.dev/description: "Identity & Access Management (operator-managed)"
gethomepage.dev/href: "https://login.olb42.com"
gethomepage.dev/icon: "keycloak"
gethomepage.dev/app: "keycloak"
gethomepage.dev/namespace: "login"
spec:
entryPoints:
- websecure
routes:
- match: Host(`login.olb42.com`)
kind: Rule
# No OIDC middleware: Keycloak is the identity provider itself.
# Traefik passes traffic straight through to the Keycloak service.
services:
- name: keycloak-service
namespace: login
port: 8080
tls:
secretName: olb42-wildcard-tls
@@ -0,0 +1,76 @@
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak
namespace: login
spec:
# ── HA: 2 replicas with Infinispan jdbc-ping cluster discovery ────────────
instances: 2
# ── Image: pin to the same major as the operator release ─────────────────
image: quay.io/keycloak/keycloak:26.2.5
startOptimized: false
# ── Database: CNPG cluster (login-postgres) ───────────────────────────────
# CNPG auto-creates the secret <cluster-name>-app with username/password.
db:
vendor: postgres
host: login-postgres-rw
port: 5432
database: keycloak
usernameSecret:
name: login-postgres-app
key: username
passwordSecret:
name: login-postgres-app
key: password
# ── HTTP: plain HTTP backend; TLS terminated at Traefik ───────────────────
http:
httpEnabled: true
httpPort: 8080
httpsPort: 8443
# No TLS secret — Traefik handles TLS via wildcard cert
# ── Hostname ──────────────────────────────────────────────────────────────
hostname:
hostname: login.olb42.com
admin: login.olb42.com
# strict=false: allow X-Forwarded-Host from Traefik
strict: false
strictBackchannel: false
# ── Proxy: trust forwarded headers from Traefik ──────────────────────────
proxy:
headers: forwarded
# ── Bootstrap admin credentials (sealed secret — see admin.sealed.secret.yaml)
bootstrapAdminSpec:
secret:
name: login-admin
# ── Additional Keycloak options ───────────────────────────────────────────
additionalOptions:
# jdbc-ping: DB-backed cluster discovery — no Kubernetes API/RBAC needed
- name: cache-stack
value: jdbc-ping
- name: cache
value: ispn
- name: log-level
value: INFO
# Forward client IP through Traefik X-Forwarded-For chain
- name: proxy-trusted-addresses
value: "0.0.0.0/0"
# ── Resources ─────────────────────────────────────────────────────────────
resources:
requests:
memory: 512Mi
cpu: 250m
limits:
memory: 1500Mi
cpu: "2"
# ── Ingress disabled: Traefik IngressRoute manages external access ─────────
ingress:
enabled: false
@@ -0,0 +1,100 @@
apiVersion: k8s.keycloak.org/v2alpha1
kind: KeycloakRealmImport
metadata:
name: home-lab
namespace: login
spec:
keycloakCRName: keycloak
# ── Realm definition ──────────────────────────────────────────────────────
# This is a Keycloak realm export JSON embedded as a structured spec.
# The operator applies this on startup and reconciles on changes.
# Add clients, groups, and roles here; per-app clients can be managed in
# their own app repos by adding additional KeycloakRealmImport resources
# targeting this same keycloakCRName.
realm:
realm: home-lab
displayName: "Home Lab"
enabled: true
loginWithEmailAllowed: true
duplicateEmailsAllowed: false
resetPasswordAllowed: true
editUsernameAllowed: false
bruteForceProtected: true
permanentLockout: false
maxFailureWaitSeconds: 900
minimumQuickLoginWaitSeconds: 60
waitIncrementSeconds: 60
quickLoginCheckMilliSeconds: 1000
maxDeltaTimeSeconds: 43200
failureFactor: 10
# ── Session settings ──────────────────────────────────────────────────
ssoSessionIdleTimeout: 1800
ssoSessionMaxLifespan: 36000
accessTokenLifespan: 300
accessTokenLifespanForImplicitFlow: 900
offlineSessionIdleTimeout: 2592000
offlineSessionMaxLifespanEnabled: false
# ── Password policy ───────────────────────────────────────────────────
passwordPolicy: "length(12) and notUsername(undefined)"
# ── Groups ────────────────────────────────────────────────────────────
groups:
- name: homelab-admin
path: /homelab-admin
- name: homelab-user
path: /homelab-user
- name: transmission-users
path: /transmission-users
- name: media-users
path: /media-users
- name: monitoring-users
path: /monitoring-users
# ── Default client scopes ─────────────────────────────────────────────
defaultDefaultClientScopes:
- web-origins
- acr
- roles
- profile
- basic
- email
# ── Clients ───────────────────────────────────────────────────────────
# traefik-oidc: shared client for traefikoidc middleware (SSO cookie domain)
# Per-app clients with dedicated redirect URIs and claim mappers should be
# added as additional KeycloakRealmImport resources in each app's repo.
clients:
- clientId: traefik-oidc
name: "Traefik OIDC"
description: "Shared OIDC client for Traefik traefikoidc middleware"
enabled: true
publicClient: false
standardFlowEnabled: true
implicitFlowEnabled: false
directAccessGrantsEnabled: false
serviceAccountsEnabled: false
authorizationServicesEnabled: false
redirectUris:
- "https://transmission-ng.olb42.com/oauth2/callback"
- "https://home.olb42.com/oauth2/callback"
- "https://dozzle.olb42.com/oauth2/callback"
- "https://pgadmin4.olb42.com/oauth2/callback"
webOrigins:
- "+"
attributes:
pkce.code.challenge.method: "S256"
post.logout.redirect.uris: "+"
protocolMappers:
- name: groups
protocol: openid-connect
protocolMapper: oidc-group-membership-mapper
consentRequired: false
config:
full.path: "false"
id.token.claim: "true"
access.token.claim: "true"
claim.name: "groups"
userinfo.token.claim: "true"
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: login
resources:
- ../../base/state
- cnpg-cluster.yaml
- keycloak-instance.yaml
- keycloak-realm-home-lab.yaml
- ingressroute.yaml
- admin.sealed.secret.yaml
commonLabels:
app.kubernetes.io/part-of: login
app.kubernetes.io/environment: production