Real Keycloak delegation experiment (#853)

Real Keycloak delegation experiment (#853)

Keycloak 26.7.3 supports the certificate-binding portion of the desktop gateway contract in a disposable HTTPS realm. This experiment uses the pinned image `quay.io/keycloak/keycloak@sha256:ff4257d0d64efbe99ed1ddfaf07765cc3c36dc7518bf8324d41961327f441c54`. It does not qualify the current Bridge or a production realm.

The fixture generates a private CA, server certificate, two client certificates and synthetic user/client credentials. Only host loopback is published. The realm enables standard token exchange and `tls.client.certificate.bound.access.tokens` on its confidential Bridge client. A password grant is used only to create the synthetic test session; it is not a proposed production login flow. No real credentials or realm exports are used.

The result records these outcomes:

CaseOutcome
Exchange with the original client certificate200; correct certificate thumbprint, `aud=gateway`, `azp=bridge`, session ID present
Exchange with another trusted certificate400 `invalid_request`
Exchange with no client certificate400 `invalid_request`
Gateway introspectionActive, with the certificate binding preserved
Logout with the wrong certificate401; token remains active
Logout with the matching certificate204; introspection becomes inactive
Browser-session parameter supplied to exchangeNo `desktop_browser_session_audience` claim in token or introspection

The final probe also checks that the exchanged token has not expired after logout, distinguishing revocation from ordinary expiry. Two exploratory runs initially expected status 400 for wrong-certificate logout; observed status was 401 with the session still active. The expectation was corrected to that exact unauthorized response, retaining the session-preservation assertion.

The development startup command exposed an internal HTTP listener despite the disable flag. Only HTTPS had been published to host loopback, and every probe used verified HTTPS, but the fixture was corrected to normal startup with an explicit local development database/cache. The final probe also asserts that the application startup log advertises HTTPS without an HTTP listener. This remains a disposable fixture configuration, not a production deployment profile.

The native support matches the pinned mTLS mapper implementation and standard exchange documentation. The administration guide's older warning about sender-constrained subject tokens does not describe the measured 26.7.3 behavior; version-pinned qualification is necessary. Incoming certificate trust follows Keycloak's mTLS configuration and truststore configuration.

A subsequent provider qualification implements and tests the missing claim in an opt-in issuer provider. This report preserves the native baseline; production Bridge integration remains open.

Remaining issuer/Bridge contract

The current Bridge verifier's default fetch transport does not present its gateway workload certificate, and its exchange form supplies no browser binding. Its issuer transport must use the same client certificate as gateway requests, including refresh/logout operations on bound tokens. Certificate rotation must account for outstanding bound refresh tokens; another certificate cannot simply resume them.

The browser claim remains required by the gateway. Supplying an arbitrary form parameter to stock token exchange does not create it. An issuer-side mapper or provider must bind a value derived from the Bridge's authenticated browser session to the verified subject session and authenticated requester. It must reject absent or malformed binding, restrict the issuing client, preserve the value through introspection, and distinguish two Bridge browser sessions belonging to the same user. Static claims, arbitrary browser actor headers and replacing this with a shared user/session identifier would not establish the required binding.

No fallback or relaxation was added to the gateway. The real tokens from this baseline remain insufficient for managed admission. Workspace/action mapping, fresh group/session reads through the production adapter, Bridge integration, browser-session negative cases and full RDP/Cockpit qualification remain open.

Reproduce and cleanup

python3 scripts/desktop-comparison/keycloak-fixture.py start --output /tmp/unique-desktop-issuer
# Wait for the owned container's realm to finish starting; do not recreate it
# because a request times out during startup.
python3 scripts/desktop-comparison/keycloak-fixture.py probe --output /tmp/unique-desktop-issuer
python3 scripts/desktop-comparison/keycloak-fixture.py stop --output /tmp/unique-desktop-issuer

Start refuses an existing directory. Probe/stop verify container ownership by label and private state. Stop removes only that container; private keys, synthetic credentials and logs remain in the private local fixture directory. Only the sanitized result and cleanup record are published.

All three experimental containers were removed and their IDs checked absent. Final probe log SHA-256: `9105a4127d79d67942d0a53d577d4f868ae94a9d1fced1b05b35c204204376c6`. Result artifact SHA-256: `6d2ab11c1c90ba6d73cd9b73be6ce3b6131ad63b9ed7f63809537eb51f9bfc7b`.