Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ Keep a Changelog, and releases use semantic versioning.

### Changed

- The portable Keycloak realm no longer embeds `ecosystem-rp-template` or
`naruon-web`; application RPs now exist only as validated runtime desired
state. Realm validation rejects application clients, and clean-realm recovery
is covered through the committed Naruon template and reconciliation path.
- Relying-party deployment controllers now send validated, secret-free metadata
to Keyverse desired-state PUT instead of applying client representations
directly to Keycloak; confidential credential placement remains a separate
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ network diagram: [`docs/topology.md`](docs/topology.md).
| Path | What |
| --- | --- |
| `docker-compose.yml` | Standalone bring-up: Keycloak + Postgres + admin service (pinned by digest) |
| `deploy/keycloak/` | Portable Keycloak realm config-as-code, passwordless flows, shared scopes, concrete Naruon RP, and service-account bootstrap |
| `deploy/keycloak/` | Portable Keycloak realm config-as-code, passwordless flows, shared scopes, and service-account bootstrap; application RPs are runtime desired state |
| `deploy/templates/` | Private deployment templates split between Keyverse preflight/desired state and explicit Keycloak Admin REST apply contracts |
| `deploy/bootstrap/` | Bootstrap pointer to the KV/DB config store |
| `deploy/scripts/healthz.sh` | Cross-component readiness probe |
Expand Down
33 changes: 16 additions & 17 deletions deploy/keycloak/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ providers are converged afterwards from the KV/DB source of truth.

| File | Responsibility |
| --- | --- |
| `realm-cwl.json` | Portable passwordless realm, shared client scopes, RP template, concrete `naruon-web` PKCE client, and account-unification service client |
| `realm-cwl.json` | Portable passwordless realm, shared client scopes, and the account-unification control-plane service client; no application RP clients |
| `kcadm-bootstrap.sh` | Idempotently inject the service-client secret, grant least-privilege realm-management roles, and reconcile the role mapper |
| `../templates/` | Reference payloads for runtime federation and additional relying-party registrations |

Expand Down Expand Up @@ -36,11 +36,12 @@ See [`../../docs/passwordless-policy.md`](../../docs/passwordless-policy.md).

## Portable realm versus deployment data

The committed realm contains no employer ADFS, LDAP/AD source, or other external
federation. Those objects are customer/deployment data and are managed through
`/federation/identity-providers`. Desired state is stored in the KV/DB backend
and can be reapplied after a realm rebuild with
`POST /federation/identity-providers:apply`.
The committed realm contains no employer ADFS, LDAP/AD source, external
federation, reusable RP template, or application RP. Those objects are
customer/deployment data. Federation is managed through
`/federation/identity-providers`; application clients are managed through
`/clients/relying-parties`. Desired state is stored in the KV/DB backend and can
be reapplied after a realm rebuild through the respective reconciliation route.

This separation also avoids Keycloak 26 import failures from placeholder SAML
URLs or invalid placeholder LDAP distinguished names.
Expand All @@ -54,21 +55,19 @@ URLs or invalid placeholder LDAP distinguished names.
- no password authenticator in any subflow reachable from `browserFlow`;
- `webauthn-register-passwordless` remains enabled;
- `basic`, `profile`, and `email` scopes exist, with `basic` providing `sub`;
- public `naruon-web` requires PKCE S256 and an access-token lifespan no greater
than 900 seconds;
- runtime application clients are rejected from the portable import;
- the `account-unification-svc` control-plane client remains present;
- committed client secrets are placeholders only.

## RP clients

`ecosystem-rp-template` is a confidential PKCE S256 blueprint. It uses the
reserved `rp.example.invalid` host so no product-specific deployment value is
silently inherited. Clones must replace redirect/origin values, client ID,
secret, and audience mapper together.

`naruon-web` is the first concrete public PKCE client. It carries the audience
and `role`/`org`/`workspace` claims required by the current Naruon session
contract. Its access tokens last 300 seconds; the longer SSO session is serviced
through normal token refresh/reissue rather than a twelve-hour bearer token.
`deploy/templates/oidc-rp-client.json` is the confidential PKCE S256 blueprint;
`deploy/templates/oidc-rp-naruon.json` is the concrete public Naruon profile.
Neither is imported with the realm. Render and preflight the chosen template,
persist it through Keyverse desired state, reconcile it into Keycloak, place any
confidential secret through the separate secret channel, and only then route
login traffic. The Naruon profile carries the reviewed audience and bounded
`role`/`org`/`workspace` claims and retains a 300-second access-token lifetime.

