Keycloak Audit: 7 Misconfigurations That Quietly Expose Your SSO
Somewhere in one of your realms there's a client with * in its Valid redirect URIs field. A developer put it there in 2023 because the OAuth flow kept failing in staging. It worked, so it shipped. That one character means an attacker can craft a login link that uses your real Keycloak, your real login page and 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 what a Keycloak audit tends to surface. The server itself is solid. The realm is the problem, because a realm collects configuration the way a shared drive collects 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 land in the same seven places. This guide covers each one: what it is, how it gets that way, a console path or kcadm.sh command that checks it today, and what it costs you when someone exploits it.
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.
01Clients
Redirects and secrets
Wildcard redirect URIs on public clients, and confidential client secrets that travel, repeat and never rotate.
02Privilege
Admin roles
Service accounts and humans holding realm-admin or manage-* roles they never use.
03Realm defaults
Lockout and lifetimes
Brute-force detection off, no password policy, and token lifetimes stretched to stop logout complaints.
04Exposure
Console and events
An admin console on the public hostname, and no event storage to prove what happened.
Every command below assumes kcadm.sh is already logged in with kcadm.sh config credentials and that your realm is called production. Part two covers setting up a read-only audit account for this, which you should do before running anything against a live realm.
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: redirect URIs must be compared by exact string match. A bare * on a public client is the worst single finding a Keycloak audit can return. Check webOrigins too. A * there switches off 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 and production, or unchanged since the client was created. Sometimes it's 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. With the 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 shows up in more than one realm or environment, treat it as compromised. Then search your repositories for it.
The consequence. A leaked secret lets an attacker authenticate as the client. 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 is an integration that needed to read users once and got 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 so on), and working out the minimal set takes twenty minutes. Granting realm-admin takes one click.
Check it. Console: Clients → (client) → Service accounts roles. With the 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. Run 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 off and 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. The server administration guide covers the lockout parameters.
The consequence. Credential stuffing against your login page runs at line speed, and SSO widens the blast radius: one cracked password opens every connected application. Turn on 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 sensible (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 stays invisible until an incident.
Check it. Console: Realm settings → Sessions and Realm settings → Tokens. With the 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 a reachable console feels like a convenience right up until the master-realm admin password gets 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) so you can keep /admin and /realms/master behind a VPN or an IP allowlist at the reverse proxy.
The consequence. The admin console is the highest-value login form you run. With finding #4 (no lockout) it can be brute-forced. With a phished admin password and no OTP, it's game over without anyone writing an exploit.
7. Login and admin events not captured
What it is. Keycloak doesn't persist user login events or admin events by default. Until you switch them on, there's no record of who logged in, from where, who failed, or (the one that matters most) who changed the realm configuration.
How it happens. Event storage is opt-in per realm and sits on a settings tab nobody reads during setup.
Check it. Console: Realm settings → Events. With the CLI:
kcadm.sh get events/config -r production \
| jq '{eventsEnabled, eventsExpiration, adminEventsEnabled, adminEventsDetailsEnabled}'
All four should be set, with an expiration long enough for your incident-response window, and ideally the events should be shipped to your SIEM rather than left in Keycloak's database.
The consequence. When something goes 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 on this list gets worse when this one is present, because you can't prove whether it was ever exploited.
How the findings chain
Auditors often score these seven one at a time. Attackers don't. A realm with three of them has a path, not three problems:
01Finding 1
Wildcard redirect
A crafted login link sends a real authorization code to an attacker domain.
02Finding 3
Admin service account
One leaked client secret becomes realm-admin, with no MFA in the way.
03Finding 4
No lockout
Credential stuffing against the login page runs at full speed.
04Finding 7
No events
Afterwards, nobody can say who logged in or who granted what.
That's why the order of fixes matters. Turn on events first (finding 7), so that whatever you discover next can be investigated. Then close the takeover paths (findings 1 and 3), then the realm defaults.
The seven checks on one page
| # | Finding | Where to look | What good looks like |
|---|---|---|---|
| 1 | Wildcard redirect URIs | redirectUris, webOrigins on each client | Exact URIs, + or exact origins |
| 2 | Weak or shared secrets | Clients → Credentials | Unique per environment, rotated on a schedule |
| 3 | Admin service accounts | role-mappings on service-account users | Only the view-* or query-* roles the app uses |
| 4 | Defaults on lockout and passwords | bruteForceProtected, passwordPolicy | Lockout on, a real policy, OTP for admins |
| 5 | Long token lifetimes | accessTokenLifespan, session timeouts | Access tokens in minutes, sessions matched to policy |
| 6 | Public admin console | /admin/master/console/ from outside | --hostname-admin behind VPN or allowlist |
| 7 | No events | events/config | Login and admin events on, with details, shipped to a SIEM |
Why the annual access review doesn't hold
Run the seven checks and you'll fix what you find. The harder problem is that a realm doesn't stay fixed.
01Review day
Clean realm
The seven checks pass and the findings get fixed.
02Weeks 1 to 8
New clients
Launches add redirect URIs, service accounts and role grants under deadline.
03Incident week
Temporary tweaks
A lifetime gets raised or a role granted to unblock someone, and stays.
04Next review
Drifted realm
Months of changes, no line item that made anyone look.
The causes are structural. Client registration has no review gate: anyone with manage-clients can add one, configured by whoever was in a hurry that day. Role grants are additive by habit: integrations get roles and nobody removes them, because removal might break something and nobody owns the decision. Lifetimes and lockout settings change during incidents, when the goal is "make it work now". And the knowledge of why a client exists lives in the head of the engineer who created it.
Six months after a clean Keycloak audit, the realm has drifted. Unlike a cloud bill, drift here has no line item that makes someone look. A yearly review is a single sample of a system that changes every sprint.
Where this series goes next
The practical answer is to make the review cheap enough to run weekly instead of yearly. Part two turns everything above into a scripted sweep with kcadm.sh and the Admin REST API: exact commands, jq filters and a weekly diff.
Part three covers the step after: CloudThinker connects to Keycloak with a read-only service account, inspects realms, clients and role mappings continuously, and flags a wildcard redirect URI or a new realm-admin grant the day it appears, with the evidence attached. Nothing in your realm changes without your approval.
Related reading
- Part two: Keycloak audit with kcadm.sh, events and the Admin API
- Part three: automating your Keycloak audit with an AI security agent
- What is non-human identity?
- AI security operations agent
Try CloudThinker free with 100 premium credits and no card required, or continue with the DIY audit walkthrough.
