Chuyển đến nội dung chính

Lesson 18: Event System and Audit Logging

Event types (Login Events, Admin Events), enable event logging, configure event listeners (jboss-logging, email), Event Store, Event Details, event filtering, query events via Admin Console and REST API, custom Event Listener SPI, ELK Stack / Grafana Loki integration for centralized logging, SIEM integration, audit compliance (SOC2, HIPAA) and alert automation.

🔒 DevSecOps — Lesson 18 Lesson 18: Event System and Audit Logging

Keycloak from Basic to Advanced

Part 5: Themes, Events, Security and Vault

xdev.asia

1. Event System Overview

Keycloak provides a comprehensive Event System system to monitor all activities in the system. Every action from logging in, registering, to changing admin configuration is logged as events.

1.1 Two types of Events

TypeDescriptionExample
Login Events (User Events)Actions related to usersLOGIN, REGISTER, LOGOUT, TOKEN_EXCHANGE
Admin EventsConfiguration changes via Admin Console/APICREATE user, UPDATE realm, DELETE client

1.2 Login Event Types

Event TypeDescriptionWhen does it occur
LOGINSuccessful loginUser entered correct credentials
LOGIN_ERRORLogin failedWrong username/password
REGISTERRegister new accountUser successfully created account
REGISTER_ERRORRegistration failedDuplicate email, validation failed
LOGOUTLogoutUser logout or session expired
CODE_TO_TOKENExchange authorization code → tokenOIDC Authorization Code flow
CODE_TO_TOKEN_ERRORToken exchange failedInvalid code, expired code
REFRESH_TOKENRefresh access tokenClient uses refresh token
REFRESH_TOKEN_ERRORRefresh token failedToken revoked or expired
CLIENT_LOGINClient authenticationService account login
INTROSPECT_TOKENToken introspectionResource server verify token
UPDATE_PASSWORDChange passwordUser change password
RESET_PASSWORDReset passwordUser reset via email link
VERIFY_EMAILEmail verificationUser click verification link
SEND_RESET_PASSWORDSend password reset emailRequest forgotten password
UPDATE_PROFILEUpdate profileUser updates personal information
REMOVE_TOTPDelete TOTP deviceUser remove OTP authenticator
UPDATE_TOTPTOTP configurationUser set OTP
GRANT_CONSENTUser grants consentOAuth2 consent screen
TOKEN_EXCHANGEToken exchangeExchange tokens between clients

1.3 Admin Event Operations

OperationDescriptionResource Types
CREATECreate new resourceUSER, CLIENT, REALM, GROUP, ROLE...
UPDATEUpdate resourceUSER, CLIENT, REALM_SETTINGS...
DELETEDelete resourceUSER, CLIENT, SESSION...
ACTIONPerform actionRESET_PASSWORD, SEND_VERIFY_EMAIL...

2. Turn on Event Logging

2.1 Qua Admin Console

  1. Login Admin Console
  2. Select Realm → Realm Settings → tab Events
  3. Configuration User events settings:
    • Save events: ON
    • Expiration: 30 days (depending on compliance requirements)
    • Saved types: Select event types to save (default: ALL)
  4. Configuration Admin events settings:
    • Save events: ON
    • Include representation: ON (save request/response body)
  5. Event listeners: Add necessary listeners

2.2 Qua REST API

# Bật event logging cho realm
curl -X PUT "http://localhost:8080/admin/realms/my-realm" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "eventsEnabled": true,
    "eventsExpiration": 2592000,
    "eventsListeners": ["jboss-logging"],
    "enabledEventTypes": [
        "LOGIN", "LOGIN_ERROR",
        "REGISTER", "REGISTER_ERROR",
        "LOGOUT",
        "CODE_TO_TOKEN", "CODE_TO_TOKEN_ERROR",
        "REFRESH_TOKEN", "REFRESH_TOKEN_ERROR",
        "CLIENT_LOGIN", "CLIENT_LOGIN_ERROR",
        "UPDATE_PASSWORD",
        "RESET_PASSWORD",
        "SEND_RESET_PASSWORD"
    ],
    "adminEventsEnabled": true,
    "adminEventsDetailsEnabled": true
  }'

