5.3.4 Identity & Access Management¶
Keycloak provides centralised authentication and authorisation for the RCIIS platform. It acts as the OIDC/SAML identity provider for the Kubernetes API, the APISIX Gateway, Grafana, and RCIIS application services — replacing scattered local credentials with a single, auditable identity layer.
How to use this page
Each component has an Install section showing the Flux manifests, a Configuration section with resource specs, and a Verify section to confirm it is working.
All code blocks are labelled with their file path in the repository. Select your target environment (AWS or Bare Metal) in any tab group — the choice syncs across the entire page.
- Using the existing
rciis-devopsrepository: All files already exist. Skip themkdirandgit add/git commitcommands — they are for users building a new repository. Simply review the files, edit values for your environment, and push. - Building a new repository from scratch: Follow the
mkdir, file creation, andgitcommands in order. - No Git access: Use
kubectl applyinstead ofgit pushto deploy manifests directly.
Keycloak Operator¶
The RCIIS deployment uses the official Keycloak Operator (k8s.keycloak.org/v2alpha1) deployed via Flux as raw Kubernetes manifests. The operator manages the Keycloak StatefulSet, database integration, and realm lifecycle declaratively.
Directory Structure¶
Create the Flux directories for your environment:
The directory will contain:
flux/infra/aws/keycloak/
├── namespace.yaml
├── operator.yaml # Operator Deployment, RBAC, Service
├── pg-instance.yaml # CloudNativePG PostgreSQL cluster
├── kc.yaml # Keycloak CR
├── httproutes.yaml # Gateway API routes for auth.rciis.africa
├── kustomization.yaml # Flux Kustomization references above
└── secrets/
└── keycloak-admin.enc.yaml # SOPS-encrypted admin credentials
The directory will contain:
flux/infra/baremetal/keycloak/
├── namespace.yaml
├── operator.yaml # Operator Deployment, RBAC, Service
├── pg-instance.yaml # CloudNativePG PostgreSQL cluster
├── kc.yaml # Keycloak CR
├── httproutes.yaml # Gateway API routes for auth.rciis.africa
├── kustomization.yaml # Flux Kustomization references above
└── secrets/
└── keycloak-admin.enc.yaml # SOPS-encrypted admin credentials
The directory will contain:
flux/infra/baremetal/keycloak/
├── namespace.yaml
├── operator.yaml # Operator Deployment, RBAC, Service
├── pg-instance.yaml # CloudNativePG PostgreSQL cluster
├── kc.yaml # Keycloak CR
├── httproutes.yaml # Gateway API routes for auth.rciis.africa
├── kustomization.yaml # Flux Kustomization references above
└── secrets/
└── keycloak-admin.enc.yaml # SOPS-encrypted admin credentials
Namespace¶
Save the following as namespace.yaml:
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
Operator Deployment¶
The operator is deployed as a Kubernetes Deployment with RBAC. Save the following as operator.yaml:
Operator Version
Pin the Keycloak operator to a specific release tag. Update image: to upgrade. Test upgrades in a non-production environment first.
# Keycloak Operator ServiceAccount, RBAC, Deployment
apiVersion: v1
kind: ServiceAccount
metadata:
name: keycloak-operator
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keycloak-operator-clusterrole
labels:
app.kubernetes.io/name: keycloak-operator
rules:
- apiGroups: [config.openshift.io]
resources: [ingresses]
verbs: [get]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keycloakcontroller-cluster-role
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
rules:
- apiGroups: [k8s.keycloak.org]
resources: [keycloaks, keycloaks/status, keycloaks/finalizers]
verbs: [get, list, watch, patch, update, create, delete]
- apiGroups: [""]
resources: [services]
verbs: [create, delete, get, list, patch, update, watch]
- apiGroups: [monitoring.coreos.com]
resources: [servicemonitors]
verbs: [create, delete, get, list, patch, update, watch]
- apiGroups: [networking.k8s.io]
resources: [networkpolicies]
verbs: [create, delete, get, list, patch, update, watch]
- apiGroups: [apps]
resources: [statefulsets]
verbs: [create, delete, get, list, patch, update, watch]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [create, delete, get, list, patch, update, watch]
- apiGroups: [""]
resources: [secrets]
verbs: [create, delete, get, list, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
name: keycloakrealmimportcontroller-cluster-role
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
rules:
- apiGroups: [k8s.keycloak.org]
resources: [keycloakrealmimports, keycloakrealmimports/status, keycloakrealmimports/finalizers]
verbs: [get, list, watch, patch, update, create, delete]
- apiGroups: [batch]
resources: [jobs]
verbs: [create, delete, get, list, patch, watch]
- apiGroups: [""]
resources: [secrets]
verbs: [create, delete, get, list, patch, update, watch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: keycloak-operator-clusterrole-binding
labels:
app.kubernetes.io/name: keycloak-operator
roleRef:
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
name: keycloak-operator-clusterrole
subjects:
- kind: ServiceAccount
name: keycloak-operator
namespace: keycloak
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: keycloak-operator-role
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
rules:
- apiGroups: [apps]
resources: [statefulsets]
verbs: [get, list, watch, create, delete, patch, update]
- apiGroups: [""]
resources: [configmaps]
verbs: [get, list, watch]
- apiGroups: [""]
resources: [secrets, services]
verbs: [get, list, watch, create, delete, patch, update]
- apiGroups: [""]
resources: [pods]
verbs: [list]
- apiGroups: [""]
resources: [pods/log]
verbs: [get]
- apiGroups: [batch]
resources: [jobs]
verbs: [get, list, watch, create, delete, patch, update]
- apiGroups: [networking.k8s.io]
resources: [ingresses]
verbs: [get, list, watch, create, delete, patch, update]
- apiGroups: [monitoring.coreos.com]
resources: [servicemonitors]
verbs: [create, delete, get, list, update, watch, patch]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloak-operator-role-binding
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
roleRef:
kind: Role
apiGroup: rbac.authorization.k8s.io
name: keycloak-operator-role
subjects:
- kind: ServiceAccount
name: keycloak-operator
namespace: keycloak
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloakcontroller-role-binding
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
roleRef:
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
name: keycloakcontroller-cluster-role
subjects:
- kind: ServiceAccount
name: keycloak-operator
namespace: keycloak
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloakrealmimportcontroller-role-binding
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
roleRef:
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
name: keycloakrealmimportcontroller-cluster-role
subjects:
- kind: ServiceAccount
name: keycloak-operator
namespace: keycloak
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: keycloak-operator-view
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
roleRef:
kind: ClusterRole
apiGroup: rbac.authorization.k8s.io
name: view
subjects:
- kind: ServiceAccount
name: keycloak-operator
namespace: keycloak
---
apiVersion: v1
kind: Service
metadata:
name: keycloak-operator
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
spec:
ports:
- name: http
port: 80
protocol: TCP
targetPort: 8080
selector:
app.kubernetes.io/name: keycloak-operator
type: ClusterIP
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak-operator
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: keycloak-operator
template:
metadata:
labels:
app.kubernetes.io/name: keycloak-operator
app.kubernetes.io/version: 26.5.3
spec:
serviceAccountName: keycloak-operator
containers:
- name: keycloak-operator
image: quay.io/keycloak/keycloak-operator:26.5.3
imagePullPolicy: Always
env:
- name: KUBERNETES_NAMESPACE
valueFrom:
fieldRef:
fieldPath: metadata.namespace
- name: RELATED_IMAGE_KEYCLOAK
value: quay.io/keycloak/keycloak:26.5.3
- name: QUARKUS_OPERATOR_SDK_CONTROLLERS_KEYCLOAKREALMIMPORTCONTROLLER_NAMESPACES
value: JOSDK_WATCH_CURRENT
- name: QUARKUS_OPERATOR_SDK_CONTROLLERS_KEYCLOAKCONTROLLER_NAMESPACES
value: JOSDK_WATCH_CURRENT
ports:
- containerPort: 8080
name: http
protocol: TCP
livenessProbe:
httpGet:
path: /q/health/live
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /q/health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
resources:
requests:
cpu: 300m
memory: 450Mi
limits:
cpu: 700m
memory: 450Mi
Same as AWS. The operator manifest is environment-agnostic.
Same as AWS. The operator manifest is environment-agnostic.
PostgreSQL Database (CloudNativePG)¶
Keycloak requires an external PostgreSQL database. This is provisioned using the CloudNativePG operator. Save the following as pg-instance.yaml:
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-postgres
namespace: keycloak
annotations:
cnpg.io/reload-secrets: "true" # Enables live reload of secrets
spec:
description: "PostgreSQL for Keycloak"
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:15.0
managed:
roles:
- name: keycloak
ensure: present
comment: Keycloak User
login: true
superuser: false
inRoles:
- pg_monitor
- pg_signal_backend
passwordSecret:
name: cnpg-keycloak-owner
bootstrap:
initdb:
database: keycloak
owner: keycloak
secret:
name: cnpg-keycloak-owner
postgresql:
pg_hba:
- host keycloak keycloak 0.0.0.0/0 scram-sha-256
superuserSecret:
name: cnpg-keycloak-superuser
storage:
storageClass: gp3
size: 10Gi
resources:
requests:
memory: "64Mi"
cpu: "10m"
limits:
memory: "1Gi"
cpu: "500m"
Same as AWS, but adjust storageClass to match your environment (e.g., ceph-rbd-single):
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-postgres
namespace: keycloak
annotations:
cnpg.io/reload-secrets: "true"
spec:
description: "PostgreSQL for Keycloak"
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:15.0
managed:
roles:
- name: keycloak
ensure: present
comment: Keycloak User
login: true
superuser: false
inRoles:
- pg_monitor
- pg_signal_backend
passwordSecret:
name: cnpg-keycloak-owner
bootstrap:
initdb:
database: keycloak
owner: keycloak
secret:
name: cnpg-keycloak-owner
postgresql:
pg_hba:
- host keycloak keycloak 0.0.0.0/0 scram-sha-256
superuserSecret:
name: cnpg-keycloak-superuser
storage:
storageClass: ceph-rbd-single
size: 10Gi
resources:
requests:
memory: "64Mi"
cpu: "10m"
limits:
memory: "1Gi"
cpu: "500m"
Same as AWS, but adjust storageClass to match your environment (e.g., ceph-rbd-single):
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-postgres
namespace: keycloak
annotations:
cnpg.io/reload-secrets: "true"
spec:
description: "PostgreSQL for Keycloak"
instances: 1
imageName: ghcr.io/cloudnative-pg/postgresql:15.0
managed:
roles:
- name: keycloak
ensure: present
comment: Keycloak User
login: true
superuser: false
inRoles:
- pg_monitor
- pg_signal_backend
passwordSecret:
name: cnpg-keycloak-owner
bootstrap:
initdb:
database: keycloak
owner: keycloak
secret:
name: cnpg-keycloak-owner
postgresql:
pg_hba:
- host keycloak keycloak 0.0.0.0/0 scram-sha-256
superuserSecret:
name: cnpg-keycloak-superuser
storage:
storageClass: ceph-rbd-single
size: 10Gi
resources:
requests:
memory: "64Mi"
cpu: "10m"
limits:
memory: "1Gi"
cpu: "500m"
Keycloak CR¶
The Keycloak custom resource declares the desired Keycloak instance. The operator watches this CR and reconciles the StatefulSet, services, and other resources. Save the following as kc.yaml:
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: kc
namespace: keycloak
spec:
instances: 1
db:
vendor: postgres
host: keycloak-postgres-rw
usernameSecret:
name: cnpg-keycloak-owner
key: username
passwordSecret:
name: cnpg-keycloak-owner
key: password
http:
httpEnabled: true
hostname:
hostname: https://auth.rciis.africa
admin: https://keycloak.rciis.africa
strict: true
ingress:
enabled: false
proxy:
headers: xforwarded
bootstrapAdmin:
service:
secret: kc-crossplane-service
Same as AWS. Replace .rciis.africa with your bare metal domain if different.
Same as AWS. Replace .rciis.africa with your bare metal domain if different.
Gateway API Routes¶
Keycloak is exposed via Gateway API HTTPRoute resources. Two routes are created: one for public users (auth.rciis.africa) and one for administrators (keycloak.rciis.africa). Save the following as httproutes.yaml:
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: keycloak-auth
namespace: keycloak
spec:
parentRefs:
- name: aws-gateway
namespace: kube-system
sectionName: https-auth
hostnames:
- auth.rciis.africa
rules:
- backendRefs:
- name: kc-service
port: 8080
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: keycloak-admin
namespace: keycloak
spec:
parentRefs:
- name: aws-gateway
namespace: kube-system
sectionName: https-keycloak
hostnames:
- keycloak.rciis.africa
rules:
- backendRefs:
- name: kc-service
port: 8080
Same as AWS. Update the parentRef sectionName if your bare metal gateway uses different listener names.
Same as AWS. Update the parentRef sectionName if your bare metal gateway uses different listener names.
Kustomization¶
Create a kustomization.yaml to tell Flux which manifests to apply:
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
- operator.yaml
- pg-instance.yaml
- kc.yaml
- httproutes.yaml
Database Credentials Secret¶
Create the PostgreSQL database credentials as a SOPS-encrypted secret. Generate a strong password:
Save the following as secrets/keycloak-admin.enc.yaml and encrypt it:
apiVersion: v1
kind: Secret
metadata:
name: cnpg-keycloak-owner
namespace: keycloak
type: kubernetes.io/basic-auth
stringData:
username: keycloak
password: <generated-password>
---
apiVersion: v1
kind: Secret
metadata:
name: cnpg-keycloak-superuser
namespace: keycloak
type: kubernetes.io/basic-auth
stringData:
username: postgres
password: <generated-password>
---
apiVersion: v1
kind: Secret
metadata:
name: kc-crossplane-service
namespace: keycloak
type: kubernetes.io/basic-auth
stringData:
username: admin
password: <generated-password>
Encrypt the file:
Commit and Deploy¶
Once all files are in place, commit and push to trigger Flux deployment:
Flux will detect the new commit and begin deploying Keycloak. To trigger an immediate sync:
Verify¶
After Keycloak is deployed, confirm it is working:
# Check that all pods are running
kubectl -n keycloak get pods
# Watch the Keycloak CR status
kubectl -n keycloak get keycloak kc -o wide
# Check the database is healthy
kubectl -n keycloak get cluster keycloak-postgres
# Get admin credentials from the secret
kubectl -n keycloak get secret kc-crossplane-service -o jsonpath='{.data.password}' | base64 -d
# Access Keycloak admin console (port-forward if needed)
kubectl -n keycloak port-forward svc/kc-service 8080:8080
# Open https://localhost:8080/admin (or https://keycloak.rciis.africa/admin if DNS/ingress is configured)
Flux Operations¶
This component is managed by Flux as Kustomization infra-keycloak.
Check whether the Kustomization is in a Ready state:
Trigger an immediate sync — pulls the latest Git revision and re-applies the manifests. Use after pushing config changes:
View recent Flux controller logs — useful for diagnosing why a sync failed:
Realm, Client & Federation Configuration (Optional)¶
Keycloak resources — realms, clients, roles, and identity providers — can be configured declaratively using Crossplane with the provider-keycloak. This is an advanced integration beyond the scope of this guide. Refer to the Keycloak Operator documentation for manual realm setup via the admin console.
Next Steps¶
Keycloak is now deployed and running. Proceed to 5.3.5 Key Management to configure HSM-backed encryption for cryptographic keys.