Skip to content

ADR 0008 — Authorization with OPA, and an internal user token#

Status Accepted
Date 2026-09-28
Authors Ruben Knuijver
Supersedes —
Superseded by —
Related #322 (configuration platform), #325 (internal user token), #326 (OPA foundation)

Context#

The /config pages are to manage CommandCenter's external services, secrets and sign-in from the UI. Until now nothing behind the BFF was protected: no route had an authorization policy, WebApi received no user at all, and every container on the shared proxy network could call WebApi directly. The identity provider is still open — Keycloak is more likely than Entra ID — and authorization was chosen to be Open Policy Agent, on the backend and in the SPA.

Decision#

  1. The BFF passes the user to the backends in a token it signs itself (ES256, 5 minutes, iss=commandcenter-bff, aud=commandcenter-internal), not the identity provider's token. It normalises the user — id, name, and roles mapped onto internal cc.* roles — from Entra or Keycloak claims alike. On a cluster marked UserAssertion it always removes the browser's Authorization header. The backends validate against the BFF's published keys.
  2. OPA decides. The policies (policies/, Rego v1) are code: reviewed in merge requests, tested in CI with a coverage gate, and baked into the commandcenter-opa image. OPA runs next to the BFF and WebApi on an internal network and its own API is locked.
  3. One batch query, data.commandcenter.authz.decisions, answers {id, action, resource} checks for one subject. The input never carries a setting's or a secret's value.
  4. Fail closed: timeout (750 ms), error, empty result or malformed decision is a denial; "could not ask" (503) is kept apart from "not allowed" (403).
  5. The backend is the boundary. The SPA asks POST /bff/authz/decisions only to hide or disable controls.

Consequences#

  • Switching between Entra ID and Keycloak only changes the BFF (claim paths and the role map).
  • Who may do what is data in the policy bundle (access/data.json); a later step serves that data as its own bundle so the Access page can change it without a release.
  • OPA is a dependency of the protected actions only; an outage answers 503 there and nothing else.
  • Every BFF instance needs the same signing key; without one outside Development the backends see no user, as before.
  • There is no in-app break-glass. OPA restarts itself; a bad policy is rolled back by deploying the previous image.

Alternatives considered#

  • Forward the identity provider's access token (on-behalf-of). Entra-specific (Keycloak's token exchange differs), puts role normalisation in every backend, and needs a token per downstream API.
  • A plain user header from the BFF. Any container on the shared network could forge it.
  • OPA only at the BFF (route level). The BFF cannot see which setting a request changes or whether it is a secret; the decision needs the backend's knowledge.
  • A bundle server for the policy code. Needs storage and credentials for OPA to start, and the policy a release runs would no longer be the one it was tested with.