Restrict Langfuse Access to AIHubSysAdmin via a Keycloak Authentication-Flow Gate
Context
Langfuse is exposed at langfuse.<domain> and uses its native Keycloak SSO integration for login (see Replace Arize Phoenix with Langfuse). Keycloak, by default, allows any authenticated realm user to complete a login against any OIDC client. On top of that, Langfuse's auto-provisioning (LANGFUSE_DEFAULT_ORG_ID / LANGFUSE_DEFAULT_ORG_ROLE: MEMBER) added every SSO user as a member of the aihub-org organization and aihub-project project. As a result, every AI-Hub user could log into Langfuse and read all traces, which may contain sensitive prompts and documents.
All other infrastructure UIs (Dagster, SeaweedFS, Attu, Backup) are gated behind oauth2-proxy sidecars with OAUTH2_PROXY_ALLOWED_GROUPS=AIHubSysAdmin. Langfuse must be equally restricted to users holding the AIHubSysAdmin realm role, but it differs from those services: it has first-class OIDC support and its own session handling, so wrapping it in an additional proxy layer adds a container, a second cookie domain, and a second login hop without adding value.
Decision Drivers
- Least privilege
Traces contain user prompts, retrieved documents, and model outputs. Only system administrators may see them. - No additional runtime component
Langfuse already authenticates against Keycloak; enforcing the restriction at the identity provider avoids an extra oauth2-proxy container and keeps the native SSO user experience (single login, clean Keycloak error page on denial). - Must reach already-running instances
The realm JSON is only imported on the first Keycloak start, and Keycloak'spartialImportAPI does not support authentication flows. The mechanism must therefore reconcile existing deployments without manual admin-console work. - Per-client scoping without per-client flow maintenance
The deny rule must only affect thelangfuseclient; all other clients (frontend, OpenWebUI, account console, oauth2-proxy clients) must be untouched. - Established pattern
The realm already denies IdP users without theAIHubAccessrole via a conditional deny sub-flow (Post Broker Login - AIHubAccess Check). The same building blocks extend naturally to a per-client role gate.
Decision
We gate Langfuse logins inside Keycloak with a conditional deny in the authentication flows:
Marker client scope: A mapper-less client scope langfuse-sysadmin-gate is attached as a default client scope to the langfuse client only. Keycloak's Condition - client scope authenticator matches a client's default scopes regardless of the requested scope parameter, so the scope acts as a reliable per-client marker.
Gate sub-flows: A conditional sub-flow — Condition - client scope (= langfuse-sysadmin-gate) AND Condition - user role (AIHubSysAdmin, negated) → Deny access — is added in two places, covering all login paths:
langfuse-gate-browserin a custom top-level browser flowbrowser-aihub(bound as the realm browser flow), which replicates the built-in browser flow (cookie, IdP redirector, forms, conditional OTP). The authentication alternatives are nested in a REQUIRED sub-flow (browser-aihub-authenticate) because Keycloak ignores ALTERNATIVE executions that share a level with a CONDITIONAL sub-flow — placing the gate next to the alternatives would break login for everyone. This flow covers users with an existing Keycloak SSO session and direct Keycloak logins.langfuse-gate-post-brokerappended to the existingPost Broker Login - AIHubAccess Checkflow. This covers fresh Azure AD brokered logins, which bypass the remainder of the browser flow after the IdP redirect.
Reconciliation on running instances: keycloak-entrypoint.sh applies the same configuration idempotently via kcadm on every container start (create marker scope, attach to client, build the browser-aihub flow if absent, add the gate sub-flows, bind the realm browser flow). Fresh installations get the identical state declaratively from the realm import; every kcadm step checks for existence first and no-ops.
Langfuse-side settings (LANGFUSE_DEFAULT_* auto-provisioning, AUTH_DISABLE_SIGNUP=false) are intentionally kept: with the Keycloak gate in place, only sysadmins can complete SSO, and auto-provisioning conveniently grants them org/project membership on first login.
Consequences
Positive
- Only users with the
AIHubSysAdminrealm role can log into Langfuse; everyone else is denied at Keycloak with a clear error message, on both fresh logins and existing SSO sessions. - No new runtime component; single login flow preserved.
- Running environments converge automatically on the next deployment (Keycloak container restart) — no manual realm surgery, no re-import, no downtime beyond the restart.
- The deny pattern is consistent with the existing
AIHubAccessgate and reusable for future per-client restrictions (add the marker scope to another client, or clone the pattern with a different scope/role).
Trade-offs
- The custom
browser-aihubflow no longer receives Keycloak's built-in browser-flow migrations on version upgrades (e.g. new passkey executions) and must be reviewed on major Keycloak upgrades. SPNEGO, organization, and passkey executions are intentionally omitted (unused in this deployment). - Langfuse's email/password login bypasses Keycloak entirely. It is disabled on
nightly/latest(AUTH_DISABLE_USERNAME_PASSWORD=true) but remains available ondev/local/buildfor development. - The gate blocks future logins only. Users already provisioned into
aihub-orgmust be removed manually in the Langfuse UI (Organization → Members), and active Langfuse sessions persist until expiry (rotateLANGFUSE_NEXTAUTH_SECRETfor immediate revocation). - Operators must hold the
AIHubSysAdminrole (Azure AD app role, mapped by therole-mapper-sysadminIdP mapper) before the rollout, otherwise they lock themselves out of Langfuse.
Related decisions: Replace Arize Phoenix with Langfuse, Keycloak as Identity Broker, Superuser via Keycloak Realm Role.