3. Event Listeners

Event Listeners handle events as they occur. Keycloak has the following listeners available:

3.1 jboss-logging Listener

Log events to Keycloak server log (enabled by default):

# Log output mẫu
2026-03-15 10:30:45,123 INFO  [org.keycloak.events] (executor-thread-1)
  type=LOGIN, realmId=my-realm, clientId=my-app, userId=abc-123,
  ipAddress=192.168.1.100, auth_method=openid-connect,
  auth_type=code, redirect_uri=https://myapp.com/callback,
  [email protected]

3.2 Email Listener

Send email to user when there is an important event (for example: login from a new device):

# Bật email listener
Realm Settings → Events → Event listeners → Thêm "email"

# Events được gửi email por defecto:
- LOGIN_ERROR (quá nhiều lần → cảnh báo compromised account)
- UPDATE_PASSWORD
- REMOVE_TOTP
- UPDATE_TOTP

4. Event Details and Event Store

4.1 Login Event Structure

{
    "time": 1710489045000,
    "type": "LOGIN",
    "realmId": "my-realm",
    "clientId": "my-web-app",
    "userId": "550e8400-e29b-41d4-a716-446655440000",
    "sessionId": "abc-session-id",
    "ipAddress": "192.168.1.100",
    "details": {
        "auth_method": "openid-connect",
        "auth_type": "code",
        "redirect_uri": "https://myapp.com/callback",
        "consent": "no_consent_required",
        "code_id": "xyz-code-id",
        "username": "[email protected]",
        "identity_provider": null
    }
}

4.2 Admin Event Structure

{
    "time": 1710489100000,
    "realmId": "my-realm",
    "authDetails": {
        "realmId": "master",
        "clientId": "security-admin-console",
        "userId": "admin-user-id",
        "ipAddress": "10.0.0.1"
    },
    "operationType": "CREATE",
    "resourceType": "USER",
    "resourcePath": "users/new-user-id",
    "representation": "{\"username\":\"newuser\",\"email\":\"[email protected]\",\"enabled\":true}"
}

4.3 Event Store — Database

Events are stored in Keycloak's database:

TableContent
EVENT_ENTITYLogin events
ADMIN_EVENT_ENTITYAdmin events

Note: Event store is saved in Keycloak DB by default. With a large number of events, you should use a custom Event Listener to ship events to the external system and set short expiration for the built-in store.

5. Event Filtering and Querying

5.1 Qua Admin Console

  1. Go to Events → tab User events or Admin events
  2. Filter events theo:
    • Event type: LOGIN, LOGIN_ERROR, REGISTER...
    • Client: Select specific client
    • User: Search theo user ID
    • Date range: From/To date
    • IP Address: Filter theo IP

5.2 Qua REST API — Login Events

# Lấy tất cả login events
curl -s "http://localhost:8080/admin/realms/my-realm/events" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter theo event type
curl -s "http://localhost:8080/admin/realms/my-realm/events?type=LOGIN_ERROR" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter theo user
curl -s "http://localhost:8080/admin/realms/my-realm/events?user=user-uuid" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter theo client và date range
curl -s "http://localhost:8080/admin/realms/my-realm/events?\
client=my-app&\
dateFrom=2026-03-01&\
dateTo=2026-03-31&\
first=0&\
max=100" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter nhiều event types
curl -s "http://localhost:8080/admin/realms/my-realm/events?\
type=LOGIN&type=LOGIN_ERROR&type=REGISTER" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

5.3 Qua REST API — Admin Events

# Lấy admin events
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter theo operation type
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events?\
operationTypes=CREATE&\
resourceTypes=USER" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

