GLOSSARY's banned term "tenant" survived in Configuration tables and Flux
postBuild substitutions across product READMEs as ${TENANT} (uppercase
ENV var). Prior banned-term greps searched lowercase `tenant` so the
ALL-CAPS form slipped through.
Product README fixes:
- products/cortex: TENANT/DOMAIN → ORGANIZATION/SOVEREIGN_DOMAIN, plus
two DNS placeholder fixes for llm-gateway and chat URLs (same shape
Pass 25/31 fixed elsewhere).
- products/fingate: 6 instances (Flux substitution, Configuration table,
4 URL templates) renamed. URL shape api.openbanking.<org>.<sov-dom>
flagged as 4-segment FQDN that doesn't match NAMING §5.1 or §5.2 —
deferred to a deeper architectural pass.
- products/fabric: Configuration table row renamed.
Component README:
- platform/keycloak: shared-sovereign hostname auth.<sovereign-domain>
and per-organization auth.<org>.<sovereign-domain> both missing
<location-code> per NAMING §5.1. Fixed.
platform/librechat ${TENANT_ID} preserved — that's Microsoft Azure AD
tenant-ID (external technology, exempted by GLOSSARY).
Validation log Pass 34 entry includes meta-note: always run a global
grep for the surfaced drift category before closing a pass, to avoid
the asymmetric-drift problem Pass 25 warned against.
|
||
|---|---|---|
| .. | ||
| README.md | ||
Keycloak
User identity for Catalyst Sovereigns. Per-Sovereign supporting service in the Catalyst control plane (see docs/PLATFORM-TECH-STACK.md §2.3). Also serves as the FAPI Authorization Server for the Fingate (Open Banking) Blueprint.
Status: Accepted | Updated: 2026-04-27
Catalyst topology (set at Sovereign provisioning time, see
docs/SECURITY.md§6):
per-organization(SME-style Sovereigns, e.g. omantel): one minimal Keycloak per Organization (single replica, embedded H2/sqlite, ~150 MB RAM, no HA). Blast radius limited to one Org.shared-sovereign(corporate self-host, e.g. bankdhofar): one HA Keycloak for the entire Sovereign with multiple realms (one per Organization), federating to the corporation's identity provider (Azure AD, Okta).
Overview
Keycloak provides:
- User identity for the Catalyst console, marketplace, admin, REST/GraphQL API, and per-Application SSO.
- OIDC / OAuth 2.0 / SAML federation to corporate IdPs.
- FAPI 2.0 compliant authorization for the Fingate Open Banking Blueprint:
- PSD2/FAPI 2.0 certification path
- eIDAS certificate validation
- Consent management
- Multi-tenant TPP support (PSD2 sense — Third Party Providers, not platform tenants)
Architecture
flowchart TB
subgraph Keycloak["Keycloak"]
Core[Core IAM]
FAPI[FAPI Module]
Consent[Consent Service]
end
subgraph Backend["Backend"]
CNPG[CNPG Postgres]
end
subgraph Integration["Integration"]
Envoy[Envoy/Cilium]
TPP[TPP Registry]
end
Envoy -->|"ext_authz"| FAPI
FAPI --> Consent
Core --> CNPG
FAPI --> TPP
FAPI 2.0 Compliance
| Feature | Status |
|---|---|
| PKCE | Required |
| Signed JWT requests | Required |
| mTLS client auth | Required |
| PAR (Pushed Authorization) | Required |
| JARM responses | Required |
Configuration
Keycloak Deployment
The deployment shape depends on Catalyst's keycloakTopology choice (see banner above):
Corporate (shared-sovereign) — one HA Keycloak per Sovereign in catalyst-keycloak namespace on the management cluster:
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak
namespace: catalyst-keycloak
spec:
instances: 3 # HA, multiple replicas
db:
vendor: postgres
host: keycloak-postgres-rw.catalyst-keycloak.svc
port: 5432
database: keycloak
usernameSecret: # ESO-managed via OpenBao
name: keycloak-db-credentials
key: username
passwordSecret:
name: keycloak-db-credentials
key: password
http:
tlsSecret: keycloak-tls
hostname:
hostname: auth.<location-code>.<sovereign-domain>
SME (per-organization) — one minimal Keycloak per Organization in the Org's namespace on the management cluster:
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak
namespace: <org> # per-Org namespace
spec:
instances: 1 # no HA at SME tier
db:
vendor: postgres # or H2/sqlite for the smallest tier
host: keycloak-postgres-rw.<org>.svc
port: 5432
database: keycloak
# ... credentials
hostname:
hostname: auth.<org>.<location-code>.<sovereign-domain>
FAPI Realm Configuration
{
"realm": "open-banking",
"enabled": true,
"sslRequired": "all",
"attributes": {
"fapi.compliance.mode": "strict",
"pkce.required": "S256",
"require.pushed.authorization.requests": "true"
},
"clientPolicies": {
"policies": [
{
"name": "fapi-advanced",
"enabled": true,
"conditions": [
{
"condition": "client-roles",
"configuration": {
"roles": ["fapi-client"]
}
}
],
"profiles": ["fapi-2-security-profile"]
}
]
}
}
eIDAS Certificate Validation
TPP certificates are validated against qualified trust service providers:
apiVersion: v1
kind: ConfigMap
metadata:
name: eidas-config
namespace: open-banking
data:
trust-anchors: |
# QTSPs for eIDAS validation
- name: qualified-tsp-1
certificate: |
-----BEGIN CERTIFICATE-----
...
-----END CERTIFICATE-----
TPP Client Registration
{
"clientId": "tpp-12345",
"clientAuthenticatorType": "client-jwt",
"redirectUris": ["https://tpp.example.com/callback"],
"attributes": {
"tpp.authorization.number": "PSDGB-FCA-123456",
"tpp.eidas.certificate": "...",
"tpp.roles": ["AISP", "PISP"]
},
"defaultClientScopes": [
"openid",
"accounts",
"payments"
]
}
Consent Flow
sequenceDiagram
participant TPP
participant Keycloak
participant User
participant ConsentService
TPP->>Keycloak: PAR request
Keycloak->>TPP: request_uri
TPP->>User: Redirect to Keycloak
User->>Keycloak: Authenticate
Keycloak->>ConsentService: Get consent page
ConsentService->>User: Show accounts/permissions
User->>Keycloak: Grant consent
Keycloak->>ConsentService: Store consent
Keycloak->>TPP: Authorization code
High Availability
HA shape depends on Catalyst's keycloakTopology:
shared-sovereign(corporate): 3 replicas behind a Service, CNPG PostgreSQL with WAL streaming to async standby, session replication via Infinispan.per-organization(SME): single replica, no session replication, restart-on-deploy is acceptable for SME-tier SLAs. Larger SMEs can opt into HA via tier upgrade — same Catalyst CR shape, just bumpedinstances.
Part of OpenOva