Auth — Keycloak migration plan
Why TrailBase today, why Keycloak eventually, the migration sequence, and what's already in-tree under nodered/keycloak/.
Where we are today
R2-D2 uses TrailBase for identity. The login form POSTs to /api/auth/login,
which forwards { email, password } to TrailBase. On success, the dashboard sets an
httpOnly session cookie that every authenticated route checks via headers populated
by the edge middleware (x-user-id, x-user-email, x-user-role).
This is a good shape for a single private tenant:
- Zero config friction. TrailBase is already in the stack for the Republic and Holoprojector data — adding identity to the same service is free.
- Server-side state. No bearer tokens floating in localStorage, no token-refresh endpoint, no rotation logic.
- Inspectable. TrailBase admin lets you see who's registered and revoke them in seconds.
But it's also a shape with a ceiling.
Where it stops working
There are three concrete reasons the current model wants to be replaced:
- The Republic has member nations. Each nation will eventually need its own namespace of operators with its own admin scope. TrailBase models users but doesn't have a first-class realm/tenant abstraction; we'd have to bolt one on.
- External SSO. Some nations have existing identity providers (GitLab/Keycloak/ AzureAD). Asking them to maintain a second password in TrailBase is a non-starter.
- Audit + group→role mapping. Today every operator is effectively an admin.
We need at least three roles (
operator,editor,admin) and an audit trail per mutation. The audit trail exists for Restreamer mutations; making it consistent across planes is a refactor that's easier on top of a real RBAC model.
Keycloak handles all three out of the box, and gives us OIDC for free.
What's already in the repo
The scaffolding for Phase A is committed under nodered/keycloak/. Nothing here is
wired into the running cluster yet — the realm JSON is a template the deploy step
will hand to kcadm, and the theme is a directory the container will mount.
nodered/keycloak/
├── realms/
│ └── r2d2-realm.json # realm=r2d2, client=r2d2-fleet, hosts r2d2.office.ilab.zone
├── themes/
│ └── r2d2/ # dark-space + teal Keycloak theme, aligned with the dashboard
│ ├── login/ # PF v5 cosmetic overlay (r2d2.css), brand macro, FTL templates
│ ├── account/ # Keycloak v3 React Account UI (logo swap only)
│ ├── email/ # dark email card (teal #2dd4bf brand bar + buttons)
│ └── STYLEGUIDE.md # token map vs r2d2-fleet/src/styles/globals.css
└── providers/
└── keycloak-altcha-jar-with-dependencies.jar # CAPTCHA on the registration flowBrand voice: wordmark "R2-D2 Fleet", tagline "Astromech Fleet Console", redirect copy reads "Boarding the fleet in N seconds…". The theme forces dark mode regardless of the operator's system preference so the SSO chrome doesn't look like a separate product from the dashboard.
Realm identifiers (everything downstream must agree on these):
| Field | Value |
|---|---|
| Realm | r2d2 |
| Client (dashboard) | r2d2-fleet |
| Login / account / email theme | r2d2 |
| Public host | r2d2.office.ilab.zone |
| Dev redirect | http://localhost:3210/api/auth/callback/keycloak |
| Logo URL (for emails) | https://r2d2.office.ilab.zone/image.png |
Target end-state
┌───────────────────────┐
│ Member nation IdP │ (Azure / GitLab / etc — optional federation)
└──────────┬────────────┘
│ OIDC
▼
┌───────────────────────┐
│ Keycloak realm │ realm: r2d2
│ ┌─────────────────┐ │ - clients: r2d2-fleet, restreamer, trailbase
│ │ groups │ │ - groups → roles (operator/editor/admin)
│ │ - admin │ │ - identity providers (per-nation OIDC)
│ │ - editors │ │
│ │ - operators │ │
│ └─────────────────┘ │
└──────────┬────────────┘
│ OIDC authorization code flow
▼
┌───────────────────────┐ ┌────────────────────────────────┐
│ r2d2-fleet │ ─────►│ TrailBase (records only) │
│ - NextAuth/Auth.js │ │ -> identity comes from │
│ client │ │ Keycloak JWT, not from │
└──────────┬────────────┘ │ TrailBase auth tables │
│ JWT in cookie └────────────────────────────────┘
▼
per-route role check → audit log row in plane-specific audit tableMigration sequence (planned, not committed)
Phase A — coexistence (no operator-visible change)
- Stand up a Keycloak realm in-cluster. Scaffolding committed —
realms/r2d2-realm.jsonhas the realm, ther2d2-fleetclient (auth code + PKCE, public + private redirect URIs for local dev on port 3210), and the role catalog. The container needs to import this JSON on first boot; that deploy step is the next concrete TODO. - Add an
AUTH_BACKEND=trailbase|keycloakenv var to r2d2-fleet. Defaulttrailbasefor compat. Implement the Keycloak path behind it but don't switch. - Mirror users: every TrailBase user becomes a Keycloak user with the same email and a forced password reset on first login.
- Run both side-by-side in a non-production deploy. Smoke-test every authenticated route.
Phase B — switch
- Flip
AUTH_BACKEND=keycloakon the prod deploy. TrailBase sessions still work for anyone who logged in before the switch (until cookie expiry), but new logins go through Keycloak. - Watch the audit log for failures. Roll back by flipping the env back — no schema changes have happened.
Phase C — RBAC and federation
- Define Keycloak groups (
admin,editor,operator) and roles. Map JWTrealm_access.rolesto a per-requestrolein the dashboard. The realm JSON already declares the role catalog (pending,read-only,editor,admin,super-admin) so this is wiring, not schema design. - Every mutating route gains a role check. The minimum role is documented in the OpenAPI spec.
- For each member nation that wants SSO, federate their existing IdP into the
r2d2Keycloak realm. The dashboard doesn't change — Keycloak handles the federation.
Phase D — TrailBase cleanup
- Drop TrailBase's auth tables. TrailBase keeps serving the Republic/Holoprojector record APIs; it stops being an identity provider.
- Delete the
AUTH_BACKEND=trailbasecode path.
Theming notes
The themes/r2d2/ theme is a cosmetic overlay on Keycloak's keycloak.v2 parent
(login) and keycloak.v3 parent (account UI). It doesn't fork any FreeMarker
templates that the parent could update — the rebrand is concentrated in:
login/resources/css/r2d2.css— dark background#0a0e1a, glass cardrgba(17,24,39,0.78)withbackdrop-filter: blur(20px), teal#2dd4bfaccent + focus glow, tri-tone (teal → blue → violet) 3px brand bar pinned to the viewport top, ambient radial-gradient galaxy overlay. All tokens are documented inthemes/r2d2/STYLEGUIDE.mdand 1:1 mapped tor2d2-fleet/src/styles/globals.css.login/r2d2-brand.ftl— the logo + wordmark + tagline macro the rest of the login templates import.email/html/template.ftl— dark email card (#111827surface on#0a0e1abody, teal brand bar, JetBrains Mono for OTP codes). All inline CSS, noprefers-color-scheme, because Outlook strips it.
When Keycloak ships a new minor version, only r2d2.css and the four R2-D2-specific FTLs
(r2d2-brand.ftl, login.ftl, info.ftl, error.ftl, login-verify-email.ftl,
register.ftl, delete-account-confirm.ftl, buttons.ftl) need a re-review against the
parent's diff. The rest is inherited.
Open questions
These are the things this plan does not yet answer. They're the next conversation to have before scheduling implementation:
- Where does Keycloak run? Same cluster as r2d2-fleet, or a separate "platform" cluster? The argument for separate is the usual one — identity outages shouldn't be caused by the system that depends on identity.
- Database backend for Keycloak. Yugabyte (consistent with the rest of the stack) or a dedicated postgres? Keycloak's Yugabyte story isn't perfectly smooth; a small postgres might be the pragmatic answer.
- Service-to-service auth. Right now the dashboard talks to Restreamer with no auth (same-cluster network policy). With Keycloak we have the option of issuing service accounts and JWTs — worth doing for Restreamer specifically, since it has state.
- TrailBase record API authentication. When TrailBase stops being the IdP, the dashboard needs to authenticate to TrailBase's record API as itself, not as the operator. That's either a TrailBase service token or a Keycloak-issued client cred.
Rollback plan
The migration is two-stepped on purpose: the AUTH_BACKEND env flag lets us flip back
to TrailBase identity at any point during Phase A or Phase B without touching schema or
code. Phase C/D introduce role checks and remove the TrailBase auth code; rolling back
from there means redeploying the previous tag.
What this plan does not propose
- It does not propose mTLS between services. Network policy + TrailBase service tokens cover the threat model in a small private cluster.
- It does not propose a separate user-management UI. Keycloak's admin console is good enough for the populations we have.
- It does not propose passkeys/WebAuthn. Could be added later inside Keycloak; not needed for the initial cut.