diff --git a/keycloak/README.md b/keycloak/README.md new file mode 100644 index 0000000..aa99557 --- /dev/null +++ b/keycloak/README.md @@ -0,0 +1,336 @@ +# Keycloak + +Identity and Access Management (IAM) solution for Kubernetes: + +- **Keycloak Operator**: Manages Keycloak instances via CRDs +- **Keycloak**: Open Source Identity and Access Management +- **PostgreSQL Integration**: Uses CloudNativePG cluster for persistence +- **OIDC Provider**: Centralized authentication for all services + +## Prerequisites + +- Kubernetes cluster (k3s) +- PostgreSQL (CloudNativePG) +- External Secrets Operator (optional, for Vault integration) +- Vault (optional, for credential storage) + +## Installation + +```bash +just keycloak::install +``` + +You will be prompted for: + +1. **Keycloak admin username**: Default is `admin` +2. **Keycloak admin password**: Auto-generated if not provided +3. **Keycloak host (FQDN)**: e.g., `auth.example.com` + +### What Gets Installed + +1. Keycloak Operator and CRDs +2. Keycloak instance with PostgreSQL backend +3. Ingress for external access +4. Admin credentials stored in Kubernetes Secret (and optionally Vault) + +The stack uses the official [Keycloak Operator](https://www.keycloak.org/operator/installation). + +## Pod Security Standards + +The keycloak namespace uses **baseline** Pod Security Standard enforcement. + +```bash +pod-security.kubernetes.io/enforce=baseline +``` + +### Why Baseline Instead of Restricted? + +The **Keycloak Operator** (provided by upstream) does not meet the `restricted` Pod Security Standard requirements: + +- Missing `allowPrivilegeEscalation: false` +- Missing `securityContext.capabilities.drop: [ALL]` +- Missing `runAsNonRoot: true` +- Missing `seccompProfile` + +Since the Operator is deployed via official manifests from GitHub, we cannot modify its security context without maintaining a custom fork. + +### Security Measures + +While using baseline enforcement at the namespace level, the **Keycloak application Pod** applies restricted-level security contexts via `unsupported.podTemplate`: + +**Pod Security Context**: + +- `runAsNonRoot: true` +- `runAsUser: 1000` +- `runAsGroup: 1000` +- `fsGroup: 1000` +- `seccompProfile.type: RuntimeDefault` + +**Container Security Context**: + +- `allowPrivilegeEscalation: false` +- `capabilities.drop: [ALL]` +- `runAsNonRoot: true` +- `seccompProfile.type: RuntimeDefault` + +**Note**: `readOnlyRootFilesystem: false` is required because Keycloak needs to write configuration files and cache data. + +### Baseline vs Restricted + +**Baseline** still provides strong security: + +- Prohibits privileged containers +- Prohibits `hostNetwork`, `hostPID`, `hostIPC` +- Prohibits `hostPath` volumes +- Restricts dangerous capabilities + +The primary difference is that baseline does not enforce seccomp profiles and capability drops, which the Operator lacks but the Keycloak Pod implements. + +## Access + +Access Keycloak at `https://your-keycloak-host/` + +**Admin Credentials**: + +- Username: Retrieved via `just keycloak::admin-username` +- Password: Retrieved via `just keycloak::admin-password` + +## Configuration + +Environment variables (set in `.env.local` or override): + +```bash +KEYCLOAK_NAMESPACE=keycloak # Kubernetes namespace +KEYCLOAK_OPERATOR_VERSION=26.4.5 # Keycloak Operator version +KEYCLOAK_REALM= # Default realm name +KEYCLOAK_HOST= # Keycloak FQDN +KEYCLOAK_ADMIN_USER= # Admin username +KEYCLOAK_ADMIN_PASSWORD= # Admin password +``` + +## Realm Management + +### Create Realm + +```bash +just keycloak::create-realm +``` + +You will be prompted for the realm name. This creates a new realm for your applications. + +### Delete Realm + +```bash +just keycloak::delete-realm +``` + +## User Management + +### Create User + +```bash +just keycloak::create-user +``` + +Interactive prompts for: + +- Username +- Email +- First name +- Last name +- Password +- Realm + +### Delete User + +```bash +just keycloak::delete-user +``` + +### Add User to Group + +```bash +just keycloak::add-user-to-group +``` + +### List Users + +```bash +just keycloak::list-users +``` + +## Client Management + +### Create OIDC Client + +```bash +just keycloak::create-client realm= client_id= redirect_url= client_secret= +``` + +This creates a confidential OIDC client with the specified settings. + +**For public clients** (e.g., browser-based apps): + +```bash +just keycloak::create-public-client realm= client_id= redirect_url= +``` + +### Delete Client + +```bash +just keycloak::delete-client +``` + +### List Clients + +```bash +just keycloak::list-clients +``` + +## Group Management + +### Create Group + +```bash +just keycloak::create-group +``` + +### Delete Group + +```bash +just keycloak::delete-group +``` + +### List Groups + +```bash +just keycloak::list-groups +``` + +## Common Integration Patterns + +### OIDC Authentication for Web Applications + +1. **Create OIDC client**: + + ```bash + just keycloak::create-client \ + realm=myrealm \ + client_id=myapp \ + redirect_url=https://myapp.example.com/callback \ + client_secret=$(just utils::random-password) + ``` + +2. **Configure your application**: + - OIDC Discovery URL: `https://your-keycloak-host/realms/myrealm` + - Client ID: `myapp` + - Client Secret: (from step 1) + - Redirect URI: `https://myapp.example.com/callback` + +3. **Create user groups for authorization**: + + ```bash + just keycloak::create-group myapp-admins + just keycloak::create-group myapp-users + ``` + +4. **Add users to groups**: + + ```bash + just keycloak::add-user-to-group alice myapp-admins + just keycloak::add-user-to-group bob myapp-users + ``` + +### JWT Token Validation + +Applications can validate JWT tokens issued by Keycloak using the public keys from: + +``` +https://your-keycloak-host/realms/myrealm/protocol/openid-connect/certs +``` + +### Kubernetes OIDC Authentication + +To enable OIDC authentication for Kubernetes API: + +```bash +just k8s::setup-oidc-auth +``` + +This configures k3s to use Keycloak for user authentication. + +## Monitoring + +Enable Prometheus monitoring for Keycloak: + +```bash +just keycloak::enable-monitoring +``` + +This creates a ServiceMonitor that scrapes metrics from Keycloak's management port (9000). + +Metrics are automatically renamed from `vendor_*` to `keycloak_*` for better discoverability. + +## Troubleshooting + +### Check Keycloak Pod Status + +```bash +kubectl get pods -n keycloak +``` + +### View Keycloak Logs + +```bash +kubectl logs -n keycloak keycloak-0 +``` + +### Check Database Connection + +Verify PostgreSQL connection: + +```bash +kubectl get secret database-config -n keycloak -o yaml +``` + +### Reset Admin Password + +```bash +# Delete existing credentials +kubectl delete secret keycloak-credentials -n keycloak +kubectl delete secret keycloak-bootstrap-admin -n keycloak + +# Recreate with new password +just keycloak::create-credentials +``` + +## Management + +### Uninstall Keycloak Instance + +```bash +just keycloak::uninstall +``` + +This removes the Keycloak instance but keeps the Operator installed. + +To keep the database: + +```bash +just keycloak::uninstall delete-db=false +``` + +### Uninstall Keycloak Operator + +```bash +just keycloak::uninstall-operator +``` + +This removes the Operator and all CRDs. + +## References + +- [Keycloak Documentation](https://www.keycloak.org/documentation) +- [Keycloak Operator](https://www.keycloak.org/operator/installation) +- [Keycloak Admin REST API](https://www.keycloak.org/docs-api/latest/rest-api/) +- [OpenID Connect](https://openid.net/connect/) diff --git a/keycloak/justfile b/keycloak/justfile index 5877de1..d2280b8 100644 --- a/keycloak/justfile +++ b/keycloak/justfile @@ -4,7 +4,7 @@ set fallback := true # https://www.keycloak.org/operator/installation export KEYCLOAK_NAMESPACE := env("KEYCLOAK_NAMESPACE", "keycloak") -export KEYCLOAK_OPERATOR_VERSION := env("KEYCLOAK_OPERATOR_VERSION", "26.3.4") +export KEYCLOAK_OPERATOR_VERSION := env("KEYCLOAK_OPERATOR_VERSION", "26.4.5") export KEYCLOAK_REALM := env("KEYCLOAK_REALM", "") export KEYCLOAK_HOST := env("KEYCLOAK_HOST", "") export K8S_OIDC_CLIENT_ID := env('K8S_OIDC_CLIENT_ID', "k8s") @@ -108,6 +108,12 @@ install-operator: #!/bin/bash set -euo pipefail just create-namespace + + # Using 'baseline' instead of 'restricted' because Keycloak Operator does not meet + # restricted requirements + kubectl label namespace ${KEYCLOAK_NAMESPACE} \ + pod-security.kubernetes.io/enforce=baseline --overwrite + echo "Installing Keycloak Operator CRDs..." kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/${KEYCLOAK_OPERATOR_VERSION}/kubernetes/keycloaks.k8s.keycloak.org-v1.yml kubectl apply -f https://raw.githubusercontent.com/keycloak/keycloak-k8s-resources/${KEYCLOAK_OPERATOR_VERSION}/kubernetes/keycloakrealmimports.k8s.keycloak.org-v1.yml diff --git a/keycloak/keycloak-cr.gomplate.yaml b/keycloak/keycloak-cr.gomplate.yaml index e4ba666..e5c538d 100644 --- a/keycloak/keycloak-cr.gomplate.yaml +++ b/keycloak/keycloak-cr.gomplate.yaml @@ -5,7 +5,7 @@ metadata: namespace: {{ .Env.KEYCLOAK_NAMESPACE }} spec: instances: 1 - image: quay.io/keycloak/keycloak:26.3.4 + image: quay.io/keycloak/keycloak:26.4 startOptimized: false # Database configuration for external PostgreSQL @@ -37,34 +37,64 @@ spec: proxy: headers: xforwarded - # Additional options and admin configuration + # http-enabled and hostname-strict are configured via http.httpEnabled and hostname.strict additionalOptions: - - name: http-enabled - value: "true" - - name: hostname-strict - value: "false" - - name: hostname-strict-https - value: "false" - - name: proxy - value: edge - name: metrics-enabled value: "true" + # Keycloak takes ~20 seconds to start, so we configure probes accordingly + # Note: Keycloak Operator v2alpha1 only supports periodSeconds and failureThreshold + startupProbe: + periodSeconds: 10 + failureThreshold: 20 + + livenessProbe: + periodSeconds: 10 + failureThreshold: 3 + + readinessProbe: + periodSeconds: 5 + failureThreshold: 3 + # Bootstrap admin configuration bootstrapAdmin: user: secret: keycloak-bootstrap-admin # Resources + # Increased memory limit to 3Gi for Keycloak 26.4 build process resources: requests: - memory: "1.5Gi" + memory: "2Gi" cpu: "500m" limits: - memory: "2Gi" + memory: "3Gi" cpu: "1000m" - # Ingress configuration (disabled - using separate Ingress resource) + unsupported: + podTemplate: + spec: + securityContext: + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + seccompProfile: + type: RuntimeDefault + containers: + - name: keycloak + securityContext: + allowPrivilegeEscalation: false + readOnlyRootFilesystem: false + runAsNonRoot: true + runAsUser: 1000 + runAsGroup: 1000 + seccompProfile: + type: RuntimeDefault + capabilities: + drop: + - ALL + ingress: enabled: false @@ -95,4 +125,3 @@ spec: name: keycloak-service port: number: 8080 -