Secure Keycloak with cnspec
Scan Keycloak realms, clients, and users against security and compliance best practices with cnspec.
Keycloak decides who gets into everything behind it, so a weak realm setting or an over-permissive client reaches every application the realm protects. cnspec reads a Keycloak server through its admin REST API and evaluates realm password and lockout policy, token and session lifespans, the clients registered in each realm and the flows they enable, users and the roles mapped to them, federated identity providers, user federation directories, authentication flows, realm signing keys, and event recording.
If you're new to cnspec, start with the Quickstart. For an overview of every identity provider cnspec can scan, see the identity scanning overview.
Prerequisites
To scan Keycloak with cnspec, you must have:
- cnspec installed on your workstation
- A reachable Keycloak server, version 20 or newer
- Credentials that hold the realm-management view roles of every realm you read
cnspec only issues GET requests to Keycloak. It never changes a realm.
Authenticate
There are two ways to authenticate.
Admin user
The password grant signs in against the built-in admin-cli client. It's the quickest way to try the provider, and it inherits every privilege of the account:
cnspec shell keycloak --url https://keycloak.example.com --username admin --password YOUR_PASSWORDService account
A confidential client with service accounts enabled uses the client credentials grant. It holds only the roles it was granted, so you can limit it to read-only access to one realm:
cnspec shell keycloak --url https://keycloak.example.com --realm production \
--client-id mondoo-scanner --client-secret YOUR_SECRETTo create the service account, add a client in the realm, disable the standard flow, enable Client authentication and Service accounts roles, then assign these realm-management roles to its service account:
view-realmview-usersview-clientsview-identity-providersview-authorization
A service account needs those roles on every realm it reads. To read every realm on the server, grant the view-realm role of the master realm's realm-management client instead.
Server URL
Pass the base URL of the server. A server installed under a context path keeps that path, for example https://keycloak.example.com/auth. Where the server certificate comes from a private authority, pass that authority with --ca-cert, either as the PEM itself or as a path to it. Trusting the authority keeps the certificate verified, which skipping verification would not.
Environment variables
KEYCLOAK_URL, KEYCLOAK_REALM, KEYCLOAK_CLIENT_ID, KEYCLOAK_CLIENT_SECRET, KEYCLOAK_USERNAME, and KEYCLOAK_PASSWORD supply the same values as the flags:
export KEYCLOAK_URL=https://keycloak.example.com
export KEYCLOAK_USERNAME=admin
export KEYCLOAK_PASSWORD=YOUR_PASSWORDConnection options
| Option | Description |
|---|---|
--url | Base URL of the Keycloak server |
--realm | Scope the scan to a single realm instead of every realm the credentials can read |
--auth-realm | Realm the token is requested from, which defaults to master for a user, or the scanned realm for a service account |
--client-id | Client the token is requested for, which defaults to admin-cli for a user |
--client-secret | Secret of a confidential client, which selects service account authentication |
--username | Admin user to authenticate as, which selects password authentication |
--password | Password of the admin user |
--ca-cert | Certificate authority to trust for the server certificate |
--discover | What to discover: auto (default), all, or realms |
Verify with a quick Keycloak check
Confirm that cnspec can reach your server by opening a cnspec shell:
cnspec shell keycloak --url https://keycloak.example.com --username admin --password YOUR_PASSWORDcnspec> keycloak.realms { name enabled sslRequired bruteForceProtected }
keycloak.realms: [
0: {
name: "master"
enabled: true
sslRequired: "external"
bruteForceProtected: false
}
1: {
name: "production"
enabled: true
sslRequired: "all"
bruteForceProtected: true
}
]If cnspec connects and lists your realms, you're ready to scan.
Scan Keycloak
cnspec scan keycloak --url https://keycloak.example.com --username admin --password YOUR_PASSWORDTo scan a single realm:
cnspec scan keycloak --url https://keycloak.example.com --realm production \
--client-id mondoo-scanner --client-secret YOUR_SECRETTo scan each realm as its own asset:
cnspec scan keycloak --url https://keycloak.example.com --username admin --password YOUR_PASSWORD --discover realmsWhen a scan completes, cnspec prints a summary of all the checks it ran, grouped by policy, along with a risk score from 0 (no risk) to 100 (highest risk). Failed checks include remediation guidance to help you fix issues. To learn more about reading scan results, read Understand cnspec Results.
Mondoo doesn't yet ship an out-of-the-box Keycloak policy, so use the checks below as a starting point and create your own policies to meet your specific requirements.
Explore and test checks interactively
Open a cnspec shell to discover resources and try out checks:
cnspec shell keycloak --url https://keycloak.example.com --username admin --password YOUR_PASSWORDReview realm password and lockout policy
passwordPolicyRules parses the raw policy into its named rules, which is easier to test against than the string Keycloak stores:
cnspec> keycloak.realms {
name
passwordPolicy
passwordPolicyRules
bruteForceProtected
permanentLockout
failureFactor
maxFailureWaitSeconds
}Review token and session lifespans
cnspec> keycloak.realms {
name
accessTokenLifespan
ssoSessionIdleTimeout
ssoSessionMaxLifespan
revokeRefreshToken
refreshTokenMaxReuse
}Review realm login settings
cnspec> keycloak.realms {
name
registrationAllowed
resetPasswordAllowed
verifyEmail
loginWithEmailAllowed
duplicateEmailsAllowed
editUsernameAllowed
rememberMe
sslRequired
}List clients and the flows they enable
cnspec> keycloak.clients {
clientId
enabled
publicClient
standardFlowEnabled
implicitFlowEnabled
directAccessGrantsEnabled
serviceAccountsEnabled
}Find clients with wildcard redirect targets
cnspec derives hasWildcardRedirectUri and hasWildcardWebOrigin so you don't have to pattern match the raw lists yourself, and wildcardRedirectUris shows which entries triggered it:
cnspec> keycloak.clients.where(hasWildcardRedirectUri == true) { clientId publicClient wildcardRedirectUris }List users that hold a realm administration role
cnspec> keycloak.realms { name users.where(hasAdminRole == true) { username email enabled } }Find service accounts and the clients they belong to
cnspec> keycloak.realms { name users.where(isServiceAccount == true) { username serviceAccountClientId } }Review user federation directories
A user federation component points at a directory the realm authenticates against, so its transport and bind settings decide whether those credentials cross the network in the clear:
cnspec> keycloak.realms { name components.where(isUserFederation == true) { name providerId connectionUrl connectionEncrypted startTls editMode } }Review federated identity providers
cnspec> keycloak.identityProviders { alias providerId enabled validateSignature useJwksUrl trustEmail syncMode }Review realm signing keys
cnspec> keycloak.realms { name keys { kid algorithm type use status isActive } }Review event recording
cnspec> keycloak.realms { name eventsConfig { eventsEnabled adminEventsEnabled adminEventsDetailsEnabled eventsExpiration eventsListeners } }Review the required actions a realm asks of a user
cnspec> keycloak.realms { name requiredActions { alias name enabled defaultAction } }Inspect an authentication flow step by step
cnspec> keycloak.realms { name browserFlowRef { alias executions { level displayName requirement providerId } } }Walk from a resource to its neighbors
Realms, clients, users, groups, and roles reference each other, so a query can start from any of them and walk in either direction:
cnspec> keycloak.clients { clientId realm { name } }
cnspec> keycloak.realms { name groups { path allRoles { name } } }
cnspec> keycloak.clients.where(serviceAccountsEnabled == true) { clientId serviceAccountUser { username allRoles { name } } }Example security checks
Ensure every realm requires TLS
sslRequired is all, external, or none. Only all covers requests that reach the server from a private network:
cnspec> keycloak.realms.all(sslRequired == "all")
[ok] value: trueEnsure every realm locks accounts after repeated failed logins
cnspec> keycloak.realms.all(bruteForceProtected == true)
[ok] value: trueEnsure the lockout threshold is strict
cnspec> keycloak.realms.where(bruteForceProtected == true).all(failureFactor <= 5)
[ok] value: trueEnsure no realm lets users register themselves
cnspec> keycloak.realms.none(registrationAllowed == true)
[ok] value: trueEnsure every realm confirms an email address
cnspec> keycloak.realms.all(verifyEmail == true)
[ok] value: trueEnsure every realm sets a password policy
A realm with no policy accepts any password the user chooses:
cnspec> keycloak.realms.all(passwordPolicyRules.length > 0)
[ok] value: trueEnsure two accounts can't share an email address
cnspec> keycloak.realms.none(duplicateEmailsAllowed == true)
[ok] value: trueEnsure access tokens are short lived
cnspec> keycloak.realms.all(accessTokenLifespan <= 300)
[ok] value: trueEnsure a refresh token is invalidated once it's used
cnspec> keycloak.realms.all(revokeRefreshToken == true)
[ok] value: trueEnsure single sign-on sessions don't idle indefinitely
cnspec> keycloak.realms.all(ssoSessionIdleTimeout <= 1800)
[ok] value: trueEnsure no realm signs tokens with a symmetric algorithm
An HMAC algorithm signs with a secret the realm shares, so any holder of that secret can mint a token:
cnspec> keycloak.realms.none(defaultSignatureAlgorithm == "HS256")
[ok] value: trueEnsure no client accepts a wildcard redirect target
A wildcard redirect URI lets an authorization code be delivered to a URL the operator never approved:
cnspec> keycloak.clients.none(hasWildcardRedirectUri == true)
[ok] value: trueEnsure no client allows every browser origin
cnspec> keycloak.clients.none(hasWildcardWebOrigin == true)
[ok] value: trueEnsure the implicit flow is disabled everywhere
The implicit flow returns tokens in the browser URL, where they land in history and referrer headers:
cnspec> keycloak.clients.none(implicitFlowEnabled == true)
[ok] value: trueEnsure every public client uses PKCE
A public client authenticates without a secret, so without a proof key its authorization code can be exchanged by whoever intercepts it:
cnspec> keycloak.clients.where(publicClient == true).all(pkceCodeChallengeMethod == "S256")
[ok] value: trueEnsure no public client uses the direct access grant
The direct access grant takes a username and password from the client itself, which bypasses the browser flow and any step it enforces:
cnspec> keycloak.clients.where(publicClient == true).none(directAccessGrantsEnabled == true)
[ok] value: trueEnsure no client receives every role of the user
With the full scope on, a token minted for one application carries the roles that authorize the others:
cnspec> keycloak.clients.none(fullScopeAllowed == true)
[ok] value: trueEnsure every user federation connection is encrypted
cnspec> keycloak.realms.all(components.where(isUserFederation == true).all(connectionEncrypted == true))
[ok] value: trueEnsure every identity provider verifies assertion signatures
Without signature validation, an assertion that claims to come from the provider is accepted on its word:
cnspec> keycloak.identityProviders.where(enabled == true).all(validateSignature == true)
[ok] value: trueEnsure no identity provider is trusted to confirm an email address
trustEmail accepts the address from the provider without the realm confirming it, which matters where accounts are matched by email:
cnspec> keycloak.identityProviders.where(enabled == true).none(trustEmail == true)
[ok] value: trueEnsure login and admin events are recorded
cnspec> keycloak.realms.all(eventsConfig.eventsEnabled == true && eventsConfig.adminEventsEnabled == true)
[ok] value: trueEnsure every realm can require a one-time password
cnspec> keycloak.realms.all(requiredActions.where(alias == "CONFIGURE_TOTP").all(enabled == true))
[ok] value: trueReview accounts that never confirmed their email address
cnspec> keycloak.realms { name users.where(enabled == true && emailVerified == false) { username email } }Learn more
- Keycloak Resource Pack Reference: every Keycloak resource and field cnspec can query
- Write Effective MQL: guide to authoring checks and queries