--- id: NK-ADR-0009 type: architecture-decision-record title: "Expanded-Mode Keycloak: Adoption Trigger and Federation Topology" status: proposed owner: net-kingdom revision: "1" proposed: "2026-09-28" last_reviewed: "2026-09-28" review_interval: 12m --- # ADR-0009 - Expanded-Mode Keycloak: Adoption Trigger and Federation Topology **Status:** Proposed (awaiting Bernd's decision; NK-WP-0011-T01) **Date:** 2026-09-28 **Deciders:** Bernd Worsch, Claude ## Context The live IAM Profile issuer is KeyCape (Authelia + LLDAP + privacyIDEA) at `https://kc.coulomb.social`. No Keycloak Deployment exists. The architecture doc says expanded mode is capability-driven, chiefly inbound enterprise federation (Entra ID, AD, SAML), and leaves the per-tenant trigger and dual-issuer rule open. ADR-0014 defines the `IAM` capability role as a tenant operating its own dedicated IAM instance. ADR-0006 keeps flex-auth as the PDP. ADR-0016 makes MFA a user preference with workload opt-in step-up. The user-engine extension-point doc requires immutable provider subjects, tenant-scoped group mappings, and no privilege from unmapped upstream claims. The estate is one node with single-instance databases, so no option here buys HA. NK-WP-0001 decision D2 (Keycloak as primary internal user store) predates all of this. ## Decision (proposed) 1. **Trigger.** A tenant moves to expanded mode only when all three hold: a named tenant, a named owner of the upstream IdP, and a concrete federation requirement the lightweight stack cannot meet. User count, or the wish to have Keycloak, is not a trigger. Until then NK-WP-0011 implementation tasks stay unstarted. 2. **Role: OIDC identity broker only.** Keycloak brokers upstream Entra ID (OIDC), AD (LDAP federation) and generic SAML 2.0 IdPs (SAML as an upstream, via Keycloak's identity-provider brokering). Downstream it issues only IAM Profile OIDC/PKCE tokens. Emitting SAML assertions to downstream applications is out of scope. Keycloak is not the primary user store (**supersedes D2**), not the PDP, and not a secret store. 3. **Isolation.** Realms are the tenant boundary. The `tenant:platform` realm is separate and reserved. A tenant without the `IAM` role gets a realm in the shared broker; a tenant with the `IAM` role gets a dedicated broker instance (ADR-0014). A shared broker gives realm-level, not process-level, isolation, so it is acceptable only for tenants whose federation trust and data classification allow that. Realm admins may not alter IAM Profile semantics, the platform realm, federation trust settings, or audit retention. tenant-engine, not Keycloak, is the source of truth for tenant existence and capability roles. 4. **Coexistence.** Each tenant has exactly one active issuer at a time, recorded by tenant-engine and published as issuer configuration. KeyCape stays unchanged and keeps serving lightweight tenants. Applications target the IAM Profile and discover the issuer; they do not embed provider choice. Account bindings remain keyed on issuer plus immutable subject, so moving a tenant between issuers is a planned migration with an explicit subject-mapping step, never a silent dual login. 5. **Assurance.** Federated login is AAL1 by default. Upstream MFA (for example Entra Conditional Access) counts as higher assurance only through a reviewed, per-IdP trust mapping written down before use. Privileged step-up continues to use privacyIDEA under ADR-0016 and the proposed IAM v0.4 step-up. The privacyIDEA provider JAR is added only if a tenant needs Keycloak-side MFA. 6. **Provisioning and mapping.** Follow the user-engine extension-point rules: JIT creates a pending projection; group mappings are tenant-scoped, versioned and deny ambiguous envelopes; no platform role from raw upstream group names. 7. **Hostname and issuer.** Not chosen here. It is assigned by the package owner under ADR-0015 with the first tenant, as a distinct host from `kc.coulomb.social`; the issuer URL is then treated as immutable. 8. **Ownership.** net-kingdom owns this contract and conformance checks. Packaging and runtime belong to the ADR-0015 owner; the database consumer to railiance-platform; OpenBao roles to railiance-platform. ## Alternatives considered - **Keycloak replaces KeyCape.** Rejected: unnecessary migration risk for working tenants; contradicts capability-driven adoption. - **Dedicated instance for every federated tenant.** Strongest isolation, but on one node the cost is multiplied database, memory and patching for tenants that do not need it. Kept for `IAM`-role tenants. - **Realm-per-tenant only, including `IAM` tenants.** Contradicts ADR-0014's meaning of `IAM`. - **Keycloak as SAML IdP downstream too.** No consumer requires it; widens the contract surface. Revisit on a named need. - **Trust upstream MFA claims by default.** Rejected: assurance would depend on each customer's tenant configuration. - **Build ahead of demand.** Rejected: adds an issuer, a database and a break-glass path with no owner or user. ## Consequences - NK-WP-0011 T02 onward stay gated on the trigger in decision 1. - T06 conformance uses `tools/iam-profile-conformance/` against IAM v0.3. - tenant-engine needs an issuer-per-tenant field; this is a dependency to raise with its owner, not something built here. - Shared-broker realms cannot be described as strongly isolated tenants. - No HA is implied; T02 and T08 must name backup ownership, off-host custody and an isolated restore proof. ## Open questions - Which named tenant and IdP owner will trigger the first deployment? - Does key-cape require changes for issuer-per-tenant discovery (EP-NK-001)? - Which claim carries the federated-assurance mapping so flex-auth can consume it?