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 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

  1. In the RunReveal app, click Add Source and choose Keycloak.
  2. Select Webhook as the ingest method and give the source a name.
  3. 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:

  1. Open the Keycloak admin console.
  2. For each realm, go to Realm Settings → Events.
  3. Under Event Listeners, add http-sender.
  4. Toggle Save Events on for both Login Events and Admin Events.
  5. 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 10

Object 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:

If using an AWS S3 bucket, use the following SNS topic ARN for your bucket notifications.

arn:aws:sns:<REGION>:253602268883:runreveal_keycloak
⚠️

SNS 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 25

Managed 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 >= 10

Admin 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 >= 5

Schema

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)

ColumnType
workspaceIDString
sourceIDString
sourceTypeString
sourceTTLUInt32
receivedAtDateTime
idString
eventTimeDateTime
eventNameString
eventIDString
srcIPString
srcASCountryCodeString
srcASNumberUInt32
srcASOrganizationString
srcCityString
srcConnectionTypeString
srcISPString
srcLatitudeFloat64
srcLongitudeFloat64
srcUserTypeString
dstIPString
dstASCountryCodeString
dstASNumberUInt32
dstASOrganizationString
dstCityString
dstConnectionTypeString
dstISPString
dstLatitudeFloat64
dstLongitudeFloat64
dstUserTypeString
actorMap(String, String)
tagsMap(String, String)
ColumnType
resourcesArray(String)
serviceNameString
enrichmentsArray(Tuple(data Map(String, String), name String, provider String, type String, value String))
readOnlyBool
rawLogString
keycloakEventIDString
realmIDString
typeString
clientIDString
userIDString
sessionIDString
ipAddressString
errorString
detailsString
usernameString
authMethodString
authTypeString
grantTypeString
redirectURIString
codeIDString
tokenIDString
refreshTokenIDString
scopeString
operationTypeString
resourceTypeString
resourcePathString
representationString
authDetails.userIDString
authDetails.clientIDString
authDetails.ipAddressString
authDetails.realmIDString