Skip to content

ADR-053: Identity & Cross-System Authorization Strategy

Date: 2026-06-04

Status: Accepted — largely delivered (updated 2026-06-09). Keycloak is the live app-facing IdP; ArgoCD + Backstage sign in via Keycloak OIDC; Dex + oauth2-proxy retired; Backstage group-based RBAC shipped (#197). What remains is the access-model-as-code generators reading the git-native Team (ADR-063), landing with the rebuild cutover (ADR-049). Complements the AWS+Kubernetes RBAC of ADR-039/ADR-040/ADR-041/ADR-047; charts the eventual replacement of the Dex broker (ADR-052). Unblocks the deferred Backstage RBAC (#197). Refined by ADR-059 — the “Identity Center is the user store, Keycloak brokers up” framing is one instance of a topology where the upstream IdP (Okta / Entra / Google / IdC / standalone) is pluggable behind the Keycloak seam. Extended by ADR-068 — once ADR-067 made Product first-class, this strategy’s team-scoped taxonomy could no longer express product-scoped or cross-team access; ADR-068 specifies the concrete model (the AccessGrant object, access:<team>/<product> roles, the release-approver/team-admin roles, and OIDC-native cluster auth) that the generators realize.

Amendment — 2026-06-08: Keycloak is the IdP of record by default

Section titled “Amendment — 2026-06-08: Keycloak is the IdP of record by default”

The original framing made AWS Identity Center the user store and Keycloak a broker up to it (ADR-059 scenario C). Building the Team→group/role taxonomy surfaced that this is the wrong default: IdC can’t emit group claims, so the access model sat inert behind a broker that couldn’t feed it — and hard-requiring an AWS Organization with Identity Center makes a reference platform un-forkable. We are therefore flipping the default:

  • Default = Keycloak is the IdP of record. Identity, membership, and the group→role taxonomy live in the platform realm (users seeded as code; see the keycloak-config module’s users input). The access model is live with no upstream — this is ADR-059 scenario B (“Keycloak-as-hub”), promoted from “a bootstrap” to the realized default. upstream = null in keycloak-config.
  • Federation is an opt-in layer, not the foundation. Set keycloak-config’s upstream to broker to a corporate IdP (Okta / Entra / Google over OIDC, or AWS IdC over SAML) when an environment has one. Nothing downstream changes — apps, OIDC claims, the access-as-code generators, and AWS access are all invariant (the whole point of the ADR-052/059 vendor-neutral seam). With a group-emitting upstream the membership gap closes automatically (the IdP-group→Keycloak-group mapper); with IdC it stays manual.
  • Unchanged: everything else in this ADR. Keycloak as the app-facing OIDC IdP, access-as-code from the git-native Team registry (gitops/teams/, ADR-063 — formerly _teams.hcl), native per-system enforcement, and AWS account access via Identity Center permission sets (orthogonal to the app IdP — IdC stays the AWS-access SoR regardless of the upstream choice).

Net: this is a default flip within the ADR-059 pluggable seam, not a new direction — it makes the realized build match scenario B and treats IdC-as-app-IdP as one optional federation mode among several.

Many systems on the platform need authorization, and the list only grows: AWS (accounts/IAM), ArgoCD, Kubernetes, Backstage, and future tools (Grafana, Crossplane/Kyverno views, etc.). Today there is one identity backbone — AWS Identity Center — but N fragmented enforcement points, each speaking its own authz dialect, and the mapping between them is hand-maintained per system.

What already works well (and stays): the AWS + Kubernetes access chain is coherent, role-based, group-mapped, team-scoped, and default-deny — SSO group → Dev-<team> permission set → DeveloperAccess-<team> IAM role → EKS access entry → team-<team>:developers → namespace RoleBinding (ADR-039/040/041/047), generated by the Crossplane Tenant Composition. This ADR does not disturb it.

What is limiting — and it traces to Identity Center as the app IdP:

  • It can’t cleanly emit group claims to OIDC apps. Backstage/Dex receives no groups over SAML (ADR-052: “it does not emit group memberships as a SAML attribute … it fails the assertion”). ArgoCD only gets them via a manual console attribute mapping, and then as opaque UUIDs (g, a4b884e8-…, role:admin) — illegible and not IaC-managed.
  • Per-app SAML signing certs + console-only attribute mappings are the one slice of this area that can’t be managed as code.
  • It is AWS-only. The platform’s north star is multi-cloud, compliance-aware, with a coherent cross-system RBAC story; an AWS-proprietary identity provider is structurally misaligned with that. ADR-051/052 already saw this and made the Dex issuer (https://sso.aws.refplat.org) deliberately vendor-neutral as the swap seam.
  • The group→role mapping is duplicated and hardcoded per system (ArgoCD UUIDs, k8s group strings, AWS permission sets, soon Backstage), and the scoping is inconsistent — k8s and AWS are team-scoped, but ArgoCD’s developer role is global (*/*).

The goal (stated): a declarative, uniform way to define authorization and propagate it across the platform, optimizing for compatibility with external / off-the-shelf systems.

Three linked decisions.

1. Enforcement model — declarative source → standard claims + generated native config (NOT a central runtime PDP)

Section titled “1. Enforcement model — declarative source → standard claims + generated native config (NOT a central runtime PDP)”

There are two opposite ways to do “uniform authz,” and only one is compatible with a heterogeneous fleet:

  • A central runtime authorization engine that every system queries at decision time (OpenFGA/Zanzibar, OPA, Cedar, the emerging AuthZEN API) is maximally uniform but minimally compatible — off-the-shelf tools (AWS, ArgoCD, Grafana, almost every future tool) will not outsource their authorization decisions; they decide internally from token claims + native config. Rejected as the foundation. It is reserved only for apps we build and control (a Backstage custom policy, future bespoke services), where we own the enforcement point.

  • A single declarative source-of-truth, compiled outward is the externally-compatible way to “define once, propagate everywhere.” Enforcement stays native (each tool’s own engine), fed two things from the one source:

    1. Standard OIDC group/role claims — the one authorization-relevant wire nearly every external tool speaks natively (the compatibility win).
    2. Generated native RBAC per system (Identity Center assignments, the IdP’s groups/roles, ArgoCD policy, k8s RoleBindings, Backstage policy).

    This is also more robust than a central PDP — no runtime authz service on the hot path of every check.

2. App-facing IdP — Keycloak, brokering to Identity Center

Section titled “2. App-facing IdP — Keycloak, brokering to Identity Center”

Adopt Keycloak as the app-facing OSS OIDC identity provider. It federates authentication up to AWS Identity Center (SAML) — AWS SSO stays the user store and the AWS console/CLI path — while Keycloak owns the group/role taxonomy locally and emits clean OIDC group/role claims (by name), per client, to every app (Backstage, ArgoCD via OIDC instead of the UUID hack, and future tools). It replaces Dex (ADR-052), reusing the same vendor-neutral issuer seam.

Keycloak was chosen over the alternatives (evaluation 2026-06-04) for the criteria that matter most here: the most mature SAML brokering (richest upstream-attribute → local-group/role mapping), the cleanest per-client OIDC claim mappers, the most mature, now-official Terraform provider (code-managed realms/groups/roles/ clients/mappers), unencumbered Apache-2.0 + CNCF governance, the strongest native fine-grained authz (Authorization Services / UMA — for our own apps later, decision 1), and first-class ArgoCD + Backstage OIDC track records. Identity Center stays the AWS IdP — the split (AWS SSO for AWS, Keycloak for apps) is intentional.

Define the team → group → role → permission taxonomy once, declaratively, extending the ADR-049 “Team envelope” (an SSO group + allowed tiers/quota/envs), and generate each system’s native config from it: Identity Center account assignments, Keycloak groups/roles/clients/mappers (Terraform provider), ArgoCD policy, Kubernetes RoleBindings, and the Backstage permission policy. This is the platform’s existing ethos (generate native config from a shared model) applied to authz, and it is the only approach that scales to “future tools” and kills the per-system duplication. It is adopted regardless of the IdP and is the higher-leverage half of this ADR.

Relationship to ADR-049 (the model this consumes)

Section titled “Relationship to ADR-049 (the model this consumes)”

This ADR is the identity + enforcement half; ADR-049 (Team / Tenant / Zone) is the model half — and the Team envelope it defines is the single declarative source decision 3 generates from. Keycloak’s group/role taxonomy mirrors the ADR-049 Team model; a Team is an SSO/Keycloak group plus its envelope. The two ADRs are co-dependent and both land with the rebuild, so finalizing the ADR-049 schema is the first implementation step — the Keycloak group taxonomy, the per-system RBAC generators, and the Backstage policy (#197) all read from it. (Backstage already forward-compat’d: the 2.3a catalog projection emits Systems carrying zone/tier so ADR-049 lands with no entity-model change.)

They interlock across two authorization planes, both derived from the one Team model: (a) the envelope — what a Team’s claims/workloads may do (tier ∈ allowedTiers, quota, envs/locations) — enforced at admission by Kyverno on the Tenant CR; and (b) user RBAC — what a human may do per system — which is this ADR’s job, where ADR-049’s environment × tier developer-access posture becomes the roles/claims generated outward. Same source, two enforcement planes.

  • Unblocks #197 (Backstage group-based RBAC): designed to consume OIDC group claims Keycloak provides cleanly. ArgoCD likewise moves from opaque group UUIDs to named claims, and its developer role can finally be team-scoped (ArgoCD Projects) consistent with k8s/AWS.
  • Keycloak is a Tier-0 component — HA, a CloudNativePG-backed DB, backups, and upgrade discipline (it is the new app front door, like the oauth2-proxy of #202). This is the honest operational cost; the lighter-ops runner-up was Zitadel (Go + Postgres) — revisit only if ops-simplicity ever outranks brokering richness.
  • Two group sources (Identity Center for AWS, Keycloak for apps) — reconciled by decision 3 (the model-as-code generates both), so they can’t drift.
  • Cloud-neutral app identity — aligns with the multi-cloud vision; the AWS-only constraint is removed from the app layer.
  • Standards posture: OIDC claims = the compatibility wire; SCIM for provisioning into downstream apps; AuthZEN (OpenID Foundation, emerging) is the standard to track for a future uniform authz-decision API but is not yet bettable; Cedar / OpenFGA / Keycloak UMA are for our own apps’ fine-grained authz only.
  • Timing: the IdP swap lands with the planned rebuild + ADR-049 (good window — that’s when the target tenant model arrives). The Dex broker (ADR-052) is the current implementation and is superseded over time. Interim Backstage RBAC (#197) either takes a thin tactical step or waits for Keycloak.
  • Stay on Identity Center + per-app workarounds (Identity Store API for Backstage groups; keep ArgoCD’s UUID mapping). Rejected: the group-claim gap and AWS-only constraint persist, the per-app SAML toil remains, and it doesn’t scale or stay DRY.
  • A central runtime authz engine for all systems (OpenFGA/OPA/Cedar/AuthZEN). Rejected as the foundation: off-the-shelf tools won’t call it; viable only for our own apps (decision 1).
  • Other OSS IdPs. Zitadel — lightest ops (Go + Postgres, fits CloudNativePG) but AGPL-3.0, a more opinionated role-claim model, thinner brokering + Terraform coverage, no native ReBAC. Authentik — open-core with per-user pricing creep and the most moving parts (Redis + worker + proxy). Ory (Hydra/Kratos) — no built-in SAML broker or RBAC taxonomy (you assemble the IdP yourself). Keycloak wins on this use case.

This ADR sets direction; implementation is separate planning:

  • The Keycloak module + unit (brokering to Identity Center, CNPG-backed, Tailscale-internal, HA), replacing the Dex module/unit (ADR-052).
  • The access-model-as-code schema (extending the ADR-049 Team envelope) + the per-system generators (Keycloak, Identity Center, ArgoCD, k8s, Backstage).
  • The ArgoCD OIDC cutover (named group claims; team-scoped Projects).
  • Backstage RBAC (#197) on OIDC group claims.
  • The Dex → Keycloak migration + retiring the per-app SAML toil.
  • Auth loose ends from #213 (logout, token rotation) revisited under the new IdP.