feat(k8s): Add Keycloak 26.6.0 with PostgreSQL 16 and classic-models realm CaC

This commit is contained in:
Stéphane Tailland
2026-08-23 07:40:40 +02:00
parent 31e6b1d6d7
commit 6d3431c2d5
14 changed files with 384 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
.DS_Store
*.tmp
+47
View File
@@ -0,0 +1,47 @@
# Infra Keycloak (Kubernetes / CaC)
Déploiement de **Keycloak 26.6.0** et **PostgreSQL 16** en Configuration-as-Code (CaC) sous Kubernetes dans le namespace `keycloak`.
## Structure du Répertoire
```text
k8s/
├── kustomization.yaml # Manifeste Kustomize principal
├── namespace.yaml # Namespace keycloak
├── secret.yaml # Secrets (Admin, DB, OIDC secrets)
├── secret.example.yaml # Gabarit des secrets
├── postgresql/
│ ├── pvc.yaml # Stockage persistant PostgreSQL (2Gi)
│ ├── statefulset.yaml # Base de données PostgreSQL 16
│ └── service.yaml # Service PostgreSQL (port 5432)
├── keycloak/
│ ├── configmap-realm.yaml # Realm classic-models (Configuration-as-Code)
│ ├── deployment.yaml # Déploiement Keycloak 26.6.0 (--import-realm)
│ └── service.yaml # Service Keycloak (ports 8080, 9000)
└── ingress/
├── tls.yaml # Secret TLS wildcard (*.sttlab.local)
└── ingress.yaml # Ingress NGINX (keycloak.sttlab.local)
```
## Fonctionnalités Clés
- **Version Keycloak** : `quay.io/keycloak/keycloak:26.6.0` (Runtime Quarkus).
- **Import Automatique du Realm (CaC)** : Le realm `classic-models` est chargé au démarrage via l'option `--import-realm` depuis le `ConfigMap` monté dans `/opt/keycloak/data/import/`.
- **Token Exchange RFC 8693** : Activé via `KC_FEATURES: "token-exchange,admin-fine-grained-authz"`.
- **Clients Configurés** :
- `librechat` : Client OIDC pour le login et le flux OAuth MCP.
- `mcp-catalog` : Audience du serveur MCP.
- `kong-token-exchange-broker` : Échangeur de jetons RFC 8693 de Kong Gateway.
- `catalog-api` & `classic-models-api` : Audiences des services backend.
- **Utilisateurs & Rôles** :
- `admin-user` (`admin`)
- `pm-user` (`product_manager`)
- `ro-user` (`read_only`)
- `customer-user` (`customer`)
- `support-user` (`support`)
## Déploiement
```bash
kubectl apply -k k8s/
```
+30
View File
@@ -0,0 +1,30 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
namespace: keycloak
annotations:
nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
nginx.ingress.kubernetes.io/proxy-buffers-number: "4"
nginx.ingress.kubernetes.io/proxy-busy-buffers-size: "256k"
nginx.ingress.kubernetes.io/proxy-body-size: "20m"
nginx.ingress.kubernetes.io/proxy-read-timeout: "600"
nginx.ingress.kubernetes.io/proxy-send-timeout: "600"
nginx.ingress.kubernetes.io/proxy-http-version: "1.1"
spec:
ingressClassName: nginx
tls:
- hosts:
- keycloak.sttlab.local
secretName: keycloak-tls
rules:
- host: keycloak.sttlab.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
+9
View File
@@ -0,0 +1,9 @@
apiVersion: v1
data:
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURQRENDQWlTZ0F3SUJBZ0lVZC91dERmK2YweVFSM3YzcVRqWnF4K3d2VFpNd0RRWUpLb1pJaHZjTkFRRUwKQlFBd0dURVhNQlVHQTFVRUF3d09LaTV6ZEhSc1lXSXViRzlqWVd3d0hoY05NalV4TVRJME1UVXpPRFExV2hjTgpNall4TVRJME1UVXpPRFExV2pBWk1SY3dGUVlEVlFRRERBNHFMbk4wZEd4aFlpNXNiMk5oYkRDQ0FTSXdEUVlKCktvWklodmNOQVFFQkJRQURnZ0VQQURDQ0FRb0NnZ0VCQUxqMHNyeXE4cVh0a1NoVC9CKy9FalZKTk4vVExISW4KUUpZYzZ4dVVIL2Y3cTNkWWRMbTRpSDBYbkZnclNRTFVvNGlxZC9NY3plZjVpV0NZd1Q1RkZhN1AxNy9UY0xWZgpnc28wajQxcU5MQmpHeERRR1E2WjFWQ3hpNm9hbjhvdzk4cnVtalZqMEUwUWdzNUNYL3JOK2djRW1aL1RaRldVCnF1ZFZvenRwZWlWcUowZHhDSXErS011dHpkMUJVN0JYeDRWUk85WSt0MWlkVWM3b1k3QlBtUzJxUEFON1R0WFAKajFLaEgyQmdMc21GNUdSaFlITlM1NzhlQ2x0Y21TMnNiUTNjN0d3T1Y5NmxwZ0Y5VGl4RXZGWGVONHhrNDNFNgoxQ09VRDdDUmlXQVowWEk4Rk1scFpHZU5aZVY0YmJWL01xVmlnMXliV05PejlNREtZajNlb1hVQ0F3RUFBYU44Ck1Ib3dIUVlEVlIwT0JCWUVGRGJ6WVdPUHI0dnhhK1ZPTnM3eEhiY054NzBZTUI4R0ExVWRJd1FZTUJhQUZEYnoKWVdPUHI0dnhhK1ZPTnM3eEhiY054NzBZTUE4R0ExVWRFd0VCL3dRRk1BTUJBZjh3SndZRFZSMFJCQ0F3SG9JTwpLaTV6ZEhSc1lXSXViRzlqWVd5Q0RITjBkR3hoWWk1c2IyTmhiREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBCnRVNHA1L09OWG03eDRZRHpTZUw4TmRUaFZMZ3VpRnVyMGpjVHh2dlBVUzRORk5zblFKazRPWHhCZ2p5MVl3cEwKM2k1Ni80bUNFZDlPQ1FGL0hsMGVGd0daQnpOS2xNb1RBMDhBQy9sQkR2UHJuVmp1VXUwcWUvYi8yS09raEVTYQp4OHJRTnAwbzhwY2owZjI5bnUwSmZScVlHa3p5MExoMnFnWGhQdTJpQyt0MlBmZGxlUkFreTF0dDY4d29nZEZMClR2SERzSmJWby9paHNQcE9ObFN5SGZ4RzZiYi8wTWt6NjRQSDQ2VFR0OUd2SmZsRUtNcUR3aXZORlVBMVA1Uy8KVTRJYzhRYXBueGM3aDIrV2JSam1Zcm51eWdDMndNQkU2ckw5T0YzZ3dDcjFma2dHR1JyNWxWUlVxMlgySFpWNQo1anFWaXY2ZXVycThPNjFOM3ZXR0NRPT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JSUV2QUlCQURBTkJna3Foa2lHOXcwQkFRRUZBQVNDQktZd2dnU2lBZ0VBQW9JQkFRQzQ5TEs4cXZLbDdaRW8KVS93ZnZ4STFTVFRmMHl4eUowQ1dIT3NibEIvMys2dDNXSFM1dUloOUY1eFlLMGtDMUtPSXFuZnpITTNuK1lsZwptTUUrUlJXdXo5ZS8wM0MxWDRMS05JK05halN3WXhzUTBCa09tZFZRc1l1cUdwL0tNUGZLN3BvMVk5Qk5FSUxPClFsLzZ6Zm9IQkptZjAyUlZsS3JuVmFNN2FYb2xhaWRIY1FpS3ZpakxyYzNkUVZPd1Y4ZUZVVHZXUHJkWW5WSE8KNkdPd1Q1a3RxandEZTA3Vno0OVNvUjlnWUM3SmhlUmtZV0J6VXVlL0hncGJYSmt0ckcwTjNPeHNEbGZlcGFZQgpmVTRzUkx4VjNqZU1aT054T3RRamxBK3drWWxnR2RGeVBCVEphV1JualdYbGVHMjFmektsWW9OY20xalRzL1RBCnltSTkzcUYxQWdNQkFBRUNnZ0VBSjBEZnNrVUJySStIOEN3cTVUM3pDbFlZSDZ5ODBSbGRqcjVpYzM3emJmaisKd2h5RzRXZTFwLzdiMHFhVkgwR3IxM1IrY3AxWllmTkc0YlpweE9KNm9KaEI3TmRlSTFBNi9Oazg0a0Y1SHpwWgpZeThYTWs4Wnc3OTZxY1p1eFVXeFFXTjBYUUpyRWdBQUV2QnBXRnhneFI2SDBERFlLWVdjNCszNzc3OEtDbWhwCmVpUWdoMWhpaEZ5OWF6QnVJN0VsTm02QzQ4VktrU1dwQUMyZHNNN2FvVkJjd1RHcjVjWjh6SVBWT2dTWitUVXMKelBINkV6b0VqRUQzajNFemRESENKMCtVdVNBTDh0L3F4ZGNEZjREdkdFeWR4S3J4OWpxSXJnalNab1UvOC94WApMUy9rRnk0cE9URnFsSDc2UUU4TytrbDRDTHZDQTdKTzNCN1p3dzM2d1FLQmdRRHJVaHdmNkQ5b3lsT3hPL3U1Cm5ndjl2YW04SGs0QlBRRjRHYi8zTU9DdzRGWnhIclFQRWg4Q1BLVXBndnhDRUM3UVNHOTJybVJjYUZXcDlqaCsKTWQyOFc5NHBlWW9wRnlkc0ZFa3F6bjFTb05qNkFFM3JhbTgwZ1JWempMZUFQNUxiRENXUVZtTmpnTTRySjJVZwpzVVhSZk5Kd3RIcEZwUkE0dmM4MUcvQk84UUtCZ1FESk5ZNGpGTmxoYUF5UytraHVJcm9PY0l2KzBoZElZcStpCktjeTJYbnBHSEd5b0s4S0pIckp2c1Via3NnTVlKVTJlT0g3cWlkNEo4Y1BDbmUwSjIvL0FvZytleEkvNnJOZzMKYll4UDNHWlp1aHNNclFEMmZ1WkxhM2pFZGJtaUxuM1A0cFNiZVJ4cDd5VitadlBlZ1dsZ0dsMHgwRE9Uc3NpNApDKzUxMnBRQ3hRS0JnRWhzYXU5L0FSVDFRRERKN3owL1lSMWs0L3Bic1ZJOFBsbUNRMHlHcElDTGFzOUJ6ZFRkCmxPR1JYSnVjRHBzZWMrbnlnU3VibkpPOElMRkFjRDZXaGxIVVRGbWphRndoSGliclQyRjNUNmJBamtiVEZTZ2gKQjd5M0JhMGNYOVYrdTJuaklTUjlFZldyek1nSFJkNTc4S3I2cWFHNlNlOXBUOGJHMUNEYVNtbGhBb0dBVlNqLwpvY1YvUkFUZjNZMWsvb2hnS3lCamdGUzNXZ0c2eUczY29RdFh3VGppbkVMNU1VUDJ6ZU5BV0NjbnZCNTl2WXUzCjI5akNTMzh5RmJyMXlvaDh0TEZpM3hYZXdvQkpleHc0bU14MTNXQXhkcW42a0ZtaGF5WnZsT2NteklmVTVCbkkKUUEyQWd5MmV0dU1vTUhLaGdQVm5vRis0bExqRXozRHRXcUJLcU9rQ2dZQlN6SHo3N1RkR0pRUVVMajJRVUFDOQozSXlaaXJmL0k4YnhPSnZrQnA3dktWODcxdHJmQXBaMDQwWDhIb1ZwZUErdjVZM3N4WkNKbUhwclhMNXZ1T1NaCnJoaHVvbTZuNHhUMi9qa2YybnR5c0UveFZGQUVrS2hVNEZISXNPR091V21YSWlSSzFLa2hJQXFhNzVBeVhESU4KdEVycWpTaGtFNHArdkQ0T0QzSmNqZz09Ci0tLS0tRU5EIFBSSVZBVEUgS0VZLS0tLS0K
kind: Secret
metadata:
name: keycloak-tls
namespace: keycloak
type: kubernetes.io/tls
File diff suppressed because one or more lines are too long
+122
View File
@@ -0,0 +1,122 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/component: idp
app.kubernetes.io/part-of: keycloak
spec:
replicas: 1
selector:
matchLabels:
app: keycloak
template:
metadata:
labels:
app: keycloak
app.kubernetes.io/name: keycloak
app.kubernetes.io/component: idp
app.kubernetes.io/part-of: keycloak
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.6.0
args:
- "start"
- "--import-realm"
ports:
- name: http
containerPort: 8080
- name: https
containerPort: 8443
- name: management
containerPort: 9000
env:
- name: KC_DB
value: "postgres"
- name: KC_DB_URL_HOST
value: "keycloak-postgresql"
- name: KC_DB_URL_PORT
value: "5432"
- name: KC_DB_URL_DATABASE
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: POSTGRES_DB
- name: KC_DB_USERNAME
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KC_DB_USERNAME
- name: KC_DB_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KC_DB_PASSWORD
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KC_BOOTSTRAP_ADMIN_USERNAME
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KC_BOOTSTRAP_ADMIN_PASSWORD
- name: KEYCLOAK_ADMIN
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KEYCLOAK_ADMIN
- name: KEYCLOAK_ADMIN_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: KEYCLOAK_ADMIN_PASSWORD
- name: KC_HTTP_ENABLED
value: "true"
- name: KC_PROXY_HEADERS
value: "xforwarded"
- name: KC_HOSTNAME
value: "https://keycloak.sttlab.local"
- name: KC_HOSTNAME_STRICT
value: "false"
- name: KC_HOSTNAME_BACKCHANNEL_DYNAMIC
value: "false"
- name: KC_FEATURES
value: "token-exchange,admin-fine-grained-authz"
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_METRICS_ENABLED
value: "true"
readinessProbe:
httpGet:
path: /health/ready
port: 9000
initialDelaySeconds: 15
periodSeconds: 10
failureThreshold: 5
livenessProbe:
httpGet:
path: /health/live
port: 9000
initialDelaySeconds: 30
periodSeconds: 15
failureThreshold: 5
resources:
requests:
cpu: 500m
memory: 1024Mi
limits:
cpu: 2000m
memory: 2048Mi
volumeMounts:
- name: realm-import
mountPath: /opt/keycloak/data/import
readOnly: true
volumes:
- name: realm-import
configMap:
name: keycloak-realm-classic-models
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/component: idp
app.kubernetes.io/part-of: keycloak
spec:
ports:
- name: http
port: 8080
targetPort: 8080
- name: management
port: 9000
targetPort: 9000
selector:
app: keycloak
+14
View File
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- secret.yaml
- postgresql/pvc.yaml
- postgresql/statefulset.yaml
- postgresql/service.yaml
- keycloak/configmap-realm.yaml
- keycloak/deployment.yaml
- keycloak/service.yaml
- ingress/tls.yaml
- ingress/ingress.yaml
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
+11
View File
@@ -0,0 +1,11 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: postgresql-data
namespace: keycloak
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 2Gi
+16
View File
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Service
metadata:
name: keycloak-postgresql
namespace: keycloak
labels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/component: database
app.kubernetes.io/part-of: keycloak
spec:
ports:
- port: 5432
targetPort: 5432
name: postgresql
selector:
app: keycloak-postgresql
+61
View File
@@ -0,0 +1,61 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: keycloak-postgresql
namespace: keycloak
labels:
app.kubernetes.io/name: postgresql
app.kubernetes.io/component: database
app.kubernetes.io/part-of: keycloak
spec:
serviceName: keycloak-postgresql
replicas: 1
selector:
matchLabels:
app: keycloak-postgresql
template:
metadata:
labels:
app: keycloak-postgresql
app.kubernetes.io/name: postgresql
app.kubernetes.io/component: database
app.kubernetes.io/part-of: keycloak
spec:
containers:
- name: postgresql
image: postgres:16-alpine
ports:
- containerPort: 5432
name: postgresql
env:
- name: POSTGRES_DB
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: POSTGRES_DB
- name: POSTGRES_USER
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: POSTGRES_USER
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: keycloak-secrets
key: POSTGRES_PASSWORD
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
volumes:
- name: data
persistentVolumeClaim:
claimName: postgresql-data
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Secret
metadata:
name: keycloak-secrets
namespace: keycloak
type: Opaque
stringData:
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: <CHANGE_ME>
POSTGRES_DB: keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: <CHANGE_ME>
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: <CHANGE_ME>
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: <CHANGE_ME>
LIBRECHAT_OIDC_SECRET: <CHANGE_ME>
CLIENT_SECRET: <CHANGE_ME>
USER_PASSWORD: <CHANGE_ME>
+19
View File
@@ -0,0 +1,19 @@
apiVersion: v1
kind: Secret
metadata:
name: keycloak-secrets
namespace: keycloak
type: Opaque
stringData:
POSTGRES_USER: keycloak
POSTGRES_PASSWORD: keycloak-db-pass-2026
POSTGRES_DB: keycloak
KC_DB_USERNAME: keycloak
KC_DB_PASSWORD: keycloak-db-pass-2026
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: mermAid-giganTic-s3ea
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: mermAid-giganTic-s3ea
LIBRECHAT_OIDC_SECRET: librechat-oidc-secret-2026
CLIENT_SECRET: classic-models-client-secret-2026
USER_PASSWORD: Password123@