|
| 1 | +# Runbook: Rename the live Keycloak realm `0mpc` → `0mcp` |
| 2 | + |
| 3 | +**Status:** Ready to execute (requires a maintenance window — platform-wide SSO downtime). |
| 4 | +**Owner:** Platform operator. |
| 5 | +**Related:** ADR 0488 (single deployment), the `0mpc.com → 0mcp.com` domain migration (release 0.179.48). |
| 6 | + |
| 7 | +--- |
| 8 | + |
| 9 | +## 1. Why this is needed |
| 10 | + |
| 11 | +The domain was renamed `0mpc.com → 0mcp.com`. The platform derives the Keycloak |
| 12 | +realm name from the domain: |
| 13 | + |
| 14 | +```yaml |
| 15 | +# inventory/group_vars/all/identity.yml |
| 16 | +keycloak_realm_name: "{{ platform_domain | split('.') | first }}" # now "0mcp" |
| 17 | +keycloak_oidc_issuer_url: "https://sso.{{ platform_domain }}/realms/{{ keycloak_realm_name }}" |
| 18 | +``` |
| 19 | +
|
| 20 | +So **config now expects realm `0mcp`**, but the **live realm is still `0mpc`**: |
| 21 | + |
| 22 | +``` |
| 23 | +GET https://sso.0mcp.com/realms/0mpc/.well-known/openid-configuration → 200 |
| 24 | +GET https://sso.0mcp.com/realms/0mcp/.well-known/openid-configuration → 404 |
| 25 | +``` |
| 26 | +
|
| 27 | +Every oauth2-proxy / OIDC client issuer URL is templated from |
| 28 | +`keycloak_realm_name`, so until the live realm matches `0mcp`, the next edge / |
| 29 | +keycloak converge will point services at a realm that does not exist. |
| 30 | +
|
| 31 | +## 2. ⚠️ Why you cannot "just converge" |
| 32 | +
|
| 33 | +`roles/keycloak_runtime/tasks/main.yml` uses: |
| 34 | +
|
| 35 | +```yaml |
| 36 | +- name: Ensure platform identity realm exists with hardened defaults |
| 37 | + community.general.keycloak_realm: |
| 38 | + realm: "{{ keycloak_realm_name }}" # 0mcp |
| 39 | + id: "{{ keycloak_realm_name }}" # 0mcp |
| 40 | +``` |
| 41 | + |
| 42 | +Running `make converge-keycloak` **without renaming first** would **create a new, |
| 43 | +empty `0mcp` realm** and leave the populated `0mpc` realm orphaned — losing every |
| 44 | +user, client, group, role mapping, and service account. **Do the live rename |
| 45 | +first, then converge.** |
| 46 | + |
| 47 | +Keycloak nuance: a realm's internal `id` is immutable. The PUT-rename below |
| 48 | +changes the realm *name* (the segment used in all `/realms/<name>/...` URLs) |
| 49 | +while the `id` stays `0mpc`. The role sets `id: 0mcp`, which will not match the |
| 50 | +renamed realm's id `0mpc`. **Decision point — pick ONE in step 5.** |
| 51 | + |
| 52 | +## 3. Pre-flight |
| 53 | + |
| 54 | +```bash |
| 55 | +# Confirm live state |
| 56 | +curl -s -o /dev/null -w 'realm 0mpc: %{http_code}\n' https://sso.0mcp.com/realms/0mpc/.well-known/openid-configuration |
| 57 | +curl -s -o /dev/null -w 'realm 0mcp: %{http_code}\n' https://sso.0mcp.com/realms/0mcp/.well-known/openid-configuration |
| 58 | +# Expect: 0mpc 200, 0mcp 404 |
| 59 | + |
| 60 | +# Admin creds live in .local (do NOT print them into shared logs) |
| 61 | +# keycloak_admin_username / keycloak_admin_password — see .local/keycloak/ or identity overlay |
| 62 | +KC=https://sso.0mcp.com |
| 63 | +``` |
| 64 | + |
| 65 | +Get an admin token (master realm): |
| 66 | + |
| 67 | +```bash |
| 68 | +TOKEN=$(curl -s "$KC/realms/master/protocol/openid-connect/token" \ |
| 69 | + -d grant_type=password -d client_id=admin-cli \ |
| 70 | + -d username="$KC_ADMIN_USER" -d password="$KC_ADMIN_PASS" | python3 -c 'import sys,json;print(json.load(sys.stdin)["access_token"])') |
| 71 | +``` |
| 72 | + |
| 73 | +## 4. Back up the realm (mandatory) |
| 74 | + |
| 75 | +```bash |
| 76 | +# Full partial-export of the realm incl. clients, roles, groups, and users. |
| 77 | +curl -s -X POST "$KC/admin/realms/0mpc/partial-export?exportClients=true&exportGroupsAndRoles=true" \ |
| 78 | + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ |
| 79 | + > ~/keycloak-0mpc-backup-$(date +%Y%m%d).json |
| 80 | +# Users are NOT included in partial-export; if you need a users backup, use |
| 81 | +# kc.sh export on the Keycloak host, or accept that users persist through the |
| 82 | +# in-place rename (they do — rename does not touch user records). |
| 83 | +wc -c ~/keycloak-0mpc-backup-*.json # sanity: non-trivial size |
| 84 | +``` |
| 85 | + |
| 86 | +## 5. Open a maintenance window, then rename |
| 87 | + |
| 88 | +Announce SSO downtime. All oauth2-proxy-fronted subdomains (grafana, ops-portal, |
| 89 | +adminer, etc.) will reject logins until step 6 completes. |
| 90 | + |
| 91 | +**Option A — in-place rename (preserves everything; realm id stays `0mpc`):** |
| 92 | + |
| 93 | +```bash |
| 94 | +curl -s -X PUT "$KC/admin/realms/0mpc" \ |
| 95 | + -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ |
| 96 | + -d '{"realm":"0mcp"}' |
| 97 | +# Verify |
| 98 | +curl -s -o /dev/null -w 'realm 0mcp: %{http_code}\n' "$KC/realms/0mcp/.well-known/openid-configuration" # expect 200 |
| 99 | +``` |
| 100 | + |
| 101 | +Then make the role idempotent against the renamed realm whose id is still `0mpc`. |
| 102 | +**Pin the id** so the `keycloak_realm` task updates (not recreates) it: |
| 103 | + |
| 104 | +```yaml |
| 105 | +# inventory/group_vars/all/identity.yml (add, alongside keycloak_realm_name) |
| 106 | +keycloak_realm_id: "0mpc" # immutable internal id; realm NAME is 0mcp |
| 107 | +``` |
| 108 | +…and change `roles/keycloak_runtime/tasks/main.yml` `id:` to |
| 109 | +`"{{ keycloak_realm_id | default(keycloak_realm_name) }}"`. (Small, separate PR.) |
| 110 | + |
| 111 | +**Option B — fresh realm via import (clean id `0mcp`, more steps):** |
| 112 | + |
| 113 | +```bash |
| 114 | +# Edit the backup: set "realm":"0mcp" and "id":"0mcp", then import. |
| 115 | +sed -i '' 's/"realm" *: *"0mpc"/"realm":"0mcp"/; s/"id" *: *"0mpc"/"id":"0mcp"/' ~/keycloak-0mpc-backup-*.json |
| 116 | +curl -s -X POST "$KC/admin/realms" -H "Authorization: Bearer $TOKEN" \ |
| 117 | + -H 'Content-Type: application/json' -d @~/keycloak-0mpc-backup-*.json |
| 118 | +# Users are not in the partial export — re-create or migrate separately. |
| 119 | +# Once verified, delete the old realm: DELETE $KC/admin/realms/0mpc |
| 120 | +``` |
| 121 | + |
| 122 | +> Recommendation: **Option A** unless you specifically need the internal id to be |
| 123 | +> `0mcp`. It is the true "rename", preserves users without a separate migration, |
| 124 | +> and is reversible (PUT `{"realm":"0mpc"}` to roll back). |
| 125 | + |
| 126 | +## 6. Reconcile config → live (re-converge) |
| 127 | + |
| 128 | +```bash |
| 129 | +# Pre-converge .env workaround (per CLAUDE.md) |
| 130 | +mv .local/open-webui/provider.env{,.bak} 2>/dev/null || true |
| 131 | +mv .local/serverclaw/provider.env{,.bak} 2>/dev/null || true |
| 132 | +
|
| 133 | +make converge-keycloak env=production |
| 134 | +make configure-edge-publication env=production # re-render oauth2-proxy issuer/login/token/jwks URLs |
| 135 | +
|
| 136 | +# Restore |
| 137 | +mv .local/open-webui/provider.env{.bak,} 2>/dev/null || true |
| 138 | +mv .local/serverclaw/provider.env{.bak,} 2>/dev/null || true |
| 139 | +``` |
| 140 | + |
| 141 | +## 7. Verify |
| 142 | + |
| 143 | +```bash |
| 144 | +curl -s -o /dev/null -w 'realm 0mcp: %{http_code}\n' https://sso.0mcp.com/realms/0mcp/.well-known/openid-configuration # 200 |
| 145 | +curl -s -o /dev/null -w 'realm 0mpc: %{http_code}\n' https://sso.0mcp.com/realms/0mpc/.well-known/openid-configuration # 404 (A) / 200-until-deleted (B) |
| 146 | +# Log into one SSO-fronted app end-to-end (e.g. grafana.0mcp.com) and confirm token issuance. |
| 147 | +``` |
| 148 | + |
| 149 | +## 8. Rollback |
| 150 | + |
| 151 | +- **Option A:** `PUT $KC/admin/realms/0mcp -d '{"realm":"0mpc"}'`, revert the |
| 152 | + `keycloak_realm_id` config change, re-converge. |
| 153 | +- **Option B:** re-point config to `0mpc`, delete the half-built `0mcp` realm. |
| 154 | + |
| 155 | +## 9. Close-out |
| 156 | + |
| 157 | +- Live-apply receipt + `platform_version` bump per CLAUDE.md §5. |
| 158 | +- Note the rename in the changelog under the next release. |
0 commit comments