generated from coulomb/repo-seed
feat(sso-mfa): T05 Keycloak manifests (NK-WP-0001-T05)
Deploys Keycloak (SSO core) in the sso namespace.
Files:
sso-mfa/k8s/keycloak/pvc.yaml — keycloak-data PVC (build cache)
sso-mfa/k8s/keycloak/middleware.yaml — rate-limit, admin-allowlist, HSTS
sso-mfa/k8s/keycloak/deployment.yaml — Deployment + Service; init container
downloads privacyIDEA provider JAR
sso-mfa/k8s/keycloak/ingress.yaml — Ingress for kc.coulomb.social (CP-NK-004)
sso-mfa/k8s/keycloak/create-secrets.sh — keycloak-config Secret
sso-mfa/k8s/keycloak/bootstrap-realm.sh— hardens master realm, creates net-kingdom realm
sso-mfa/k8s/keycloak/README.md — apply order, custom image guide, DR
sso-mfa/k8s/verify-t05.sh — T05 done-criteria verification script
Config points added: CP-NK-004 (kc.coulomb.social), CP-NK-005 (provider JAR URL).
CP-NK-005 must be set before applying deployment.yaml.
Pending: apply to live cluster, set CP-NK-005, run bootstrap-realm.sh, verify-t05.sh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
47
CONFIG.md
47
CONFIG.md
@@ -23,6 +23,8 @@ If yes to any of the above, don't add it here.
|
||||
| CP-NK-001 | ACME contact email | `bernd.worsch+netkingdom@gmail.com` | `sso-mfa/k8s/cert-manager/issuers.yaml:38` |
|
||||
| CP-NK-002 | privacyIDEA portal hostname | `pink.coulomb.social` | `sso-mfa/k8s/privacyidea/ingress.yaml` |
|
||||
| CP-NK-003 | privacyIDEA self-service hostname | `pink-account.coulomb.social` | `sso-mfa/k8s/privacyidea/ingress.yaml` |
|
||||
| CP-NK-004 | Keycloak SSO hostname | `kc.coulomb.social` | `sso-mfa/k8s/keycloak/deployment.yaml`, `sso-mfa/k8s/keycloak/ingress.yaml` |
|
||||
| CP-NK-005 | privacyIDEA Keycloak Provider JAR URL | *(not set — edit before apply)* | `sso-mfa/k8s/keycloak/deployment.yaml` |
|
||||
|
||||
---
|
||||
|
||||
@@ -83,3 +85,48 @@ gains a structured "operator contact" concept.
|
||||
|
||||
**Scope:** All TLS certificates issued by the `letsencrypt-prod` ClusterIssuer across
|
||||
the entire cluster.
|
||||
|
||||
---
|
||||
|
||||
## CP-NK-004 — Keycloak SSO hostname
|
||||
|
||||
**Value:** `kc.coulomb.social`
|
||||
**Set:** 2026-03-19
|
||||
**Set by:** worsch
|
||||
|
||||
**Location(s):**
|
||||
- `sso-mfa/k8s/keycloak/deployment.yaml` — `KC_HOSTNAME` env var
|
||||
- `sso-mfa/k8s/keycloak/ingress.yaml` — both Ingress `host` fields
|
||||
|
||||
**Why non-default:** Subdomain prefix must be chosen by the operator. `kc` =
|
||||
**K**ey**c**loak, consistent with the service-initial naming pattern.
|
||||
|
||||
**Scope:** TLS certificate, Traefik routing, Keycloak's internal hostname strictness
|
||||
check, and all OIDC/SAML redirect URIs registered in this realm. Changing this
|
||||
hostname after clients are registered requires updating all registered redirect URIs.
|
||||
|
||||
---
|
||||
|
||||
## CP-NK-005 — privacyIDEA Keycloak Provider JAR URL
|
||||
|
||||
**Value:** *(not set — operator must edit before applying T05)*
|
||||
**Set:** —
|
||||
**Set by:** —
|
||||
|
||||
**Location(s):**
|
||||
- `sso-mfa/k8s/keycloak/deployment.yaml` — `PROVIDER_JAR_URL` env var in the
|
||||
`install-privacyidea-provider` init container
|
||||
|
||||
**Why non-default:** The JAR URL depends on the chosen release version, which must
|
||||
be verified for compatibility with the deployed Keycloak image version. There is no
|
||||
stable "latest" URL suitable for automation.
|
||||
|
||||
**How to set:**
|
||||
1. Browse https://github.com/privacyIDEA/keycloak-provider/releases
|
||||
2. Choose a release compatible with the Keycloak image version in `deployment.yaml`.
|
||||
3. Edit `deployment.yaml`: replace `EDIT_BEFORE_APPLY` with the `.jar` download URL.
|
||||
4. Update this entry with the chosen URL and version.
|
||||
|
||||
**Scope:** Keycloak init container only. If switching to a custom Keycloak image
|
||||
(see T05 README "Custom image" section), this config point becomes obsolete and
|
||||
can be removed.
|
||||
|
||||
203
sso-mfa/k8s/keycloak/README.md
Normal file
203
sso-mfa/k8s/keycloak/README.md
Normal file
@@ -0,0 +1,203 @@
|
||||
# T05 — Phase 4: Deploy Keycloak
|
||||
|
||||
Phase 4 of NK-WP-0001: deploys the SSO core (Keycloak) in the `sso` namespace.
|
||||
|
||||
**Hostname (config point CP-NK-004):**
|
||||
- `kc.coulomb.social` — OIDC/SAML SSO portal, admin console
|
||||
|
||||
**Prerequisites:**
|
||||
- T02 complete: `sso` namespace and NetworkPolicies applied, cert-manager running.
|
||||
- T03 complete: PostgreSQL cluster `net-kingdom-pg` in `databases` namespace is Ready.
|
||||
- T04 complete: privacyIDEA is Running; `bootstrap-admin.sh` has been run so the
|
||||
`privacyidea-trigger-admin` Secret exists in the `mfa` namespace.
|
||||
- T01 Phase 0a complete: `gen-secrets.sh` run, all secrets in KeePassXC.
|
||||
|
||||
---
|
||||
|
||||
## Before you apply: two required edits
|
||||
|
||||
### Edit 1 — Provider JAR URL (CP-NK-005, required)
|
||||
|
||||
The init container in `deployment.yaml` downloads the privacyIDEA Keycloak Provider
|
||||
JAR. You must set the URL before applying:
|
||||
|
||||
1. Go to https://github.com/privacyIDEA/keycloak-provider/releases
|
||||
2. Download the JAR for a release compatible with your Keycloak image version.
|
||||
3. Edit `deployment.yaml`: find `PROVIDER_JAR_URL` and replace `EDIT_BEFORE_APPLY`
|
||||
with the real URL.
|
||||
4. Add the URL as CP-NK-005 in `CONFIG.md` (see bottom of this README).
|
||||
|
||||
If your cluster has no egress internet access, see **Custom image** below.
|
||||
|
||||
### Edit 2 — Admin console IP allowlist (optional but recommended)
|
||||
|
||||
Edit `middleware.yaml`: update `keycloak-admin-allowlist.spec.ipAllowList.sourceRange`
|
||||
to your actual VPN/office CIDRs.
|
||||
|
||||
---
|
||||
|
||||
## Apply order
|
||||
|
||||
### Step 1 — Create secrets
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s/keycloak
|
||||
chmod +x create-secrets.sh bootstrap-realm.sh
|
||||
./create-secrets.sh
|
||||
```
|
||||
|
||||
Creates `keycloak-config` in the `sso` namespace (KC_DB_URL, KC_DB_PASSWORD,
|
||||
KC_BOOTSTRAP_ADMIN_PASSWORD).
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Set provider JAR URL and apply manifests
|
||||
|
||||
After editing `PROVIDER_JAR_URL` in `deployment.yaml`:
|
||||
|
||||
```bash
|
||||
# From sso-mfa/k8s/keycloak/
|
||||
kubectl apply -f pvc.yaml
|
||||
kubectl apply -f middleware.yaml
|
||||
kubectl apply -f deployment.yaml
|
||||
kubectl apply -f ingress.yaml
|
||||
```
|
||||
|
||||
**Wait for the pod to reach Running+Ready** (DB migrations + provider build on first
|
||||
boot — allow up to 5 minutes):
|
||||
|
||||
```bash
|
||||
kubectl get pods -n sso -w
|
||||
# Expected: keycloak-<hash> 1/1 Running
|
||||
```
|
||||
|
||||
If the pod is stuck in `Init`, check the init container logs first:
|
||||
```bash
|
||||
kubectl logs -n sso -l app.kubernetes.io/name=keycloak -c install-privacyidea-provider
|
||||
```
|
||||
|
||||
Common causes of `Init` failure:
|
||||
- `PROVIDER_JAR_URL` still set to `EDIT_BEFORE_APPLY` → edit and reapply
|
||||
- No egress internet access → use custom image (see below)
|
||||
|
||||
If the pod is in `CrashLoopBackOff`, check main container logs:
|
||||
```bash
|
||||
kubectl logs -n sso -l app.kubernetes.io/name=keycloak --previous
|
||||
```
|
||||
|
||||
Common causes of Keycloak crash:
|
||||
- `keycloak-config` Secret missing → run `create-secrets.sh`
|
||||
- PostgreSQL not reachable → verify T03, check NetworkPolicies
|
||||
- Wrong DB password → re-run `create-secrets.sh` with corrected secrets
|
||||
- `KC_DB_URL` format incorrect → must be `jdbc:postgresql://...` (not SQLAlchemy format)
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Bootstrap realm
|
||||
|
||||
After the pod is Running and Ready:
|
||||
|
||||
```bash
|
||||
./bootstrap-realm.sh
|
||||
```
|
||||
|
||||
This:
|
||||
1. Authenticates to the Keycloak admin REST API inside the pod.
|
||||
2. Hardens the master realm (SSL required, brute-force protection, token lifetimes).
|
||||
3. Creates the `net-kingdom` application realm with equivalent hardening.
|
||||
|
||||
**Immediately after bootstrap completes:**
|
||||
1. Log in to `https://kc.coulomb.social/admin` as `admin`.
|
||||
2. Create a permanent admin account (with MFA — configure MFA flow in T06 first).
|
||||
3. Disable or delete the bootstrap `admin` account once the permanent admin is enrolled.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Verify
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s
|
||||
chmod +x verify-t05.sh
|
||||
./verify-t05.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Custom image (recommended for production)
|
||||
|
||||
The init-container approach downloads the provider JAR from the internet on every pod
|
||||
restart. For production or air-gapped clusters, build a custom image:
|
||||
|
||||
```dockerfile
|
||||
FROM quay.io/keycloak/keycloak:26.0
|
||||
# Add the privacyIDEA provider JAR (download from GitHub releases first)
|
||||
COPY keycloak-provider-VERSION.jar /opt/keycloak/providers/
|
||||
# Build an optimized image — this bakes in the provider at build time
|
||||
RUN /opt/keycloak/bin/kc.sh build
|
||||
```
|
||||
|
||||
Push to your registry, then:
|
||||
1. Update `deployment.yaml`: change the `keycloak` container `image` to your custom image.
|
||||
2. Change `args: ["start"]` to `args: ["start", "--optimized"]` for faster startup.
|
||||
3. Remove the `install-privacyidea-provider` init container entirely.
|
||||
4. The `providers` emptyDir volume and its mount can also be removed.
|
||||
|
||||
---
|
||||
|
||||
## NetworkPolicy design
|
||||
|
||||
Keycloak sits behind the NetworkPolicies applied in T02 (netpol-sso.yaml):
|
||||
|
||||
| Source | Destination | Port | Purpose |
|
||||
|--------|-------------|------|---------|
|
||||
| Traefik (kube-system) | Keycloak (sso) | 8080 | OIDC/SAML login pages |
|
||||
| Keycloak (sso) | privacyIDEA (mfa) | 8080 | MFA challenge API (T06) |
|
||||
| Keycloak (sso) | PostgreSQL (databases) | 5432 | Database |
|
||||
|
||||
Outbound to anything other than privacyIDEA, PostgreSQL, and kube-dns is denied.
|
||||
|
||||
---
|
||||
|
||||
## Post-deploy steps (after verify-t05.sh passes)
|
||||
|
||||
### Rotate the bootstrap admin password
|
||||
|
||||
The `KC_BOOTSTRAP_ADMIN_PASSWORD` in `keycloak-config` was the initial credential.
|
||||
After creating a permanent admin:
|
||||
|
||||
1. In Keycloak admin console: Users → admin → Disable account (or delete it).
|
||||
2. Rotate `KC_BOOTSTRAP_ADMIN_PASSWORD` in KeePassXC and re-run `create-secrets.sh`.
|
||||
3. Restart the Keycloak deployment: `kubectl rollout restart deployment/keycloak -n sso`
|
||||
|
||||
### Admin console IP restriction
|
||||
|
||||
Update `middleware.yaml` `keycloak-admin-allowlist.spec.ipAllowList.sourceRange`
|
||||
to your actual VPN/office CIDRs:
|
||||
|
||||
```bash
|
||||
kubectl apply -f middleware.yaml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Adding CP-NK-005 to CONFIG.md
|
||||
|
||||
After setting the provider JAR URL, add it to `CONFIG.md` as CP-NK-005:
|
||||
|
||||
```markdown
|
||||
| CP-NK-005 | privacyIDEA provider JAR URL | <the URL you used> | `sso-mfa/k8s/keycloak/deployment.yaml` |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Disaster recovery
|
||||
|
||||
If the `keycloak-data` PVC is lost (build cache only — all state is in PostgreSQL):
|
||||
|
||||
1. Create a new PVC with `pvc.yaml`.
|
||||
2. Restart the deployment — Keycloak will rebuild from PostgreSQL on first start.
|
||||
Allow 3–5 minutes for the rebuild + DB reconnect.
|
||||
3. Realm config, clients, and users are preserved in the database.
|
||||
|
||||
If the PostgreSQL `keycloak_db` database is lost, restore from the CNPG backup
|
||||
(T03 procedure) before restarting Keycloak.
|
||||
150
sso-mfa/k8s/keycloak/bootstrap-realm.sh
Executable file
150
sso-mfa/k8s/keycloak/bootstrap-realm.sh
Executable file
@@ -0,0 +1,150 @@
|
||||
#!/usr/bin/env bash
|
||||
# bootstrap-realm.sh — Phase 1 Keycloak bootstrap for T05
|
||||
#
|
||||
# Runs AFTER the Keycloak pod is Running and Ready (verify with verify-t05.sh).
|
||||
#
|
||||
# Actions (T05 scope):
|
||||
# 1. Authenticate to Keycloak admin REST API via kcadm.sh in the pod
|
||||
# 2. Harden master realm: brute-force protection, token lifetimes, SSL required
|
||||
# 3. Create the net-kingdom application realm
|
||||
# 4. Enable realm-level brute-force protection and SSL
|
||||
#
|
||||
# Out of scope for T05 (handled in T06):
|
||||
# - privacyIDEA authentication flow configuration
|
||||
# - User federation / LDAP resolver setup
|
||||
# - Per-client OIDC settings
|
||||
# - Break-glass admin account (T07)
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x bootstrap-realm.sh
|
||||
# ./bootstrap-realm.sh [--realm-name NAME]
|
||||
#
|
||||
# Options:
|
||||
# --realm-name NAME Name for the application realm (default: net-kingdom)
|
||||
#
|
||||
# Prerequisites:
|
||||
# - kubectl configured with cluster access
|
||||
# - Keycloak pod Running+Ready in the sso namespace
|
||||
# - KC_ADMIN_PASSWORD available (prompted interactively if not set as env var)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="sso"
|
||||
REALM_NAME="net-kingdom"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--realm-name) REALM_NAME="$2"; shift 2 ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# ── Find the running Keycloak pod ─────────────────────────────────────────────
|
||||
KC_POD=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=keycloak \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$KC_POD" ]]; then
|
||||
echo "ERROR: No running Keycloak pod found in namespace $NAMESPACE." >&2
|
||||
echo "Run verify-t05.sh to diagnose." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Using pod: $KC_POD"
|
||||
|
||||
# ── Admin password ────────────────────────────────────────────────────────────
|
||||
if [[ -z "${KC_ADMIN_PASSWORD:-}" ]]; then
|
||||
echo -n "Keycloak admin password (KC_ADMIN_PASSWORD): "
|
||||
read -rs KC_ADMIN_PASSWORD
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ -z "$KC_ADMIN_PASSWORD" ]]; then
|
||||
echo "ERROR: KC_ADMIN_PASSWORD is required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ── Helper: run kcadm.sh inside the pod ──────────────────────────────────────
|
||||
kcadm() {
|
||||
kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
/opt/keycloak/bin/kcadm.sh "$@"
|
||||
}
|
||||
|
||||
# ── 1. Authenticate ───────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Authenticating ───────────────────────────────────────────────────────"
|
||||
kcadm config credentials \
|
||||
--server http://localhost:8080 \
|
||||
--realm master \
|
||||
--user admin \
|
||||
--password "$KC_ADMIN_PASSWORD"
|
||||
|
||||
echo " Authenticated as admin@master"
|
||||
|
||||
# ── 2. Harden master realm ────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "── Hardening master realm ───────────────────────────────────────────────"
|
||||
|
||||
# Require SSL for all connections to master realm (external access requires HTTPS).
|
||||
kcadm update realms/master \
|
||||
--set 'sslRequired=external'
|
||||
echo " sslRequired=external (master)"
|
||||
|
||||
# Brute-force protection: lock after 5 failures for 5 minutes.
|
||||
kcadm update realms/master \
|
||||
--set 'bruteForceProtected=true' \
|
||||
--set 'failureFactor=5' \
|
||||
--set 'waitIncrementSeconds=60' \
|
||||
--set 'maxFailureWaitSeconds=900' \
|
||||
--set 'minimumQuickLoginWaitSeconds=60'
|
||||
echo " Brute-force protection enabled (master)"
|
||||
|
||||
# Shorten access token lifetime for security.
|
||||
kcadm update realms/master \
|
||||
--set 'accessTokenLifespan=300' # 5 min
|
||||
echo " Access token lifetime: 300s (master)"
|
||||
|
||||
# ── 3. Create the net-kingdom application realm ───────────────────────────────
|
||||
echo ""
|
||||
echo "── Creating realm: $REALM_NAME ─────────────────────────────────────────"
|
||||
|
||||
if kcadm get realms/"$REALM_NAME" &>/dev/null; then
|
||||
echo " Realm $REALM_NAME already exists — skipping creation."
|
||||
else
|
||||
kcadm create realms \
|
||||
--set "realm=$REALM_NAME" \
|
||||
--set 'enabled=true' \
|
||||
--set 'displayName=net-kingdom' \
|
||||
--set 'sslRequired=external' \
|
||||
--set 'registrationAllowed=false' \
|
||||
--set 'bruteForceProtected=true' \
|
||||
--set 'failureFactor=5' \
|
||||
--set 'waitIncrementSeconds=60' \
|
||||
--set 'maxFailureWaitSeconds=900' \
|
||||
--set 'minimumQuickLoginWaitSeconds=60' \
|
||||
--set 'accessTokenLifespan=300' \
|
||||
--set 'refreshTokenMaxReuse=0' \
|
||||
--set 'revokeRefreshToken=true'
|
||||
echo " Realm $REALM_NAME created."
|
||||
fi
|
||||
|
||||
# ── 4. Summary ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " T05 realm bootstrap complete."
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "What was done:"
|
||||
echo " - master realm: SSL required, brute-force protection, short token lifetimes"
|
||||
echo " - realm '$REALM_NAME' created with equivalent hardening"
|
||||
echo ""
|
||||
echo "Next steps (T06):"
|
||||
echo " 1. Log in to https://kc.coulomb.social/admin with the bootstrap admin."
|
||||
echo " Immediately create a permanent admin account, then delete/disable"
|
||||
echo " the bootstrap 'admin' account (or rotate KC_BOOTSTRAP_ADMIN_PASSWORD)."
|
||||
echo " 2. In realm '$REALM_NAME':"
|
||||
echo " - Configure the privacyIDEA authentication flow (see T06 README)."
|
||||
echo " - Set privacyIDEA base URL: https://pink.coulomb.social"
|
||||
echo " - Set trigger-admin credentials from the privacyidea-trigger-admin Secret."
|
||||
echo " 3. Run verify-t05.sh to confirm all T05 done-criteria."
|
||||
65
sso-mfa/k8s/keycloak/create-secrets.sh
Executable file
65
sso-mfa/k8s/keycloak/create-secrets.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# create-secrets.sh — create the keycloak-config K8s Secret
|
||||
#
|
||||
# Usage:
|
||||
# ./create-secrets.sh [secrets-dir]
|
||||
#
|
||||
# <secrets-dir> is the output directory from sso-mfa/bootstrap/gen-secrets.sh
|
||||
# (default: ../../bootstrap/secrets).
|
||||
#
|
||||
# Creates ONE Secret in the sso namespace:
|
||||
# keycloak-config — KC_DB_URL, KC_DB_PASSWORD, KC_BOOTSTRAP_ADMIN_PASSWORD
|
||||
#
|
||||
# This secret must exist before applying deployment.yaml.
|
||||
#
|
||||
# Re-run with --rotate to update secrets after a rotation in KeePassXC.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SECRETS_DIR="${1:-../../bootstrap/secrets}"
|
||||
KC_ENV="$SECRETS_DIR/keycloak/secrets.env"
|
||||
PG_ENV="$SECRETS_DIR/postgres/secrets.env"
|
||||
|
||||
if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||
echo "ERROR: secrets directory not found: $SECRETS_DIR" >&2
|
||||
echo "Run sso-mfa/bootstrap/gen-secrets.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
for f in "$KC_ENV" "$PG_ENV"; do
|
||||
if [[ ! -f "$f" ]]; then
|
||||
echo "ERROR: $f not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# Read values from the generated env files in subshells to avoid polluting env.
|
||||
KC_ADMIN_PASSWORD=$(bash -c "source '$KC_ENV' 2>/dev/null; echo \$KC_ADMIN_PASSWORD")
|
||||
KC_DB_PASSWORD=$(bash -c "source '$KC_ENV' 2>/dev/null; echo \$KC_DB_PASSWORD")
|
||||
|
||||
if [[ -z "$KC_ADMIN_PASSWORD" || -z "$KC_DB_PASSWORD" ]]; then
|
||||
echo "ERROR: could not read KC_ADMIN_PASSWORD or KC_DB_PASSWORD from $KC_ENV" >&2
|
||||
echo "Check that gen-secrets.sh ran successfully." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct the JDBC database URL.
|
||||
# CloudNativePG read-write service: net-kingdom-pg-rw.databases.svc.cluster.local
|
||||
# Keycloak uses JDBC format (jdbc:postgresql://...) — NOT the SQLAlchemy URI format.
|
||||
KC_DB_URL="jdbc:postgresql://net-kingdom-pg-rw.databases.svc.cluster.local:5432/keycloak_db"
|
||||
|
||||
echo "Creating K8s Secret: keycloak-config (namespace: sso)"
|
||||
kubectl create secret generic keycloak-config \
|
||||
--namespace=sso \
|
||||
--from-literal=KC_DB_URL="$KC_DB_URL" \
|
||||
--from-literal=KC_DB_PASSWORD="$KC_DB_PASSWORD" \
|
||||
--from-literal=KC_BOOTSTRAP_ADMIN_PASSWORD="$KC_ADMIN_PASSWORD" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo ""
|
||||
echo "Done. Secret keycloak-config created in namespace: sso"
|
||||
echo ""
|
||||
echo "Next:"
|
||||
echo " 1. Edit deployment.yaml: set PROVIDER_JAR_URL to the privacyIDEA provider JAR URL (CP-NK-005)."
|
||||
echo " 2. Apply manifests (see README.md apply order)."
|
||||
echo " 3. After the pod is Running+Ready, run: ./bootstrap-realm.sh"
|
||||
251
sso-mfa/k8s/keycloak/deployment.yaml
Normal file
251
sso-mfa/k8s/keycloak/deployment.yaml
Normal file
@@ -0,0 +1,251 @@
|
||||
# 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
|
||||
87
sso-mfa/k8s/keycloak/ingress.yaml
Normal file
87
sso-mfa/k8s/keycloak/ingress.yaml
Normal file
@@ -0,0 +1,87 @@
|
||||
# Ingress — Keycloak (namespace: sso)
|
||||
#
|
||||
# kc.coulomb.social — SSO portal, OIDC/SAML endpoints, user login
|
||||
#
|
||||
# TLS: cert-manager issues the certificate via the letsencrypt-prod ClusterIssuer
|
||||
# (T02). Public DNS for kc.coulomb.social must resolve to the cluster's external
|
||||
# IP before cert-manager can complete the ACME HTTP-01 challenge.
|
||||
#
|
||||
# Middlewares:
|
||||
# keycloak-rate-limit — 100 req/min per IP (all paths)
|
||||
# keycloak-admin-allowlist — restrict /admin/* to VPN/office IPs (see middleware.yaml)
|
||||
# keycloak-hsts — inject HSTS header on all HTTPS responses
|
||||
#
|
||||
# Config points (see CONFIG.md):
|
||||
# CP-NK-004 kc.coulomb.social
|
||||
|
||||
# ── Main portal — kc.coulomb.social ──────────────────────────────────────────
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: keycloak
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/name: keycloak
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.middlewares: >-
|
||||
sso-keycloak-rate-limit@kubernetescrd,
|
||||
sso-keycloak-hsts@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: kc.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: keycloak
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- secretName: kc-tls
|
||||
hosts:
|
||||
- kc.coulomb.social
|
||||
---
|
||||
# ── Admin console — kc.coulomb.social/admin — restricted to VPN/office IPs ──
|
||||
# Separate Ingress so the admin-allowlist middleware applies only to /admin/*.
|
||||
# Traefik prefers the more-specific /admin prefix over the / prefix above.
|
||||
#
|
||||
# The admin console path in Keycloak 20+ is /admin/.
|
||||
# Realm-specific admin REST API is at /admin/realms/{realm}/...
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: keycloak-admin
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/name: keycloak
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.middlewares: >-
|
||||
sso-keycloak-rate-limit@kubernetescrd,
|
||||
sso-keycloak-admin-allowlist@kubernetescrd,
|
||||
sso-keycloak-hsts@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: kc.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /admin
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: keycloak
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- secretName: kc-tls
|
||||
hosts:
|
||||
- kc.coulomb.social
|
||||
71
sso-mfa/k8s/keycloak/middleware.yaml
Normal file
71
sso-mfa/k8s/keycloak/middleware.yaml
Normal file
@@ -0,0 +1,71 @@
|
||||
# Traefik Middlewares for Keycloak (namespace: sso)
|
||||
#
|
||||
# Middleware names follow the pattern referenced in ingress.yaml annotations:
|
||||
# sso-keycloak-rate-limit@kubernetescrd
|
||||
# sso-keycloak-admin-allowlist@kubernetescrd
|
||||
# sso-keycloak-hsts@kubernetescrd
|
||||
#
|
||||
# Traefik API version:
|
||||
# Traefik v3 (K3s >= 1.30): traefik.io/v1alpha1
|
||||
# Traefik v2 (K3s < 1.30): traefik.containo.us/v1alpha1
|
||||
# Check: kubectl get middleware -n sso -o yaml | grep apiVersion
|
||||
# Update all documents below if you need the v2 apiVersion.
|
||||
|
||||
# ── Rate limit — all KC endpoints ────────────────────────────────────────────
|
||||
# 100 requests/minute per client IP; burst of 20 allowed.
|
||||
# Higher than privacyIDEA because OIDC discovery + JS app calls are bursty.
|
||||
# The /realms/{realm}/.well-known/openid-configuration call alone counts.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: keycloak-rate-limit
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
spec:
|
||||
rateLimit:
|
||||
average: 100
|
||||
period: 1m
|
||||
burst: 20
|
||||
---
|
||||
# ── Admin console allowlist — restrict /admin to VPN/office IPs ──────────────
|
||||
# Applied to the /admin Ingress (see ingress.yaml — separate Ingress for /admin/).
|
||||
#
|
||||
# ADJUST sourceRange to your actual VPN / office CIDR(s) before going live.
|
||||
# Leaving RFC-1918 ranges here is only a dev/staging default.
|
||||
#
|
||||
# Traefik v3 uses ipAllowList; Traefik v2 uses ipWhiteList.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: keycloak-admin-allowlist
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
spec:
|
||||
ipAllowList:
|
||||
# EDIT: replace with your VPN/office CIDRs (see CONFIG.md for the pattern).
|
||||
sourceRange:
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
---
|
||||
# ── HSTS — HTTP Strict Transport Security ────────────────────────────────────
|
||||
# Keycloak docs recommend HSTS for all deployments.
|
||||
# Traefik terminates TLS; Keycloak runs HTTP internally.
|
||||
# This header is injected by Traefik on all HTTPS responses.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: keycloak-hsts
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
spec:
|
||||
headers:
|
||||
stsSeconds: 31536000 # 1 year
|
||||
stsIncludeSubdomains: true
|
||||
stsPreload: true
|
||||
22
sso-mfa/k8s/keycloak/pvc.yaml
Normal file
22
sso-mfa/k8s/keycloak/pvc.yaml
Normal file
@@ -0,0 +1,22 @@
|
||||
# PersistentVolumeClaim for Keycloak (namespace: sso)
|
||||
#
|
||||
# keycloak-data — /opt/keycloak/data
|
||||
# Holds: Keycloak build cache (data/generated/) produced by kc.sh build.
|
||||
# Persisting this avoids a full provider rebuild on every pod restart.
|
||||
# Also holds H2 emergency data (only used if PostgreSQL is unreachable).
|
||||
#
|
||||
# Adjust storage size before production deployment.
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: keycloak-data
|
||||
namespace: sso
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: sso
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
220
sso-mfa/k8s/verify-t05.sh
Executable file
220
sso-mfa/k8s/verify-t05.sh
Executable file
@@ -0,0 +1,220 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-t05.sh — verify NK-WP-0001-T05 done-criteria
|
||||
#
|
||||
# Checks:
|
||||
# 1. Keycloak pod is Running+Ready in the sso namespace
|
||||
# 2. Keycloak Service exists on port 8080
|
||||
# 3. Traefik Middlewares exist
|
||||
# 4. Ingress resources exist with correct hostname
|
||||
# 5. TLS certificate issued by cert-manager
|
||||
# 6. Required K8s Secrets are present
|
||||
# 7. PVC is Bound
|
||||
# 8. Provider JAR is present inside the pod
|
||||
# 9. Keycloak health endpoints respond (started, ready)
|
||||
# 10. net-kingdom realm exists
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x verify-t05.sh
|
||||
# ./verify-t05.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="sso"
|
||||
KC_HOSTNAME="kc.coulomb.social"
|
||||
REALM_NAME="net-kingdom"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
pass() { echo " [PASS] $1"; ((PASS++)); }
|
||||
fail() { echo " [FAIL] $1"; ((FAIL++)); }
|
||||
warn() { echo " [WARN] $1"; ((WARN++)); }
|
||||
|
||||
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
|
||||
|
||||
# ── 1. Keycloak pod ───────────────────────────────────────────────────────────
|
||||
section "1. Keycloak pod (namespace: $NAMESPACE)"
|
||||
|
||||
KC_POD=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=keycloak \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$KC_POD" ]]; then
|
||||
pass "Pod Running: $KC_POD"
|
||||
|
||||
READY=$(kubectl get pod -n "$NAMESPACE" "$KC_POD" \
|
||||
-o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false")
|
||||
if [[ "$READY" == "true" ]]; then
|
||||
pass "Pod readiness probe passing"
|
||||
else
|
||||
fail "Pod is Running but not Ready (probe failing — check logs)"
|
||||
fi
|
||||
else
|
||||
PENDING=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=keycloak \
|
||||
-o name 2>/dev/null | wc -l || echo 0)
|
||||
if [[ "$PENDING" -gt 0 ]]; then
|
||||
fail "Keycloak pod(s) exist but none are Running (check: kubectl describe pod -n $NAMESPACE)"
|
||||
else
|
||||
fail "No Keycloak pods found in namespace $NAMESPACE — apply deployment.yaml"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 2. Service ────────────────────────────────────────────────────────────────
|
||||
section "2. Service"
|
||||
|
||||
if kubectl get service keycloak -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Service keycloak exists"
|
||||
PORT=$(kubectl get service keycloak -n "$NAMESPACE" \
|
||||
-o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "")
|
||||
if [[ "$PORT" == "8080" ]]; then
|
||||
pass "Service port: 8080"
|
||||
else
|
||||
warn "Service port is $PORT (expected 8080)"
|
||||
fi
|
||||
else
|
||||
fail "Service keycloak not found in namespace $NAMESPACE"
|
||||
fi
|
||||
|
||||
# ── 3. Traefik Middlewares ────────────────────────────────────────────────────
|
||||
section "3. Traefik Middlewares"
|
||||
|
||||
for mw in keycloak-rate-limit keycloak-admin-allowlist keycloak-hsts; do
|
||||
if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Middleware $mw exists"
|
||||
else
|
||||
fail "Middleware $mw not found — apply middleware.yaml"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 4. Ingress resources ──────────────────────────────────────────────────────
|
||||
section "4. Ingress resources"
|
||||
|
||||
for ing in keycloak keycloak-admin; do
|
||||
if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Ingress $ing exists"
|
||||
else
|
||||
fail "Ingress $ing not found — apply ingress.yaml"
|
||||
fi
|
||||
done
|
||||
|
||||
KC_HOST=$(kubectl get ingress keycloak -n "$NAMESPACE" \
|
||||
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
|
||||
if [[ "$KC_HOST" == "$KC_HOSTNAME" ]]; then
|
||||
pass "Ingress host: $KC_HOSTNAME"
|
||||
else
|
||||
fail "Ingress host is '$KC_HOST' (expected $KC_HOSTNAME)"
|
||||
fi
|
||||
|
||||
# ── 5. TLS certificate ────────────────────────────────────────────────────────
|
||||
section "5. TLS certificate"
|
||||
|
||||
if kubectl get secret kc-tls -n "$NAMESPACE" &>/dev/null; then
|
||||
CERT_READY=$(kubectl get certificate kc -n "$NAMESPACE" \
|
||||
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
|
||||
if [[ "$CERT_READY" == "True" ]]; then
|
||||
pass "Certificate kc is Ready (TLS secret kc-tls exists)"
|
||||
else
|
||||
warn "TLS secret kc-tls exists but certificate status is not Ready (DNS propagation pending?)"
|
||||
fi
|
||||
else
|
||||
warn "TLS secret kc-tls not yet issued (cert-manager pending — check DNS and ACME)"
|
||||
fi
|
||||
|
||||
# ── 6. K8s Secrets ────────────────────────────────────────────────────────────
|
||||
section "6. K8s Secrets (namespace: $NAMESPACE)"
|
||||
|
||||
if kubectl get secret keycloak-config -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Secret keycloak-config exists"
|
||||
else
|
||||
fail "Secret keycloak-config not found — run create-secrets.sh"
|
||||
fi
|
||||
|
||||
# ── 7. PVC ────────────────────────────────────────────────────────────────────
|
||||
section "7. PersistentVolumeClaim"
|
||||
|
||||
STATUS=$(kubectl get pvc keycloak-data -n "$NAMESPACE" \
|
||||
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
|
||||
if [[ "$STATUS" == "Bound" ]]; then
|
||||
pass "PVC keycloak-data: Bound"
|
||||
elif [[ "$STATUS" == "not found" ]]; then
|
||||
fail "PVC keycloak-data not found — apply pvc.yaml"
|
||||
else
|
||||
fail "PVC keycloak-data status: $STATUS (expected Bound)"
|
||||
fi
|
||||
|
||||
# ── 8. Provider JAR ───────────────────────────────────────────────────────────
|
||||
section "8. privacyIDEA provider JAR (inside pod)"
|
||||
|
||||
if [[ -n "$KC_POD" ]]; then
|
||||
if kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
test -f /opt/keycloak/providers/keycloak-provider.jar 2>/dev/null; then
|
||||
pass "Provider JAR present: /opt/keycloak/providers/keycloak-provider.jar"
|
||||
else
|
||||
fail "Provider JAR not found — check init container logs: kubectl logs -n $NAMESPACE $KC_POD -c install-privacyidea-provider"
|
||||
fi
|
||||
|
||||
# Check that the provider was picked up (appears in the build output directory)
|
||||
if kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
ls /opt/keycloak/data/generated/ 2>/dev/null | grep -q .; then
|
||||
pass "Keycloak build cache present (provider build completed)"
|
||||
else
|
||||
warn "Build cache empty — Keycloak may still be building (check pod logs)"
|
||||
fi
|
||||
else
|
||||
warn "Skipping provider JAR check — no running pod"
|
||||
fi
|
||||
|
||||
# ── 9. Health endpoints ───────────────────────────────────────────────────────
|
||||
section "9. Keycloak health endpoints (via kubectl exec)"
|
||||
|
||||
if [[ -n "$KC_POD" ]]; then
|
||||
for endpoint in /health/started /health/ready /health/live; do
|
||||
STATUS_CODE=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
curl -s -o /dev/null -w "%{http_code}" "http://localhost:9000${endpoint}" \
|
||||
2>/dev/null || echo "")
|
||||
if [[ "$STATUS_CODE" == "200" ]]; then
|
||||
pass "GET localhost:9000${endpoint} → 200"
|
||||
else
|
||||
fail "GET localhost:9000${endpoint} → $STATUS_CODE (expected 200)"
|
||||
fi
|
||||
done
|
||||
else
|
||||
warn "Skipping health endpoint checks — no running pod"
|
||||
fi
|
||||
|
||||
# ── 10. Realm exists ──────────────────────────────────────────────────────────
|
||||
section "10. Keycloak realm: $REALM_NAME"
|
||||
|
||||
if [[ -n "$KC_POD" ]]; then
|
||||
REALM_STATUS=$(kubectl exec -n "$NAMESPACE" "$KC_POD" -- \
|
||||
curl -s -o /dev/null -w "%{http_code}" \
|
||||
"http://localhost:8080/realms/${REALM_NAME}" 2>/dev/null || echo "")
|
||||
if [[ "$REALM_STATUS" == "200" ]]; then
|
||||
pass "Realm $REALM_NAME exists and responds"
|
||||
elif [[ "$REALM_STATUS" == "404" ]]; then
|
||||
warn "Realm $REALM_NAME not found — run bootstrap-realm.sh"
|
||||
else
|
||||
warn "Realm $REALM_NAME check returned HTTP $REALM_STATUS"
|
||||
fi
|
||||
else
|
||||
warn "Skipping realm check — no running pod"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " T05 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T06"
|
||||
exit 1
|
||||
elif [[ "$WARN" -gt 0 ]]; then
|
||||
echo " Result: PARTIAL — T05 core is up; WARN items should be resolved before T06"
|
||||
exit 0
|
||||
else
|
||||
echo " Result: COMPLETE — T05 done-criteria met; proceed to T06 (realm config & MFA flow)"
|
||||
exit 0
|
||||
fi
|
||||
Reference in New Issue
Block a user