Collect Keycloak logs
This document explains how to configure Keycloak to push logs to Google Security Operations using webhooks.
Keycloak is an open-source identity and access management (IAM) solution that provides single sign-on (SSO), user federation, identity brokering, and social login capabilities. It supports OpenID Connect, OAuth 2.0, and SAML 2.0 protocols and tracks user events (login, logout, registration, password changes) and admin events (user, client, realm, and role management operations) for security auditing.
Before you begin
Make sure that you have the following prerequisites:
- A Google SecOps instance
- A running Keycloak instance (version 20 or later recommended)
- Administrator access to the Keycloak Admin Console
- Access to the Keycloak server filesystem or container to deploy extensions
- Access to Google Cloud Console (for API key creation)
Create webhook feed in Google SecOps
Create the feed
- Go to SIEM Settings > Feeds.
- Click Add New Feed.
- On the next page, click Configure a single feed.
- In the Feed name field, enter a name for the feed (for example,
Keycloak Events). - Select Webhook as the Source type.
- Select Keycloak as the Log type.
- Click Next.
- Specify values for the following input parameters:
- Split delimiter (optional): Enter
\nto split multi-line events (each webhook POST contains a single event, so this can be left empty). - Asset namespace: The asset namespace
- Ingestion labels: The label to be applied to the events from this feed
- Split delimiter (optional): Enter
- Click Next.
- Review your new feed configuration in the Finalize screen, and then click Submit.
Generate and save secret key
After creating the feed, you must generate a secret key for authentication:
- On the feed details page, click Generate Secret Key.
- A dialog displays the secret key.
- Copy and save the secret key securely.
Important: The secret key is displayed only once and cannot be retrieved later. If you lose it, you must generate a new secret key.
Get the feed endpoint URL
- Go to the Details tab of the feed.
- In the Endpoint Information section, copy the Feed endpoint URL.
The URL format is:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreateor
https://<REGION>-malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreateSave this URL for the next steps.
Click Done.
Create Google Cloud API key
Chronicle requires an API key for authentication. Create a restricted API key in the Google Cloud Console.
Create the API key
- Go to the Google Cloud Console Credentials page.
- Select your project (the project associated with your Chronicle instance).
- Click Create credentials > API key.
- An API key is created and displayed in a dialog.
- Click Edit API key to restrict the key.
Restrict the API key
- In the API key settings page:
- Name: Enter a descriptive name (for example,
Chronicle Webhook API Key)
- Name: Enter a descriptive name (for example,
- Under API restrictions:
- Select Restrict key.
- In the Select APIs dropdown, search for and select Google SecOps API (or Chronicle API).
- Click Save.
- Copy the API key value from the API key field at the top of the page.
- Save the API key securely.
Enable event storage in Keycloak
Before configuring the webhook extension, enable event storage in Keycloak so that events are generated and available for forwarding.
Enable user events
- Sign in to the Keycloak Admin Console.
- Select the realm you want to monitor from the realm dropdown in the upper-left corner.
- Go to Realm Settings > Events.
- Select the User events settings sub-tab.
- Enable the Save events toggle.
- Set the Expiration period (minimum recommended: 7 days).
- Click Save.
Enable admin events
- On the same Events tab, select the Admin events settings sub-tab.
- Enable the Save events toggle.
- Enable the Include representation toggle to capture full details of changed objects.
- Set the Expiration period (minimum recommended: 7 days).
- Click Save.
Install the webhook event listener extension
Keycloak does not include a native webhook event listener. Install the keycloak-events extension from Phase Two (p2-inc) to enable webhook delivery.
Download and deploy the extension
Download the latest release JAR from the keycloak-events releases page on Maven Central or build from source:
git clone https://github.com/p2-inc/keycloak-events.git cd keycloak-events mvn clean installCopy the resulting fat JAR file into the Keycloak
providersdirectory:cp target/keycloak-events-*.jar /opt/keycloak/providers/Rebuild and restart Keycloak:
/opt/keycloak/bin/kc.sh build /opt/keycloak/bin/kc.sh start
Enable the webhook event listener
- Sign in to the Keycloak Admin Console.
- Select the target realm from the realm dropdown.
- Go to Realm Settings > Events.
- In the Event listeners dropdown, select ext-event-webhook.
- Click Save.
Configure Keycloak webhook
Construct the webhook URL
Combine the Chronicle endpoint URL and API key:
<ENDPOINT_URL>?key=<API_KEY>Example:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...
Create webhook subscription via Keycloak REST API
The keycloak-events extension provides REST endpoints for managing webhook subscriptions. Use the Keycloak Admin REST API to create a webhook.
Step 1: Obtain an access token
Request an access token from Keycloak using an admin account:
TOKEN=$(curl -sS -X POST "https://<KEYCLOAK_HOST>/realms/master/protocol/openid-connect/token" \ -H "Content-Type: application/x-www-form-urlencoded" \ --data-urlencode "grant_type=password" \ --data-urlencode "client_id=admin-cli" \ --data-urlencode "username=<ADMIN_USERNAME>" \ --data-urlencode "password=<ADMIN_PASSWORD>" \ | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')
Replace the following:
<KEYCLOAK_HOST>: Your Keycloak server hostname and port (for example,keycloak.example.com:8443)<ADMIN_USERNAME>: Your Keycloak admin username<ADMIN_PASSWORD>: Your Keycloak admin password
Step 2: Create the webhook
Send a POST request to create the webhook subscription for the target realm:
curl -sS -X POST "https://<KEYCLOAK_HOST>/realms/<REALM_NAME>/webhooks" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Content-Type: application/json" \ -d '{ "enabled": "true", "url": "<ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY>", "secret": "<WEBHOOK_HMAC_SECRET>", "eventTypes": ["*"] }'
Replace the following:
<KEYCLOAK_HOST>: Your Keycloak server hostname<REALM_NAME>: The name of the realm to monitor (for example,masterormy-realm)<ENDPOINT_URL>: The Chronicle feed endpoint URL copied earlier<API_KEY>: The Google Cloud API key created earlier<SECRET_KEY>: The Chronicle webhook secret key generated earlier<WEBHOOK_HMAC_SECRET>: An arbitrary secret string for HMAC signing of webhook payloads (for example,mySecretKey123)
Step 3: Verify the webhook
Confirm the webhook was created by listing all webhooks for the realm:
curl -sS -X GET "https://<KEYCLOAK_HOST>/realms/<REALM_NAME>/webhooks" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json"
The response returns a list of webhook objects. Verify that your webhook appears with "enabled": "true" and the correct URL.
Webhook event types
The eventTypes field accepts an array of expressions to filter which events are sent:
*— Send all events (recommended for SIEM integration)access.*— Send all access eventsadmin.*— Send all admin eventsadmin.USER-*— Send all admin events related to usersadmin-USER-CREATE— Send only user creation admin events
Webhook payload format
The webhook sends events as HTTP POST requests with JSON payloads. Example user event payload:
{ "id": "987865-1a2b-3c4d-9876-654321abc", "time": 1767799710612, "type": "LOGIN", "realmId": "12345abcde-1a2b-4d3c-9876-abcd456", "clientId": "account-console", "userId": "abcd456-1234-5678-abc9-987gfed654", "sessionId": "efghij-9876-abcd-456-11223344", "ipAddress": "203.0.113.45", "details": { "auth_method": "openid-connect", "auth_type": "code", "redirect_uri": "https://app.example.com/callback", "consent": "no_consent_required", "username": "jdoe" } }
Webhook retry behavior
The extension uses automatic exponential backoff for retries when a non-2xx response is received:
| Parameter | Default Value | Description |
|---|---|---|
| backoffInitialInterval | 500 ms | Initial retry interval |
| backoffMaxElapsedTime | 900000 ms (15 min) | Maximum total retry time |
| backoffMaxInterval | 180000 ms (3 min) | Maximum interval between retries |
| backoffMultiplier | 5 | Multiplier for each retry interval |
| backoffRandomizationFactor | 0.5 | Randomization factor for jitter |
Authentication methods reference
Chronicle webhook feeds support multiple authentication methods. Choose the method that your vendor supports.
Method 1: Custom headers (Recommended)
If your vendor supports custom HTTP headers, use this method for better security.
Request format:
POST <ENDPOINT_URL> HTTP/1.1 Content-Type: application/json x-goog-chronicle-auth: <API_KEY> x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Advantages:
- API key and secret not visible in URL
- More secure (headers not logged in web server access logs)
- Preferred method when vendor supports it
Method 2: Query parameters
If your vendor does not support custom headers, append credentials to the URL.
URL format:
<ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY>Example:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...&secret=abcd1234...Request format:
POST <ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY> HTTP/1.1 Content-Type: application/json { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Disadvantages:
- Credentials visible in URL
- May be logged in web server access logs
- Less secure than headers
Method 3: Hybrid (URL + Header)
Some configurations use API key in URL and secret key in header.
Request format:
POST <ENDPOINT_URL>?key=<API_KEY> HTTP/1.1 Content-Type: application/json x-chronicle-auth: <SECRET_KEY> { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
Authentication header names
Chronicle accepts the following header names for authentication:
For API key:
x-goog-chronicle-auth(recommended)X-Goog-Chronicle-Auth(case-insensitive)
For secret key:
x-chronicle-auth(recommended)X-Chronicle-Auth(case-insensitive)
Webhook limits and best practices
Request limits
| Limit | Value |
|---|---|
| Max request size | 4 MB |
| Max QPS (queries per second) | 15,000 |
| Request timeout | 30 seconds |
| Retry behavior | Automatic with exponential backoff |
UDM mapping table
| Log Field | UDM Mapping | Logic |
|---|---|---|
| payload.client_id | additional.fields | Merged with fields created from payload.client_id, payload.realm_id |
| payload.realm_id | additional.fields | |
| source_timestamp | metadata.event_timestamp | Parsed using date filter with patterns ISO8601 and yyyy-MM-dd'T'HH:mm:ss.SSSZ |
| payload.ip_address | metadata.event_type | Set to "STATUS_UPDATE" if payload.ip_address is not empty, else "USER_UNCATEGORIZED" if uuid is not empty, else "GENERIC_EVENT" |
| uuid | metadata.event_type | |
| payload.type | metadata.product_event_type | Value copied directly |
| payload.session_id | network.session_id | Value copied directly |
| payload.ip_address | principal.ip | Value copied directly |
| source_metadata.schema | principal.resource.attribute.labels | Merged with labels created from source_metadata.schema, source_metadata.table, source_metadata.is_deleted (converted to string), source_metadata.change_type, source_metadata.tx_id, source_metadata.lsn |
| source_metadata.table | principal.resource.attribute.labels | |
| source_metadata.is_deleted | principal.resource.attribute.labels | |
| source_metadata.change_type | principal.resource.attribute.labels | |
| source_metadata.tx_id | principal.resource.attribute.labels | |
| source_metadata.lsn | principal.resource.attribute.labels | |
| uuid | principal.user.userid | Value copied directly |
| object | security_result.detection_fields | Merged with labels created from object, read_method, payload.id |
| read_method | security_result.detection_fields | |
| payload.id | security_result.detection_fields | |
| redirect_uri | target.url | Value copied directly |
| username | target.user.userid | Value copied directly |
| metadata.product_name | metadata.product_name | Set to "KEYCLOAK" |
| metadata.vendor_name | metadata.vendor_name | Set to "KEYCLOAK" |
Need more help? Get answers from Community members and Google SecOps professionals.