## Bootstrap

Expand Down
130 changes: 0 additions & 130 deletions deploy/keycloak/realm-cwl.json
Original file line number Diff line number Diff line change
Expand Up @@ -250,136 +250,6 @@
],
"defaultOptionalClientScopes": [],
"clients": [
{
"clientId": "ecosystem-rp-template",
"name": "Ecosystem RP template",
"enabled": true,
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"secret": "__set_from_kv__",
"redirectUris": [
"https://rp.example.invalid/auth/callback"
],
"webOrigins": [
"+"
],
"defaultClientScopes": [
"basic",
"profile",
"email"
],
"optionalClientScopes": [],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://rp.example.invalid/",
"access.token.lifespan": "300"
},
"protocolMappers": [
{
"name": "audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "ecosystem-rp-template",
"access.token.claim": "true",
"id.token.claim": "false",
"introspection.token.claim": "true"
}
}
],
"fullScopeAllowed": false
},
{
"clientId": "naruon-web",
"name": "Naruon web client",
"enabled": true,
"protocol": "openid-connect",
"publicClient": true,
"standardFlowEnabled": true,
"implicitFlowEnabled": false,
"directAccessGrantsEnabled": false,
"serviceAccountsEnabled": false,
"redirectUris": [
"https://naruon.example/auth/callback",
"https://naruon.example/auth/passkey-complete"
],
"webOrigins": [
"https://naruon.example"
],
"defaultClientScopes": [
"basic",
"profile",
"email"
],
"optionalClientScopes": [],
"attributes": {
"pkce.code.challenge.method": "S256",
"post.logout.redirect.uris": "https://naruon.example/",
"access.token.lifespan": "300"
},
"protocolMappers": [
{
"name": "audience",
"protocol": "openid-connect",
"protocolMapper": "oidc-audience-mapper",
"consentRequired": false,
"config": {
"included.client.audience": "naruon-web",
"access.token.claim": "true",
"id.token.claim": "false",
"introspection.token.claim": "true"
}
},
{
"name": "naruon-role",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"claim.name": "role",
"claim.value": "member",
"jsonType.label": "String",
"access.token.claim": "true",
"id.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "naruon-org",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"claim.name": "org",
"claim.value": "org-cwl",
"jsonType.label": "String",
"access.token.claim": "true",
"id.token.claim": "true",
"introspection.token.claim": "true"
}
},
{
"name": "naruon-workspace",
"protocol": "openid-connect",
"protocolMapper": "oidc-hardcoded-claim-mapper",
"consentRequired": false,
"config": {
"claim.name": "workspace",
"claim.value": "workspace-org-cwl",
"jsonType.label": "String",
"access.token.claim": "true",
"id.token.claim": "true",
"introspection.token.claim": "true"
}
}
],
"fullScopeAllowed": false
},
{
"clientId": "account-unification-svc",
"name": "Account unification service",
Expand Down
3 changes: 2 additions & 1 deletion deploy/templates/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ all `{{placeholders}}` must be resolved from the platform KV before use.
| `oidc-rp-client.json` | Keyverse RP desired-state API | Keyverse → RP | `POST /clients/relying-parties:validate` | `PUT /clients/relying-parties/{client_id}` |
| `oidc-rp-naruon.json` | Keyverse RP desired-state API | Keyverse → Naruon | `POST /clients/relying-parties:validate` | `PUT /clients/relying-parties/naruon-web` |

The portable realm contains no employer-specific federation. External SAML and
The portable realm contains no employer-specific federation or application
relying-party client. External SAML and
OIDC providers are customer or deployment data stored in the Keyverse KV/DB
desired-state registry and reconciled into Keycloak. OIDC relying-party clients
are likewise reconciled through Keyverse desired state rather than applied
Expand Down
5 changes: 5 additions & 0 deletions docs/OPERABILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ must test the **Naruon** product login/token/authorization journey using the
`naruon-web` RP client ID and verify the expected audience and bounded claims.
Mapper unit tests alone do not prove Naruon product authorization readiness.

After a clean realm import, bootstrap `account-unification-svc`, reconcile the
rendered runtime RP desired state, place confidential credentials where
applicable, and only then run controlled login acceptance or route traffic. The
portable realm intentionally creates no application RP as a fallback.

## Account merge recovery

Merge and SCIM full replacement (`PUT`) must hold the shared operation lock. Protected-main `PATCH active=false` is not currently inside that shared-lock guarantee and must not be treated as transactionally serialized with merge. On failure, classify whether state changed in Keycloak, Keyverse audit, linked identities, or tombstone status. Re-observe before retry. Never infer a retry is safe solely from the previous HTTP response. Preserve survivor and duplicate lineage in audit.
Expand Down
11 changes: 6 additions & 5 deletions docs/adr/0008-keyverse-rp-authorization-boundary.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,11 @@ The snapshot is reproducible from the Keyverse README at immutable revision
Keyverse itself also has two boundaries that must not be confused with
downstream application authorization:

- the portable realm currently contains the reviewed `naruon-web` claim shape
(`role`, `org`, and `workspace`), but those values are deployment/profile
data and do not grant privilege by themselves; user/tenant role derivation
and downstream authorization remain separate acceptance obligations;
- the reviewed runtime `naruon-web` desired-state template carries the bounded
`role`, `org`, and `workspace` claim shape, but those values are deployment
data and do not grant privilege by themselves; the portable realm contains
no application RP, and user/tenant role derivation plus downstream
authorization remain separate acceptance obligations;
- the account-unification inbound admin/SCIM surface currently has one
deployment-owned operator bearer gate. That is a coarse service boundary,
not per-operation RBAC or ABAC. Multi-operator production use requires
Expand Down Expand Up @@ -70,7 +71,7 @@ downstream application authorization:
bindings deny access;
- `role` is optional at token-validation level, but an RP that makes an RBAC
decision must require a recognized role and treat a missing or unknown
role as no privilege. The current portable `naruon-web` profile allows
role as no privilege. The current runtime `naruon-web` profile allows
only `member`; any new role value requires a separately reviewed mapper
profile, an exact issuer-side test, and downstream elevation/downgrade
tests;
Expand Down
17 changes: 10 additions & 7 deletions docs/doctoring/oidc-rp-claim-mapper-profile.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,13 +115,16 @@ RED receipt before the template was added.

## Limitations and follow-up

This slice does not prove a live authorization-code/PKCE exchange, downstream
audience acceptance, user/session migration, or clean-realm recovery. Those are
runtime evidence boundaries. It also does not remove runtime application
clients from the portable realm; that migration remains a separate reviewed
change. Any new mapper type, claim name, token destination, resource audience,
or native-client redirect profile requires explicit design and regression
coverage rather than extension by configuration alone.
The portable realm preserves only the `account-unification-svc` control-plane
client and contains no runtime application RP clients. A deterministic
clean-realm regression proves the committed Naruon template can recreate one
exact in-sync application client, including its closed mapper profile, through
Keyverse desired state. This does not prove a live authorization-code/PKCE exchange,
downstream audience acceptance, user/session migration, or hosted Keycloak
restore; those remain runtime evidence boundaries. Any new mapper type, claim
name, token destination, resource audience, or native-client redirect profile
requires explicit design and regression coverage rather than extension by
configuration alone.

## References

Expand Down
8 changes: 5 additions & 3 deletions docs/passwordless-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,14 @@ password to phish, reuse, reset, or leak.
| `resetPasswordAllowed` | `false` | No password-reset surface exists |
| `requiredActions[webauthn-register-passwordless]` | enabled | Keycloak can execute the passkey enrollment action |
| `webAuthnPolicyPasswordless*` | resident key and user verification required | Passkeys are discoverable and user-verified |
| `naruon-web.attributes[access.token.lifespan]` | `300` seconds | Public-client bearer exposure is bounded independently of the longer SSO session |

`scripts/validate_realm.py` follows every nested subflow reachable from
`browserFlow` and fails CI if it finds `auth-password-form`,
`auth-username-password-form`, or another password authenticator. It also caps
public `naruon-web` access tokens at 900 seconds.
`auth-username-password-form`, or another password authenticator. It also
rejects application RP clients from the portable realm. The runtime
`deploy/templates/oidc-rp-naruon.json` profile independently fixes the public
client access-token lifetime at 300 seconds and is validated by the same closed
Keyverse RP preflight used during deployment.

## Password-free headless registration

Expand Down
6 changes: 6 additions & 0 deletions docs/rp-onboarding.md
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,12 @@ validates the resulting tokens.

## Desired-state lifecycle

After a clean realm import, bootstrap the account-unification service first,
then validate and reconcile each runtime RP, place a confidential client secret
through the separate approved channel where applicable, and finally perform
controlled login acceptance before routing users. The portable realm does not
pre-create an application RP.

The authenticated surface is:

```text
Expand Down
Loading
Loading