openova/platform/keycloak
e3mrah 7f859dbb4b
feat(bp-keycloak): tenant-mode realm with wordpress/openclaw/stalwart OIDC clients (1.4.0, #915) (#918)
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>
2026-05-05 13:29:40 +04:00
..
chart feat(bp-keycloak): tenant-mode realm with wordpress/openclaw/stalwart OIDC clients (1.4.0, #915) (#918) 2026-05-05 13:29:40 +04:00
blueprint.yaml feat(bp-keycloak): tenant-mode realm with wordpress/openclaw/stalwart OIDC clients (1.4.0, #915) (#918) 2026-05-05 13:29:40 +04:00
README.md docs(pass-34): banned-term TENANT sweep + keycloak hostname drift 2026-04-27 22:42:50 +02:00

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"
  ]
}

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 bumped instances.

Part of OpenOva