收集 Keycloak 日志
本文档介绍了如何配置 Keycloak 以使用 Webhook 将日志推送到 Google Security Operations。
Keycloak 是一款开源身份和访问权限管理 (IAM) 解决方案,提供单点登录 (SSO)、用户联合身份验证、身份代理和社交登录功能。它支持 OpenID Connect、OAuth 2.0 和 SAML 2.0 协议,并跟踪用户事件(登录、退出、注册、密码更改)和管理员事件(用户、客户端、Realm 和角色管理操作),以进行安全审核。
准备工作
请确保您满足以下前提条件:
- Google SecOps 实例
- 正在运行的 Keycloak 实例(建议使用 20 或更高版本)
- 对 Keycloak 管理控制台的管理员访问权限
- 对 Keycloak 服务器文件系统或容器的访问权限,用于部署扩展程序
- 对 Google Cloud Console 的访问权限(用于创建 API 密钥)
在 Google SecOps 中创建 Webhook Feed
创建 Feed
- 依次前往 SIEM 设置 > Feed。
- 点击添加新 Feed。
- 在下一页上,点击配置单个 Feed。
- 在 Feed 名称字段中,输入 Feed 的名称(例如
Keycloak Events)。 - 选择 Webhook 作为来源类型。
- 选择 Keycloak 作为日志类型。
- 点击下一步。
- 为以下输入参数指定值:
- 定界符(可选):输入
\n以拆分多行事件(每个网络钩子 POST 都包含一个事件,因此可以留空)。 - 资产命名空间:资产命名空间
- 注入标签:要应用于此 Feed 中事件的标签
- 定界符(可选):输入
- 点击下一步。
- 在最终确定界面中查看新的 Feed 配置,然后点击提交。
生成并保存密钥
创建 Feed 后,您必须生成用于身份验证的密钥:
- 在 Feed 详情页面上,点击生成密钥。
- 系统会显示一个对话框,其中包含密钥。
- 复制并妥善保存此密钥。
重要提示:密钥只会显示一次,之后无法再检索。如果丢失,您必须生成新的密钥。
获取 Feed 端点网址
- 前往相应 Feed 的详细信息标签页。
- 在端点信息部分,复制 Feed 端点网址。
网址格式为:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate或
https://<REGION>-malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate保存此网址以供后续步骤使用。
点击完成。
创建 Google Cloud API 密钥
Chronicle 需要 API 密钥才能进行身份验证。在 Google Cloud Console 中创建受限 API 密钥。
创建 API 密钥
- 前往 Google Cloud 控制台的“凭据”页面。
- 选择您的项目(与您的 Chronicle 实例关联的项目)。
- 依次点击创建凭据> API 密钥。
- 系统会创建一个 API 密钥,并在对话框中显示该密钥。
- 点击修改 API 密钥以限制密钥。
限制 API 密钥
- 在 API 密钥设置页面中:
- 名称:输入一个描述性名称(例如
Chronicle Webhook API Key)
- 名称:输入一个描述性名称(例如
- 在 API 限制下:
- 选择限制密钥。
- 在选择 API 下拉菜单中,搜索并选择 Google SecOps API(或 Chronicle API)。
- 点击保存。
- 从页面顶部的 API 密钥字段复制 API 密钥值。
- 安全地保存 API 密钥。
在 Keycloak 中启用事件存储
在配置 webhook 扩展程序之前,请在 Keycloak 中启用事件存储,以便生成事件并可供转发。
启用用户事件
- 登录 Keycloak 管理控制台。
- 从左上角的下拉菜单中选择要监控的大区。
- 前往 Realm Settings > Events。
- 选择用户事件设置子标签页。
- 启用保存活动切换开关。
- 设置到期期限(建议至少 7 天)。
- 点击保存。
启用管理员事件
- 在同一事件标签页中,选择管理事件设置子标签页。
- 启用保存活动切换开关。
- 启用包含表示形式切换开关,以捕获已更改对象的完整详细信息。
- 设置到期期限(建议至少 7 天)。
- 点击保存。
安装 Webhook 事件监听器扩展程序
Keycloak 不包含原生网络钩子事件监听器。安装第二阶段 (p2-inc) 的 keycloak-events 扩展程序,以启用 webhook 传送。
下载并部署扩展程序
从 Maven Central 上的 keycloak-events 发布页面下载最新的发布 JAR,或从源代码构建:
git clone https://github.com/p2-inc/keycloak-events.git cd keycloak-events mvn clean install将生成的胖 JAR 文件复制到 Keycloak
providers目录中:cp target/keycloak-events-*.jar /opt/keycloak/providers/重新构建并重启 Keycloak:
/opt/keycloak/bin/kc.sh build /opt/keycloak/bin/kc.sh start
启用 webhook 事件监听器
- 登录 Keycloak 管理控制台。
- 从 realm 下拉菜单中选择目标 realm。
- 前往 Realm Settings > Events。
- 在事件监听器下拉菜单中,选择 ext-event-webhook。
- 点击保存。
配置 Keycloak 网络钩子
构建网络钩子网址
将 Chronicle 端点网址和 API 密钥组合在一起:
<ENDPOINT_URL>?key=<API_KEY>示例:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...
通过 Keycloak REST API 创建 Webhook 订阅
keycloak-events 扩展程序提供用于管理 Webhook 订阅的 REST 端点。使用 Keycloak Admin REST API 创建 Webhook。
第 1 步:获取访问令牌
使用管理员账号从 Keycloak 请求访问令牌:
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')
替换以下内容:
<KEYCLOAK_HOST>:您的 Keycloak 服务器主机名和端口(例如keycloak.example.com:8443)<ADMIN_USERNAME>:您的 Keycloak 管理员用户名<ADMIN_PASSWORD>:您的 Keycloak 管理员密码
第 2 步:创建 Webhook
发送 POST 请求,为目标 realm 创建 webhook 订阅:
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": ["*"] }'
替换以下内容:
<KEYCLOAK_HOST>:您的 Keycloak 服务器主机名<REALM_NAME>:要监控的大区的名称(例如master或my-realm)<ENDPOINT_URL>:之前复制的 Chronicle Feed 端点网址<API_KEY>:之前创建的 Google Cloud API 密钥<SECRET_KEY>:之前生成的 Chronicle Webhook 密钥<WEBHOOK_HMAC_SECRET>:用于对 Webhook 载荷进行 HMAC 签名的任意密文(例如,mySecretKey123)
第 3 步:验证 Webhook
通过列出相应 realm 的所有 webhook 来确认 webhook 是否已创建:
curl -sS -X GET "https://<KEYCLOAK_HOST>/realms/<REALM_NAME>/webhooks" \ -H "Authorization: Bearer ${TOKEN}" \ -H "Accept: application/json"
响应会返回一个 webhook 对象列表。验证您的 Webhook 是否显示 "enabled": "true" 和正确的网址。
网络钩子事件类型
eventTypes 字段接受一个表达式数组,用于过滤要发送的事件:
*- 发送所有事件(建议用于 SIEM 集成)access.*- 发送所有访问事件admin.*- 发送所有管理员事件admin.USER-*- 发送与用户相关的所有管理员事件admin-USER-CREATE- 仅发送用户创建管理员事件
网络钩子载荷格式
Webhook 以包含 JSON 载荷的 HTTP POST 请求的形式发送事件。用户事件载荷示例:
{ "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 重试行为
当收到非 2xx 响应时,扩展程序会使用自动指数退避算法进行重试:
| 参数 | 默认值 | 说明 |
|---|---|---|
| backoffInitialInterval | 500 毫秒 | 初始重试间隔 |
| backoffMaxElapsedTime | 900000 毫秒(15 分钟) | 重试总时长上限 |
| backoffMaxInterval | 180000 毫秒(3 分钟) | 重试之间的最大间隔 |
| backoffMultiplier | 5 | 每次重试间隔的乘数 |
| backoffRandomizationFactor | 0.5 | 抖动的随机化因素 |
身份验证方法参考
Chronicle webhook Feed 支持多种身份验证方法。选择供应商支持的方法。
方法 1:自定义标头(推荐)
如果您的供应商支持自定义 HTTP 标头,请使用此方法以提高安全性。
请求格式:
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" }
优点:
- API 密钥和密文在网址中不可见
- 更安全(标头不会记录在 Web 服务器访问日志中)
- 供应商支持时的首选方法
方法 2:查询参数
如果您的供应商不支持自定义标头,请将凭据附加到网址。
网址格式:
<ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY>示例:
https://malachiteingestion-pa.googleapis.com/v2/unstructuredlogentries:batchCreate?key=AIzaSyD...&secret=abcd1234...请求格式:
POST <ENDPOINT_URL>?key=<API_KEY>&secret=<SECRET_KEY> HTTP/1.1 Content-Type: application/json { "event": "data", "timestamp": "2025-01-15T10:30:00Z" }
缺点:
- 网址中显示的凭据
- 可能会记录在 Web 服务器访问日志中
- 安全性不如标头
方法 3:混合(网址 + 标头)
某些配置在网址中使用 API 密钥,在标头中使用密钥。
请求格式:
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" }
身份验证标头名称
Chronicle 接受以下身份验证标头名称:
对于 API 密钥:
x-goog-chronicle-auth(推荐)X-Goog-Chronicle-Auth(不区分大小写)
对于密钥:
x-chronicle-auth(推荐)X-Chronicle-Auth(不区分大小写)
Webhook 限制和最佳实践
请求限制
| 限制 | 值 |
|---|---|
| 最大请求大小 | 4 MB |
| 最大 QPS(每秒查询次数) | 15000 |
| 请求超时 | 30 秒 |
| 重试行为 | 自动(使用指数退避算法) |
UDM 映射表
| 日志字段 | UDM 映射 | 逻辑 |
|---|---|---|
| payload.client_id | additional.fields | 与根据 payload.client_id、payload.realm_id 创建的字段合并 |
| payload.realm_id | additional.fields | |
| source_timestamp | metadata.event_timestamp | 使用日期过滤条件(采用 ISO8601 和 yyyy-MM-dd'T'HH:mm:ss.SSSZ 格式)进行解析 |
| payload.ip_address | metadata.event_type | 如果 payload.ip_address 不为空,则设置为“STATUS_UPDATE”;否则,如果 uuid 不为空,则设置为“USER_UNCATEGORIZED”;否则,设置为“GENERIC_EVENT” |
| uuid | metadata.event_type | |
| payload.type | metadata.product_event_type | 直接复制值 |
| payload.session_id | network.session_id | 直接复制值 |
| payload.ip_address | principal.ip | 直接复制值 |
| source_metadata.schema | principal.resource.attribute.labels | 与根据 source_metadata.schema、source_metadata.table、source_metadata.is_deleted(转换为字符串)、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 | 直接复制值 |
| 对象 | security_result.detection_fields | 与根据对象、read_method、payload.id 创建的标签合并 |
| read_method | security_result.detection_fields | |
| payload.id | security_result.detection_fields | |
| redirect_uri | target.url | 直接复制值 |
| 用户名 | target.user.userid | 直接复制值 |
| metadata.product_name | metadata.product_name | 设置为“KEYCLOAK” |
| metadata.vendor_name | metadata.vendor_name | 设置为“KEYCLOAK” |
username" from "details_json |
target.user.userid |
从变更日志映射 |
redirect_uri" from "details_json |
target.url |
从变更日志映射 |
realm_id" and "client_id |
additional.fields |
从变更日志映射 |