PR #911 wired the SME tenant orchestrator to emit realmConfig.tenant.enabled=true on the per-tenant bp-keycloak HelmRelease — but the chart had no template that consumed those values, so the WordPress / OpenClaw / Stalwart OIDC integrations had no client registered in the tenant realm and SSO failed end-to-end. This change adds the chart-side template the orchestrator was already emitting for. When realmConfig.tenant.enabled=true: * configmap-sovereign-realm.yaml SKIPS (mutual-exclusion guard added on the existing template) so only one realm CM is rendered. * NEW templates/configmap-tenant-realm.yaml renders a realm import ConfigMap (same name `<release>-sovereign-realm-config` so the upstream keycloak-config-cli existingConfigmap reference still resolves) carrying the tenant realm + 3 OIDC clients: - wordpress (confidential, auth-code; redirect URIs cover the openid-connect-generic plugin's admin-ajax.php callback + /wp-login.php fallback) - openclaw (confidential, auth-code; redirect URI /oauth/callback per #915 spec) - stalwart (confidential, serviceAccountsEnabled=true so the directory.keycloak type=oidc backend can use client_credentials to introspect IMAP/SMTP tokens; standardFlowEnabled=true for webmail UI auth-code) * NEW per-app Secrets emitted in the same template scope as the realm ConfigMap so the realm JSON's `secret` field and the K8s Secret bytes never drift: - wordpress-oidc-client-secret - openclaw-oidc-client-secret - stalwart-oidc-client-secret (carries BOTH client-secret AND OIDC_CLIENT_SECRET keys for the two consumer paths) * Each per-app secret persists across helm upgrade via lookup-or-generate (mirrors marketplace-api/secret.yaml pattern from issue #887 and the existing catalyst-api-server secret in configmap-sovereign-realm.yaml). helm.sh/resource-policy: keep so bytes outlive uninstall. * Fail-closed validation when realmConfig.tenant.enabled=true and any of realmName / parentDomain / subdomain is unset (Inviolable Principle #4). NEW tests/tenant-realm-oidc-clients.sh covers 6 cases: 1. Sovereign-mode default render unchanged (kubectl + catalyst-ui + catalyst-api-server clients present, no tenant artefacts leak). 2. Tenant-mode render produces exactly ONE realm CM under the expected name + zero leaked Sovereign-only resources. 3. Tenant realm JSON parses + 3 OIDC clients present with the redirect-URI / publicClient / serviceAccountsEnabled shape per #915 spec; Secret bytes match realm JSON's `secret` fields. 4. Fail-closed validation when tenant fields missing. 5. keycloak-config-cli post-install Job projects the realm CM by SAME name in BOTH modes. 6. Operator-supplied per-app clientSecret overrides the lookup-or-generate path. Existing tests/observability-toggle.sh + tests/oidc-kubectl-client.sh still pass. Sovereign-mode unchanged. The chart now consumes the values the orchestrator (PR #911) was already emitting; no orchestrator change needed. Closes #915 (C1 sub-task) and unblocks #899 (per-tenant Keycloak realm-config materialisation). Co-authored-by: hatiyildiz <hatiyildiz@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| chart | ||
| blueprint.yaml | ||
| 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