Keycloak Audit: 7 Misconfigurations That Quietly Expose Your SSO
Somewhere in one of your realms there is a client with * in its Valid redirect URIs field. A developer put it there in 2023 because the OAuth flow kept failing in staging, and it worked, so it shipped. That single character means an attacker can craft a login link that uses your real Keycloak, your real login page, your users' real credentials — and delivers the authorization code to a domain the attacker controls. Your single sign-on is now a token-minting service for whoever finds it first.
That's the uncomfortable thing a Keycloak audit surfaces: the server itself is solid, but a realm accumulates configuration the way a shared drive accumulates files. Every new client, every role grant, every "temporary" tweak to make an integration work stays there until someone goes looking. And because Keycloak sits in front of everything, each misconfiguration is multiplied by every application behind it.
The failures are predictable, though. Across mid-market deployments they concentrate in the same seven places. This guide covers each one: what it is, how it gets that way, one concrete way to check it today — a console path or a kcadm.sh command — and what it costs you when it's exploited.
This is part one of a three-part series. Part two is a hands-on Keycloak audit using only kcadm.sh and the Admin REST API; part three covers keeping the audit continuous with an AI agent.
1. Public clients with wildcard redirect URIs
What it is. A public client (no secret — SPAs, mobile apps) whose Valid redirect URIs contain a wildcard: *, https://*.example.com/*, or an http:// scheme in production. Keycloak only checks that the redirect_uri in an authorization request matches a registered pattern; a wildcard means almost anything matches.
How it happens. Redirect mismatches are the most common OAuth integration error, and widening the pattern makes the error go away. Nobody comes back to narrow it.
Check it. Console: Clients → (client) → Settings → Valid redirect URIs. Across a whole realm:
kcadm.sh get clients -r production --fields clientId,publicClient,redirectUris,webOrigins \
| jq '.[] | select((.redirectUris[]? | test("\\*")) or (.webOrigins[]? == "*"))'
The consequence. Authorization-code interception and account takeover. The OAuth 2.0 Security Best Current Practice (RFC 9700) is blunt about this: redirect URIs must be compared by exact match. A bare * on a public client is the single worst finding a Keycloak audit can return. Check webOrigins too — a * there disables CORS protection for the token endpoint.
2. Confidential clients with weak or shared secrets
What it is. Confidential clients authenticate with a secret. In practice those secrets end up committed to Git, pasted into wikis, reused across dev/staging/production, or unchanged since the client was created — sometimes still a human-chosen string someone typed over the generated one.
How it happens. Keycloak generates a strong secret at creation, but it has to travel to the application somehow, and every hop (env file, CI variable, Slack message) is a place it leaks. Rotation is manual, so it doesn't happen.
Check it. Console: Clients → (client) → Credentials. Via CLI, pull the secret for a specific client and compare environments:
kcadm.sh get clients -r production -q clientId=backend-api --fields id,clientId
kcadm.sh get clients/CLIENT_UUID/client-secret -r production
If the same value appears in more than one realm or environment, treat it as compromised. Also grep your repositories for it.
The consequence. A leaked secret lets an attacker authenticate as the client — and if that client has a service account (see below), the secret is effectively a password to your IdP that no MFA policy touches.
3. Overprivileged service accounts and realm-admin grants
What it is. Clients with Service accounts enabled hold roles from the realm-management client. The dangerous pattern: an integration that needed to read users once, granted realm-admin "to get it working." That composite role can create users, reset passwords, and change any realm setting.
How it happens. The realm-management roles are granular (view-users, query-groups, manage-clients...), and figuring out the minimal set takes twenty minutes. Granting realm-admin takes one click.
Check it. Console: Clients → (client) → Service accounts roles. Via CLI, resolve the service-account user and inspect its role mappings:
kcadm.sh get clients/CLIENT_UUID/service-account-user -r production
kcadm.sh get users/SERVICE_ACCOUNT_USER_ID/role-mappings -r production \
| jq '.clientMappings["realm-management"].mappings[].name'
Anything returning realm-admin, manage-users, or manage-realm for an app that only reads data is a finding. Do the same review for human accounts in the master realm.
The consequence. Compromise of one backend service becomes full identity-provider takeover: silent user creation, password resets on executive accounts, new clients with wildcard redirects. This is how a small breach becomes a total one.
4. Brute-force protection and password policy left at defaults
What it is. Keycloak ships with brute-force detection disabled and with an empty password policy. Out of the box, a realm accepts unlimited login attempts against passwords like password1.
How it happens. Both settings live in places nobody visits after initial setup, and nothing warns you they're off.
Check it. Console: Realm settings → Security defenses → Brute force detection and Authentication → Policies → Password policy. Or in one call:
kcadm.sh get realms/production \
--fields bruteForceProtected,failureFactor,waitIncrementSeconds,maxFailureWaitSeconds,passwordPolicy
bruteForceProtected: false or a null passwordPolicy is your answer. See the server administration guide for the lockout parameters.
The consequence. Credential stuffing against your login page succeeds at line speed, and SSO amplifies the blast radius: one cracked password opens every connected application. Enable lockout, set a real policy, and require OTP as a required action for privileged accounts.
5. Excessive token and session lifetimes
What it is. Keycloak's defaults are reasonable — 5-minute access tokens, 30-minute SSO idle timeout — but teams raise them to stop mobile apps from "logging people out": 24-hour access tokens, 30-day offline sessions, idle timeouts measured in weeks.
How it happens. Session complaints reach the identity team immediately; the security cost of a long-lived token is invisible until an incident.
Check it. Console: Realm settings → Sessions and Realm settings → Tokens. Via CLI (values are in seconds):
kcadm.sh get realms/production \
--fields accessTokenLifespan,ssoSessionIdleTimeout,ssoSessionMaxLifespan,offlineSessionIdleTimeout
The consequence. Two failures at once. A stolen access token stays usable for hours instead of minutes. And disabling a departed employee doesn't fully take effect until their tokens expire — with a 24-hour lifespan, "access revoked" means "access revoked tomorrow." Keep access tokens at minutes and let refresh flows do the work.
6. Admin console reachable from the internet
What it is. The admin console and Admin REST API served on the same public hostname as your login pages. Every credential-stuffing bot that finds your SSO also finds /admin/.
How it happens. One hostname is simpler to deploy, and the console being reachable feels like a feature right up until the master-realm admin password is phished.
Check it. From a network that shouldn't have access:
curl -s -o /dev/null -w '%{http_code}\n' https://sso.example.com/admin/master/console/
A 200 from the open internet is a finding. Keycloak supports a separate admin hostname (--hostname-admin) precisely so you can keep /admin and /realms/master behind a VPN or IP allowlist at the reverse proxy.
The consequence. The admin console is the highest-value login form you operate. Combined with finding #4 (no lockout) it is brute-forceable; combined with a phished admin password and no OTP it is game over without an exploit ever being written.
7. Login and admin events not captured
What it is. Keycloak does not persist user login events or admin events by default. Until you switch them on, there is no record of who logged in, from where, who failed, or — critically — who changed the realm configuration.
How it happens. Event storage is opt-in per realm and lives on a settings tab nobody reads during setup.
Check it. Console: Realm settings → Events. Via CLI:
kcadm.sh get events/config -r production \
| jq '{eventsEnabled, eventsExpiration, adminEventsEnabled, adminEventsDetailsEnabled}'
All four should be enabled, with an expiration long enough for your incident-response window, and ideally shipped to your SIEM rather than left in Keycloak's database.
The consequence. When something does go wrong, you can't answer the first three questions of any investigation: when did the attacker log in, what did they touch, and who granted that role. Every other finding in this list gets worse when this one is present, because you can't prove whether it was ever exploited.
Why the annual access review doesn't hold
Run the seven checks above and you'll fix what you find. The harder problem is that a realm doesn't stay fixed. Every application launch adds a client, configured by whoever was in a hurry that day. Roles accrete with each integration and are never removed. A lifetime gets raised during an incident and stays raised. Six months after a clean Keycloak audit, the realm has drifted — and unlike a cloud bill, drift here has no line item that makes someone look.
The practical answer is to make the review cheap enough to run weekly instead of annually. Part two of this series turns everything above into a scripted sweep using kcadm.sh and the Admin REST API — exact commands, jq filters, the lot.
Keep the realm honest continuously
Everything in this guide can run unattended. Olivier, CloudThinker's security agent, connects to Keycloak with a read-only service account and inspects realms, clients, and role mappings continuously — flagging a wildcard redirect URI or a new realm-admin grant the day it appears, not at the next review. Nothing in your realm is changed without your approval.
Try CloudThinker free — 100 premium credits, no card required — or continue with the DIY audit walkthrough.
