Keycloak
Keycloak is an open-source identity and access management solution. RunReveal’s native Keycloak source ingests both user events (login, logout, token refresh, password reset, registration, etc.) and admin events (CRUD operations against realms, users, clients, roles, and groups) into the unified RunReveal log table for cross-source detections, search, and alerting.
Ingest Methods
Set up the source using one of the following methods:
- Webhook — push events directly from Keycloak via the
p2-inc/keycloak-eventsextension. - Object Storage — ship Keycloak’s JSON event stream (jboss-logging or stdout) into S3, Azure Blob, GCS, R2, or MinIO.
Webhook Setup
The simplest way to forward Keycloak events to RunReveal is the community
ext-event-http extension from p2-inc/keycloak-events.
The extension fires an HTTP POST for each event Keycloak emits — one JSON object per
request — to a configurable target URI.
1. Create the source in RunReveal
- In the RunReveal app, click Add Source and choose Keycloak.
- Select Webhook as the ingest method and give the source a name.
- Save the source. RunReveal will display a webhook URL of the form
https://api.runreveal.com/sources/keycloak/webhook/<KSUID>. The KSUID embedded in the path is the only secret — anyone with this URL can post events, so treat it like a credential.
2. Install and configure the Keycloak extension
Follow the ext-event-http install guide.
At a minimum you need to configure the following Keycloak SPI options (typically via
environment variables when running the standard Keycloak image):
KC_SPI_EVENTS_LISTENER_HTTP_SENDER_TARGET_URI=https://api.runreveal.com/sources/keycloak/webhook/<KSUID>
KC_SPI_EVENTS_LISTENER_HTTP_SENDER_RETRY=3
KC_SPI_EVENTS_LISTENER_HTTP_SENDER_BACKOFF_INITIAL_INTERVAL=PT1S
KC_SPI_EVENTS_LISTENER_HTTP_SENDER_FILTER_INCLUDE_EVENTS=*Then enable the listener for each realm you want to forward events from:
- Open the Keycloak admin console.
- For each realm, go to Realm Settings → Events.
- Under Event Listeners, add
http-sender. - Toggle Save Events on for both Login Events and Admin Events.
- Click Save.
3. Verify
Generate any Keycloak event (e.g. log in to the admin console). Within a few seconds the event should arrive in RunReveal:
SELECT * FROM runreveal.keycloak_logs ORDER BY eventTime DESC LIMIT 10Object Storage Setup
If you already ship Keycloak’s JSON-formatted event log (jboss-logging JSON layout, or
stdout JSON via Vector / Grafana Agent / Fluent Bit) into a cloud bucket, point a RunReveal
object-storage source at that bucket. RunReveal expects NDJSON — one Keycloak event
JSON object per line.
Pick the storage backend that matches your environment and follow the setup guide:
- AWS S3 Bucket
- AWS S3 Bucket with Custom SQS
- Azure Blob Storage
- Google Cloud Storage
- Cloudflare R2 Bucket
- MinIO
If using an AWS S3 bucket, use the following SNS topic ARN for your bucket notifications.
arn:aws:sns:<REGION>:253602268883:runreveal_keycloakSNS topic & Custom SQS. Use the ARN above in your event notification tied to your
S3 bucket — the topic name must match (runreveal_keycloak). For
Custom SQS, set the queue URL and region in RunReveal; see
AWS S3 Bucket with Custom SQS.
Note: BYOC, On-Prem, and BYODB customers must use their AWS account ID
in the ARN instead of 253602268883.
Event Shapes
Keycloak emits two distinct event shapes; RunReveal normalizes both into the same
keycloak_logs view.
User events (auth flow)
{
"id": "827218a4-0952-41b4-bbc7-617f7af6f73d",
"time": 1776867579090,
"type": "LOGIN_ERROR",
"realmId": "ce1541f1-8f04-49bd-9ee7-2873f7c7b1a1",
"clientId": "security-admin-console",
"userId": "be558f7b-be76-4d6c-bb66-5a23e4c8fbce",
"ipAddress": "172.17.0.1",
"error": "invalid_user_credentials",
"details": {
"auth_method": "openid-connect",
"auth_type": "code",
"redirect_uri": "http://127.0.0.1:8080/admin/master/console/",
"username": "test"
}
}eventName is set from the top-level type field
(LOGIN, LOGIN_ERROR, LOGOUT, CODE_TO_TOKEN, REFRESH_TOKEN, REGISTER,
RESET_PASSWORD_ERROR, TOKEN_EXCHANGE, USER_DISABLED_BY_*, etc.).
Admin events
{
"id": "dfbb2b76-47d5-40e0-9c14-91a3a6af3f5f",
"time": 1776867508190,
"realmId": "ce1541f1-8f04-49bd-9ee7-2873f7c7b1a1",
"authDetails": {
"realmId": "ce1541f1-8f04-49bd-9ee7-2873f7c7b1a1",
"clientId": "7d1a2d7d-4331-4d7e-ab9d-d6e0378d9f2f",
"userId": "e3d034d3-4ac9-4a59-967b-01b8079062d4",
"ipAddress": "172.17.0.1"
},
"operationType": "UPDATE",
"resourceType": "REALM",
"resourcePath": "users/be558f7b-be76-4d6c-bb66-5a23e4c8fbce",
"representation": "{...}"
}eventName is set to <operationType>:<resourceType> (e.g. CREATE:USER,
UPDATE:REALM, DELETE:CLIENT).
Verify It’s Working
Once the source is connected, logs should begin flowing within a minute or two. Validate with:
SELECT eventTime, eventName, srcIP, actor, realmID, resourceType, resourcePath
FROM runreveal.keycloak_logs
ORDER BY eventTime DESC
LIMIT 25Managed Detections (Reference SQL)
The following detections will ship in the public detection pack as a follow-up. You can already use them as scheduled queries in RunReveal today.
Brute force login attempts
Counts failed logins per (userID, ipAddress) over a rolling 15-minute window. Alerts
when a user/IP pair exceeds 10 failed logins.
SELECT
userID,
ipAddress,
realmID,
countIf(type = 'LOGIN_ERROR') AS failed_logins,
min(eventTime) AS first_seen,
max(eventTime) AS last_seen
FROM runreveal.keycloak_logs
WHERE eventTime >= now() - INTERVAL 15 MINUTE
AND type = 'LOGIN_ERROR'
GROUP BY userID, ipAddress, realmID
HAVING failed_logins >= 10Admin privilege change
Alerts on any privileged admin operation against a user, client, role, group, or realm.
SELECT
eventTime,
`authDetails.userID` AS adminUserID,
`authDetails.ipAddress` AS adminIP,
operationType,
resourceType,
resourcePath,
realmID
FROM runreveal.keycloak_logs
WHERE eventTime >= now() - INTERVAL 1 HOUR
AND operationType IN ('CREATE', 'UPDATE', 'DELETE')
AND resourceType IN (
'USER', 'CLIENT', 'ROLE', 'REALM', 'GROUP',
'REALM_ROLE', 'CLIENT_ROLE', 'REALM_ROLE_MAPPING', 'CLIENT_ROLE_MAPPING'
)Suspicious token activity
Catches lockouts and bursts of failed token operations per user/IP over 10 minutes. Lockout events are high-signal on their own; token-error bursts catch token-stealing or replay attempts.
SELECT
userID,
ipAddress,
realmID,
countIf(type IN (
'CODE_TO_TOKEN_ERROR',
'REFRESH_TOKEN_ERROR',
'TOKEN_EXCHANGE_ERROR'
)) AS token_errors,
countIf(type IN (
'USER_DISABLED_BY_PERMANENT_LOCKOUT',
'USER_DISABLED_BY_TEMPORARY_LOCKOUT'
)) AS lockouts,
min(eventTime) AS first_seen,
max(eventTime) AS last_seen
FROM runreveal.keycloak_logs
WHERE eventTime >= now() - INTERVAL 10 MINUTE
AND (
type IN (
'CODE_TO_TOKEN_ERROR',
'REFRESH_TOKEN_ERROR',
'TOKEN_EXCHANGE_ERROR',
'USER_DISABLED_BY_PERMANENT_LOCKOUT',
'USER_DISABLED_BY_TEMPORARY_LOCKOUT'
)
)
GROUP BY userID, ipAddress, realmID
HAVING lockouts >= 1 OR token_errors >= 5Schema
The following columns are exposed for this source. RunReveal applies schema normalization across all sources, ensuring uniform field names and data types for cross-source queries and reusable detection logic.
Table: keycloak_logs (62 columns)
keycloak_logs (62 columns)| Column | Type |
|---|---|
workspaceID | String |
sourceID | String |
sourceType | String |
sourceTTL | UInt32 |
receivedAt | DateTime |
id | String |
eventTime | DateTime |
eventName | String |
eventID | String |
srcIP | String |
srcASCountryCode | String |
srcASNumber | UInt32 |
srcASOrganization | String |
srcCity | String |
srcConnectionType | String |
srcISP | String |
srcLatitude | Float64 |
srcLongitude | Float64 |
srcUserType | String |
dstIP | String |
dstASCountryCode | String |
dstASNumber | UInt32 |
dstASOrganization | String |
dstCity | String |
dstConnectionType | String |
dstISP | String |
dstLatitude | Float64 |
dstLongitude | Float64 |
dstUserType | String |
actor | Map(String, String) |
tags | Map(String, String) |
| Column | Type |
|---|---|
resources | Array(String) |
serviceName | String |
enrichments | Array(Tuple(data Map(String, String), name String, provider String, type String, value String)) |
readOnly | Bool |
rawLog | String |
keycloakEventID | String |
realmID | String |
type | String |
clientID | String |
userID | String |
sessionID | String |
ipAddress | String |
error | String |
details | String |
username | String |
authMethod | String |
authType | String |
grantType | String |
redirectURI | String |
codeID | String |
tokenID | String |
refreshTokenID | String |
scope | String |
operationType | String |
resourceType | String |
resourcePath | String |
representation | String |
authDetails.userID | String |
authDetails.clientID | String |
authDetails.ipAddress | String |
authDetails.realmID | String |
Helpful Links
- Keycloak Project - Official Keycloak open-source identity and access management project.
- p2-inc/keycloak-events - Community extension that adds the
http-senderevent listener used by this integration. - Keycloak Server Administration: Auditing and Events - Upstream documentation on enabling user and admin events per realm.