Three coordinated deliverables for Sandbox Wave 1b — scaffolding +
design + the ONE prerequisite (long-lived org-scoped JWT) the rest of
Sandbox depends on.
Deliverable 1 — newapi proxy contract:
- products/sandbox/docs/newapi-proxy-contract.md: agent-pod env
(LLM_GATEWAY_URL / OPENAI_BASE_URL alias), provider selection
(?provider=qwen; default Qwen via omtd.bankdhofar.com), per-Sandbox
token issuance via /admin/tokens/sandbox bridge, lifecycle +
rotation, auth model.
- platform/newapi/internal/handler/sandbox_token.go: bridge handler
stub. Validates the inbound PAT (typ=pat + aud=newapi + org_id
cross-check vs request body), then echoes a NewAPI-shaped response
so the contract is testable without the upstream NewAPI admin
API. Wave 4 wires the actual upstream calls.
Deliverable 2 — Claude Code BYOS OAuth:
- products/sandbox/docs/claude-code-byos.md: UX (Connect Claude Max →
OAuth → refresh token Secret/catalyst-system/sandbox-byos-claude-
code-<user-uid>), Pod env injection (ANTHROPIC_API_KEY bypassing
newapi), per-session toggle, revocation paths, chart wiring.
- products/catalyst/bootstrap/api/internal/handler/byos_claude_code.go:
POST /start, GET /callback, DELETE, GET /status — four endpoints
behind RequireSession. Honest 503 + 501 surface so the popup
flow exercises end-to-end against the placeholder client_id;
Wave 4 flips it live.
Deliverable 3 — Long-lived org-scoped JWT (THE prerequisite):
- platform/keycloak/chart/templates/configmap-sovereign-realm.yaml +
configmap-tenant-realm.yaml: add `org` protocolMapper emitting
user attribute `org` as claim `org_id`; add `org` to default
client scopes for ALL clients.
- core/services/auth/handlers/handlers.go: include typ=session in
JWTs + document the cross-service claim contract.
- core/services/auth/handlers/pat.go: NEW POST /auth/pat with
admin-configurable TTL (default 7d, max 90d), audience claim,
capabilities pass-through, typ=pat discriminator.
- core/services/auth/handlers/routes.go + main.go: wire /auth/pat
behind JWTAuth middleware.
- core/services/shared/auth/claims.go: single Claims struct +
HasCapability/HasGroup helpers + ContextKey for cross-service
consumers (sandbox-controller, newapi bridge, MCP server).
- products/catalyst/bootstrap/api/internal/auth/session.go: align
Org JSON tag with new `org_id` claim; UnmarshalJSON accepts BOTH
legacy `org` and new `org_id` so a rolling chart upgrade does
not regress org-scoped queries.
Out of scope (Wave 4 wires):
- Sandbox CRD + controller (writes Secret, mounts Pod env).
- Actual outbound HTTP to Anthropic /oauth/token + KMS encrypt.
- Actual outbound HTTP to NewAPI admin API.
- Per-Sandbox capability projection from Keycloak groups.
- PAT revocation lookup (jti store) + /auth/pats list.
- Settings UI card + session-toolbar routing toggle.
Build verification (go vet + go build clean):
- core/services/auth/...
- core/services/shared/...
- platform/newapi/internal/handler/...
- products/catalyst/bootstrap/api/...
Founder TODO (single knob to flip BYOS live, Wave 4):
Register an Anthropic OAuth client at
https://console.anthropic.com/settings/oauth (public PKCE,
redirect=https://console.<sov-fqdn>/api/v1/sandbox/byos/claude-code/callback)
and paste the client_id into clusters/<sovereign>/bootstrap-kit/
sandbox.yaml. Today every BYOS endpoint returns 503 with a clear
message pointing at claude-code-byos.md §8.
Refs: products/sandbox/docs/architecture.md §6 (THE prerequisite).
Co-authored-by: hatiyildiz <269457768+hatiyildiz@users.noreply.github.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