Skip to content

Commit 0824bd4

Browse files
baditaflorinclaude
andauthored
docs(runbook): add Keycloak realm rename 0mpc -> 0mcp procedure (#21)
After the 0mpc.com -> 0mcp.com domain migration, keycloak_realm_name now derives '0mcp' but the live realm is still '0mpc'. A naive converge-keycloak would create an empty '0mcp' realm and orphan the populated '0mpc' one (community.general.keycloak_realm creates by id; realm id is immutable). Document the safe in-place PUT rename (Option A, preserves users/clients) vs fresh-import (Option B), with backup, maintenance-window, reconcile-converge, verify, and rollback steps. Execution is deferred to a maintenance window. Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 1d79f6c commit 0824bd4

1 file changed

Lines changed: 158 additions & 0 deletions

File tree

Lines changed: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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

Comments
 (0)