1. Tổng quan Event System
Keycloak cung cấp hệ thống Event System toàn diện để theo dõi mọi hoạt động trong hệ thống. Mỗi hành động từ đăng nhập, đăng ký, đến thay đổi cấu hình admin đều được ghi lại thành events.
1.1 Hai loại Events
| Loại | Mô tả | Ví dụ |
|---|---|---|
| Login Events (User Events) | Các hành động liên quan đến người dùng | LOGIN, REGISTER, LOGOUT, TOKEN_EXCHANGE |
| Admin Events | Các thay đổi cấu hình qua Admin Console/API | CREATE user, UPDATE realm, DELETE client |
1.2 Login Event Types
| Event Type | Mô tả | Khi nào xảy ra |
|---|---|---|
LOGIN | Đăng nhập thành công | User nhập đúng credentials |
LOGIN_ERROR | Đăng nhập thất bại | Sai username/password |
REGISTER | Đăng ký tài khoản mới | User tạo account thành công |
REGISTER_ERROR | Đăng ký thất bại | Email trùng, validation fail |
LOGOUT | Đăng xuất | User logout hoặc session expired |
CODE_TO_TOKEN | Exchange authorization code → token | OIDC Authorization Code flow |
CODE_TO_TOKEN_ERROR | Token exchange thất bại | Invalid code, expired code |
REFRESH_TOKEN | Refresh access token | Client dùng refresh token |
REFRESH_TOKEN_ERROR | Refresh token thất bại | Token revoked hoặc expired |
CLIENT_LOGIN | Client authentication | Service account login |
INTROSPECT_TOKEN | Token introspection | Resource server verify token |
UPDATE_PASSWORD | Đổi mật khẩu | User thay đổi password |
RESET_PASSWORD | Reset mật khẩu | User reset qua email link |
VERIFY_EMAIL | Xác thực email | User click verification link |
SEND_RESET_PASSWORD | Gửi email reset password | Request forgot password |
UPDATE_PROFILE | Cập nhật hồ sơ | User cập nhật thông tin cá nhân |
REMOVE_TOTP | Xóa TOTP device | User gỡ OTP authenticator |
UPDATE_TOTP | Cấu hình TOTP | User thiết lập OTP |
GRANT_CONSENT | User cấp quyền consent | OAuth2 consent screen |
TOKEN_EXCHANGE | Token exchange | Exchange token giữa các clients |
1.3 Admin Event Operations
| Operation | Mô tả | Resource Types |
|---|---|---|
CREATE | Tạo mới resource | USER, CLIENT, REALM, GROUP, ROLE... |
UPDATE | Cập nhật resource | USER, CLIENT, REALM_SETTINGS... |
DELETE | Xóa resource | USER, CLIENT, SESSION... |
ACTION | Thực hiện hành động | RESET_PASSWORD, SEND_VERIFY_EMAIL... |
2. Bật Event Logging
2.1 Qua Admin Console
- Đăng nhập Admin Console
- Chọn Realm → Realm Settings → tab Events
- Cấu hình User events settings:
- Save events: ON
- Expiration: 30 days (tùy yêu cầu compliance)
- Saved types: Chọn event types cần lưu (mặc định: ALL)
- Cấu hình Admin events settings:
- Save events: ON
- Include representation: ON (lưu request/response body)
- Event listeners: Thêm listeners cần thiết
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 xử lý events khi chúng xảy ra. Keycloak có sẵn các listeners sau:
3.1 jboss-logging Listener
Ghi events vào Keycloak server log (mặc định đã bật):
# 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
Gửi email cho user khi có events quan trọng (ví dụ: login từ thiết bị mới):
# 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 và Event Store
4.1 Cấu trúc Login Event
{
"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 Cấu trúc Admin Event
{
"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 được lưu trong database của Keycloak:
| Table | Nội dung |
|---|---|
EVENT_ENTITY | Login events |
ADMIN_EVENT_ENTITY | Admin events |
Lưu ý: Event store mặc định lưu trong DB Keycloak. Với lượng lớn events, nên sử dụng custom Event Listener để ship events ra hệ thống bên ngoài và set expiration ngắn cho built-in store.
5. Event Filtering và Truy vấn
5.1 Qua Admin Console
- Vào Events → tab User events hoặc Admin events
- Filter events theo:
- Event type: LOGIN, LOGIN_ERROR, REGISTER...
- Client: Chọn client cụ thể
- 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 cho phép tạo custom Event Listener thông qua Service Provider Interface (SPI).
6.1 Tạo 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 Đăng ký SPI
# src/main/resources/META-INF/services/org.keycloak.events.EventListenerProviderFactory
com.example.CustomEventListenerProviderFactory
6.5 Deploy và Kích hoạt
# 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
Để tích hợp với centralized logging, cấu hình Keycloak 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 mẫu
{
"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 Cấu hình 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. Tích hợp ELK Stack
Ship Keycloak logs đến ELK Stack (Elasticsearch, Logstash, Kibana) để phân tích tập trung.
8.1 Kiến trúc tổng quan
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
Tạo Kibana dashboards để monitor:
- Login Success/Failure Rate — Bar chart theo thời gian
- Top Login Errors — Pie chart theo error type
- Login by Geo Location — Map visualization
- Failed Logins by IP — Table phát hiện brute-force
- User Registration Trend — Line chart theo ngày
- Admin Operations Audit — Data table với full details
9. Tích hợp Grafana Loki
Grafana Loki là giải pháp log aggregation nhẹ hơn ELK, phù hợp cho 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
Tích hợp Keycloak events với 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 Case | Event Pattern | Action |
|---|---|---|
| Brute-force Detection | Nhiều LOGIN_ERROR từ cùng IP | Alert + Block IP |
| Account Takeover | Login từ GeoIP bất thường | Alert + Require MFA |
| Privilege Escalation | Admin assign role admin | Alert + Review |
| Data Exfiltration | Nhiều token requests bất thường | Alert + Revoke sessions |
| Suspicious Registration | Nhiều REGISTER từ cùng IP | Alert + CAPTCHA |
11. Audit Compliance
11.1 SOC2 Requirements
| SOC2 Control | Keycloak Implementation |
|---|---|
| CC6.1 — Logical access security | Event logging cho LOGIN, LOGIN_ERROR, PASSWORD changes |
| CC6.2 — User authentication | MFA events, registration events |
| CC6.3 — Access authorization | Admin Events cho role/permission changes |
| CC7.2 — Security monitoring | Real-time alerting trên login failures |
| CC8.1 — Change management | Admin Events với representations |
11.2 HIPAA Requirements
| HIPAA Control | Keycloak Implementation |
|---|---|
| §164.312(b) — Audit controls | Bật tất cả event types, admin events với representations |
| §164.312(d) — Person authentication | Event logging cho authentication attempts |
| §164.308(a)(5) — Security awareness | Email 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 qua /metrics endpoint (cần bật 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
- Bật cả Login Events và Admin Events — Không bỏ sót bất kỳ hoạt động nào trong hệ thống.
- Ship events ra external system — Không chỉ dựa vào built-in event store. Sử dụng ELK/Loki/SIEM cho long-term storage.
- Bật admin event representations — Lưu request/response body cho admin operations để audit đầy đủ.
- Set appropriate retention — Tuân thủ compliance requirements (SOC2: 1 năm, HIPAA: 6 năm).
- Monitor login failure rates — Set alerts cho brute-force detection và account takeover.
- Correlate events — Kết hợp Keycloak events với application logs để có bức tranh toàn diện.
- Protect event logs — Log data chứa PII, cần mã hóa at rest và in transit, hạn chế access.