# Filter theo resource path
curl -s "http://localhost:8080/admin/realms/my-realm/admin-events?\
resourcePath=users" \
  -H "Authorization: Bearer $ACCESS_TOKEN" | jq .

6. Custom Event Listener SPI

Keycloak allows creating custom Event Listeners through Service Provider Interface (SPI).

6.1 Create Maven Project

<!-- pom.xml -->
<project>
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>custom-event-listener</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>

    <properties>
        <keycloak.version>26.1.0</keycloak.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-server-spi-private</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.keycloak</groupId>
            <artifactId>keycloak-services</artifactId>
            <version>${keycloak.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
</project>

6.2 Implement EventListenerProvider

// src/main/java/com/example/CustomEventListenerProvider.java
package com.example;

import org.keycloak.events.Event;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventType;
import org.keycloak.events.admin.AdminEvent;
import org.keycloak.events.admin.OperationType;
import org.keycloak.models.KeycloakSession;
import org.jboss.logging.Logger;

import java.util.Map;

public class CustomEventListenerProvider implements EventListenerProvider {

    private static final Logger log = Logger.getLogger(CustomEventListenerProvider.class);
    private final KeycloakSession session;

    public CustomEventListenerProvider(KeycloakSession session) {
        this.session = session;
    }

    @Override
    public void onEvent(Event event) {
        // Xử lý Login Events
        log.infof("Event: type=%s, realmId=%s, clientId=%s, userId=%s, ip=%s",
                event.getType(),
                event.getRealmId(),
                event.getClientId(),
                event.getUserId(),
                event.getIpAddress());

        // Xử lý theo event type
        switch (event.getType()) {
            case LOGIN:
                handleLogin(event);
                break;
            case LOGIN_ERROR:
                handleLoginError(event);
                break;
            case REGISTER:
                handleRegister(event);
                break;
            default:
                break;
        }
    }

    @Override
    public void onEvent(AdminEvent event, boolean includeRepresentation) {
        // Xử lý Admin Events
        log.infof("AdminEvent: operation=%s, resourceType=%s, resourcePath=%s, realmId=%s",
                event.getOperationType(),
                event.getResourceType(),
                event.getResourcePath(),
                event.getRealmId());

        if (event.getOperationType() == OperationType.DELETE) {
            handleAdminDelete(event);
        }
    }

    private void handleLogin(Event event) {
        // Ví dụ: Gửi event đến Kafka
        String payload = buildEventPayload(event);
        // kafkaProducer.send("keycloak-login-events", payload);
        log.debugf("Login event sent to message broker: %s", payload);
    }

    private void handleLoginError(Event event) {
        Map<String, String> details = event.getDetails();
        String username = details != null ? details.get("username") : "unknown";
        String error = event.getError();

        log.warnf("Login failure: user=%s, error=%s, ip=%s",
                username, error, event.getIpAddress());

        // Ví dụ: Increment metric counter cho monitoring
        // metricsService.incrementCounter("login_failures",
        //     "realm", event.getRealmId(),
        //     "error", error);
    }

    private void handleRegister(Event event) {
        log.infof("New user registered: userId=%s, realm=%s",
                event.getUserId(), event.getRealmId());
    }

    private void handleAdminDelete(AdminEvent event) {
        log.warnf("Admin DELETE operation: resource=%s/%s by admin=%s",
                event.getResourceType(),
                event.getResourcePath(),
                event.getAuthDetails().getUserId());
    }

    private String buildEventPayload(Event event) {
        // Tạo JSON payload cho message broker
        StringBuilder sb = new StringBuilder();
        sb.append("{");
        sb.append("\"type\":\"").append(event.getType()).append("\",");
        sb.append("\"realmId\":\"").append(event.getRealmId()).append("\",");
        sb.append("\"userId\":\"").append(event.getUserId()).append("\",");
        sb.append("\"clientId\":\"").append(event.getClientId()).append("\",");
        sb.append("\"ipAddress\":\"").append(event.getIpAddress()).append("\",");
        sb.append("\"time\":").append(event.getTime());
        sb.append("}");
        return sb.toString();
    }

    @Override
    public void close() {
        // Cleanup resources
    }
}

6.3 Implement EventListenerProviderFactory

// src/main/java/com/example/CustomEventListenerProviderFactory.java
package com.example;

import org.keycloak.Config;
import org.keycloak.events.EventListenerProvider;
import org.keycloak.events.EventListenerProviderFactory;
import org.keycloak.models.KeycloakSession;
import org.keycloak.models.KeycloakSessionFactory;

public class CustomEventListenerProviderFactory implements EventListenerProviderFactory {

    public static final String PROVIDER_ID = "custom-event-listener";

    @Override
    public EventListenerProvider create(KeycloakSession session) {
        return new CustomEventListenerProvider(session);
    }

    @Override
    public void init(Config.Scope config) {
        // Đọc cấu hình từ keycloak config
        // Ví dụ: String kafkaBrokers = config.get("kafka-brokers", "localhost:9092");
    }

    @Override
    public void postInit(KeycloakSessionFactory factory) {
        // Post-initialization
    }

    @Override
    public void close() {
        // Cleanup
    }

    @Override
    public String getId() {
        return PROVIDER_ID;
    }
}

6.4 Register SPI

# src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
com.example.CustomEventListenerProviderFactory

6.5 Deploy and Activate

# Build
mvn clean package

# Deploy
cp target/custom-event-listener-1.0.0.jar $KEYCLOAK_HOME/providers/
$KEYCLOAK_HOME/bin/kc.sh build

# Kích hoạt: Admin Console → Realm Settings → Events → Event listeners
# Thêm "custom-event-listener"

7. Keycloak JSON Logging

To integrate with centralized logging, configure Keycloak to output JSON logs:

# Bật JSON logging
bin/kc.sh start \
  --log=console \
  --log-console-output=json

# Hoặc qua environment variables
KC_LOG=console
KC_LOG_CONSOLE_OUTPUT=json

7.1 JSON Log Output sample

{
    "timestamp": "2026-03-15T10:30:45.123Z",
    "level": "INFO",
    "loggerClassName": "org.keycloak.events",
    "loggerName": "org.keycloak.events",
    "message": "type=LOGIN, realmId=my-realm, clientId=my-app, userId=abc-123, ipAddress=192.168.1.100",
    "threadName": "executor-thread-1",
    "threadId": 42,
    "hostName": "keycloak-0",
    "processName": "keycloak",
    "processId": 1
}

7.2 Configure Log Levels

# Cấu hình log levels cho events
bin/kc.sh start \
  --log=console \
  --log-console-output=json \
  --log-level=INFO \
  --log-level=org.keycloak.events:DEBUG

# Environment variables
KC_LOG_LEVEL=INFO
KC_LOG_LEVEL=org.keycloak.events:DEBUG

8. Integrating ELK Stack

Ship Keycloak logs to ELK Stack (Elasticsearch, Logstash, Kibana) for centralized analysis.

8.1 General architecture

Keycloak (JSON logs)
    ↓
Filebeat (log shipper)
    ↓
Logstash (processing & enrichment)
    ↓
Elasticsearch (storage & indexing)
    ↓
Kibana (visualization & dashboards)

8.2 Filebeat Configuration

# filebeat.yml
filebeat.inputs:
  - type: container
    paths:
      - /var/log/containers/keycloak-*.log
    processors:
      - decode_json_fields:
          fields: ["message"]
          target: "keycloak"
          overwrite_keys: true
      - add_fields:
          target: ""
          fields:
            service.name: keycloak
            environment: production

output.logstash:
  hosts: ["logstash:5044"]

8.3 Logstash Pipeline

# logstash/pipeline/keycloak.conf
input {
  beats {
    port => 5044
  }
}

filter {
  if [service][name] == "keycloak" {
    # Parse Keycloak event message
    if [keycloak][message] =~ "^type=" {
      kv {
        source => "[keycloak][message]"
        field_split => ", "
        value_split => "="
        target => "kc_event"
      }

      mutate {
        add_field => {
          "event_type" => "%{[kc_event][type]}"
          "realm" => "%{[kc_event][realmId]}"
          "client_id" => "%{[kc_event][clientId]}"
        }
      }
    }

    # GeoIP enrichment
    if [kc_event][ipAddress] {
      geoip {
        source => "[kc_event][ipAddress]"
        target => "geo"
      }
    }

    # Detect suspicious patterns
    if [kc_event][type] == "LOGIN_ERROR" {
      mutate {
        add_tag => ["login_failure"]
      }
    }
  }
}

output {
  if [service][name] == "keycloak" {
    elasticsearch {
      hosts => ["elasticsearch:9200"]
      index => "keycloak-events-%{+YYYY.MM.dd}"
    }
  }
}

8.4 Kibana Dashboard

Create Kibana dashboards to monitor:

  • Login Success/Failure Rate — Bar chart over time
  • Top Login Errors — Pie chart theo error type
  • Login by Geo Location — Map visualization
  • Failed Logins by IP — Brute-force detection table
  • User Registration Trend — Line chart by day
  • Admin Operations Audit — Data table with full details

9. Grafana Loki Integration

Grafana Loki is a lighter log aggregation solution than ELK, suitable for Kubernetes environments.

9.1 Promtail Configuration

# promtail-config.yml
server:
  http_listen_port: 9080

positions:
  filename: /tmp/positions.yaml

clients:
  - url: http://loki:3100/loki/api/v1/push

scrape_configs:
  - job_name: keycloak
    kubernetes_sd_configs:
      - role: pod
    relabel_configs:
      - source_labels: [__meta_kubernetes_pod_label_app]
        regex: keycloak
        action: keep
      - source_labels: [__meta_kubernetes_namespace]
        target_label: namespace
      - source_labels: [__meta_kubernetes_pod_name]
        target_label: pod
    pipeline_stages:
      - json:
          expressions:
            level: level
            logger: loggerName
            message: message
            timestamp: timestamp
      - labels:
          level:
          logger:
      - match:
          selector: '{app="keycloak"} |~ "type=LOGIN|type=REGISTER|type=LOGOUT"'
          stages:
            - regex:
                expression: 'type=(?P<event_type>\w+), realmId=(?P<realm>[\w-]+), clientId=(?P<client_id>[\w-]+), userId=(?P<user_id>[\w-]+)'
            - labels:
                event_type:
                realm:

9.2 Grafana Dashboard Queries

# Login failures trong 1 giờ qua
{app="keycloak"} |~ "type=LOGIN_ERROR" | json | count_over_time({app="keycloak"} |~ "LOGIN_ERROR" [1h])

# Login events theo realm
sum by (realm) (count_over_time({app="keycloak"} |~ "type=LOGIN" [5m]))

# Top IPs với login failures
{app="keycloak"} |~ "type=LOGIN_ERROR" | regexp `ipAddress=(?P<ip>[\d.]+)` | count by (ip) | sort desc | limit 10

10. SIEM Integration

Integrate Keycloak events with Security Information and Event Management (SIEM) systems.

10.1 Splunk Integration

# Cấu hình Filebeat ship đến Splunk HEC
output.logstash:
  enabled: false

output.http:
  enabled: true
  hosts: ["https://splunk-hec:8088"]
  path: "/services/collector/event"
  headers:
    Authorization: "Splunk <HEC_TOKEN>"
  format: json

10.2 SIEM Use Cases

Use CaseEvent PatternAction
Brute-force DetectionMultiple LOGIN_ERROR from same IPAlert + Block IP
Account TakeoverLogin from unusual GeoIPAlert + Require MFA
Privilege EscalationAdmin assign role adminAlert + Review
Data ExfiltrationMany unusual token requestsAlert + Revoke sessions
Suspicious RegistrationMultiple REGISTER from same IPAlert + CAPTCHA

11. Audit Compliance

11.1 SOC2 Requirements

SOC2 ControlKeycloak Implementation
CC6.1 — Logical access securityEvent logging cho LOGIN, LOGIN_ERROR, PASSWORD changes
CC6.2 — User authenticationMFA events, registration events
CC6.3 — Access authorizationAdmin Events cho role/permission changes
CC7.2 — Security monitoringReal-time alerting on login failures
CC8.1 — Change managementAdmin Events with representations

11.2 HIPAA Requirements

HIPAA ControlKeycloak Implementation
§164.312(b) — Audit controlsEnable all event types, admin events with representations
§164.312(d) — Person authenticationEvent logging cho authentication attempts
§164.308(a)(5) — Security awarenessEmail notifications cho suspicious activity

11.3 Retention Policy

# Cấu hình event retention
# SOC2: minimum 1 năm
# HIPAA: minimum 6 năm

# Trong Keycloak (built-in store)
# Realm Settings → Events → Expiration: 365 days

# Trong Elasticsearch (centralized logging)
# ILM Policy:
# - Hot: 30 days (SSD)
# - Warm: 335 days (HDD)
# - Cold/Frozen: 5+ years (S3/GCS)
# - Delete: 7 years

12. Alert Automation

12.1 Prometheus Alerting

Keycloak expose metrics via /metrics endpoint (needs metrics-enabled):

# Bật metrics
bin/kc.sh start --metrics-enabled=true
# prometheus-alerts.yml
groups:
  - name: keycloak-security
    rules:
      - alert: HighLoginFailureRate
        expr: |
          sum(rate(keycloak_login_error_total[5m])) by (realm)
          /
          sum(rate(keycloak_login_total[5m])) by (realm)
          > 0.3
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High login failure rate in realm {{ $labels.realm }}"
          description: "Login failure rate is {{ $value | humanizePercentage }} (threshold: 30%)"

      - alert: BruteForceDetected
        expr: |
          sum(increase(keycloak_login_error_total[5m])) by (realm) > 50
        for: 2m
        labels:
          severity: critical
        annotations:
          summary: "Possible brute-force attack on realm {{ $labels.realm }}"
          description: "{{ $value }} login failures in 5 minutes"

      - alert: UnusualRegistrationSpike
        expr: |
          sum(increase(keycloak_registrations_total[10m])) by (realm) > 100
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "Unusual registration spike in realm {{ $labels.realm }}"

12.2 Alertmanager Routing

# alertmanager.yml
route:
  receiver: default
  routes:
    - match:
        severity: critical
      receiver: pagerduty-security
      continue: true
    - match:
        severity: critical
      receiver: slack-security
    - match:
        severity: warning
      receiver: slack-ops

receivers:
  - name: default
    email_configs:
      - to: [email protected]

  - name: slack-security
    slack_configs:
      - api_url: https://hooks.slack.com/services/xxx
        channel: '#security-alerts'
        title: '{{ .GroupLabels.alertname }}'
        text: '{{ .CommonAnnotations.description }}'

  - name: pagerduty-security
    pagerduty_configs:
      - service_key: <pagerduty-integration-key>
        severity: critical

13. Best Practices

  • Turn on both Login Events and Admin Events — Don't miss any activity in the system.
  • Ship events to external system — Don't just rely on the built-in event store. Use ELK/Loki/SIEM for long-term storage.
  • Enable admin event representations — Save request/response body for admin operations for full auditing.
  • Set appropriate retention — Comply with compliance requirements (SOC2: 1 year, HIPAA: 6 years).
  • Monitor login failure rates — Set alerts for brute-force detection and account takeover.
  • Correlate events — Combine Keycloak events with application logs for a comprehensive picture.
  • Protect event logs — Log data contains PII, needs encryption at rest and in transit, access restricted.