- Authors

- Name
- Youngju Kim
- @fjvbn20031
- はじめに
- Keycloak のイベントシステム
- EventListener SPI — イベントを外部へ送る
- メトリクスエンドポイント — Micrometer と /metrics
- Grafana ダッシュボードの構成
- OpenTelemetry Tracing
- 異常検知 — Brute Force と Credential Stuffing
- アラートルールの例(Prometheus Alertmanager)
- 監査コンプライアンス — ISMS-P / SOC 2 の観点
- 運用ベストプラクティスまとめ
- おわりに
- 参考資料
はじめに
認証システムは、障害が起きた瞬間にすべてのサービスの障害になります。ログインができなければ、ユーザーから見ればサービス全体が落ちているのと同じであり、さらに恐ろしいのは「静かなセキュリティインシデント」— credential stuffing が数日間進行しているのに誰も気づかない状況です。だからこそ、Keycloak 運用の成熟度は結局のところ可観測性(observability)の成熟度に収束します。
幸い、2026 年の Keycloak は可観測性の面で以前とは比較にならないほど良くなりました。26.x 系は micrometer ベースのメトリクス、ユーザーイベントメトリクス、OpenTelemetry tracing を組み込みで備え、26.6 では zero-downtime rolling update のような運用機能まで加わりました。本記事では、Keycloak のイベントシステムと保持ポリシー、EventListener SPI による外部送信、Prometheus + Grafana モニタリングスタックの構成、tracing とログ収集、brute force / credential stuffing の検知、アラートルール、そして ISMS-P / SOC 2 監査対応まで、可観測性の全体像を描きます。
Keycloak のイベントシステム
Login Events と Admin Events
Keycloak は二種類のイベントを発生させます。
| 区分 | Login Events (User Events) | Admin Events |
|---|---|---|
| 発生主体 | エンドユーザーの行為 | 管理者 / Admin API の行為 |
| 代表的イベント | LOGIN、LOGIN_ERROR、LOGOUT、REGISTER、UPDATE_PASSWORD、TOKEN_REFRESH | CREATE/UPDATE/DELETE(user、client、role、realm 設定) |
| 主な用途 | セキュリティモニタリング、ユーザー行動分析 | 変更監査(who changed what) |
| ペイロード | ユーザー、client、IP、結果、エラーコード | リソースパス、変更前後の representation |
イベント収集は realm 単位で有効化します。Admin Console の Realm Settings からも可能ですが、コードで管理しましょう。
# イベント設定 (kcadm.sh)
./kcadm.sh update events/config -r myrealm \
-s eventsEnabled=true \
-s 'eventsExpiration=2592000' \
-s 'enabledEventTypes=["LOGIN","LOGIN_ERROR","LOGOUT","REGISTER","UPDATE_PASSWORD","UPDATE_EMAIL","REMOVE_TOTP","UPDATE_TOTP","TOKEN_EXCHANGE","REFRESH_TOKEN_ERROR"]' \
-s adminEventsEnabled=true \
-s adminEventsDetailsEnabled=true
eventsExpirationは秒単位の保持期間です。上の例は 30 日。adminEventsDetailsEnabledを有効にすると、変更された representation(JSON)が一緒に保存され、「何がどう変わったか」まで追跡できます。監査要件があるなら必須です。- 26.x では admin events にも別途の有効期限設定が適用されるため、二種類の保持期間を両方とも明示してください。
参照は Admin API で可能です。
# 直近のログイン失敗イベントを取得
./kcadm.sh get events -r myrealm \
-q type=LOGIN_ERROR -q max=50
# 特定ユーザーの admin 変更履歴
./kcadm.sh get admin-events -r myrealm \
-q resourcePath=users/USER_UUID -q max=20
イベント保持と DB 負荷 — よくある運用事故
イベントは Keycloak のメイン DB に保存されます(EVENT_ENTITY、ADMIN_EVENT_ENTITY テーブル)。ここで二つの事故パターンが繰り返されます。
事故パターン 1: 無限保持
eventsExpiration 未設定 → テーブルが数億行に肥大化
→ イベント INSERT の遅延 → ログイントランザクション全体の遅延
→ 「ログインが遅い」チケットの殺到
事故パターン 2: 一括削除の爆弾
後から有効期限を設定 → 期限切れ処理が数億行の削除を試行
→ DB ロック競合、WAL/redo の急増 → 稼働中の DB が麻痺
実務ガイドラインは次の通りです。
- イベント保持は**運用参照用の短期(7〜30 日)**に抑え、長期保管は外部システム(下記 SPI の節)へエクスポートします。Keycloak DB は監査ログのアーカイブではありません。
- すでに肥大化したテーブルは有効期限設定に任せず、メンテナンス時間にバッチ削除(パーティション単位、LIMIT の繰り返し)で整理してから有効期限を設定します。
enabledEventTypesを空にするとすべてのタイプが保存されます。CODE_TO_TOKEN、TOKEN_REFRESH のような高頻度イベントが必要か検討し、必要なタイプだけを明示してください。トークン更新が頻繁な SPA が多い環境では、この差が数十倍の行数の差として現れます。
EventListener SPI — イベントを外部へ送る
DB ポーリングの代わりに、イベント発生時点で外部システム(Kafka、Loki、SIEM)へプッシュするのが EventListener SPI です。組み込み listener には jboss-logging(ログ出力)と email(ユーザー通知)があり、カスタム実装を JAR としてデプロイできます。
Kafka へ送信するカスタム Listener
public class KafkaEventListenerProvider implements EventListenerProvider {
private final KafkaProducer<String, String> producer;
private final String topic;
private final ObjectMapper mapper = new ObjectMapper();
public KafkaEventListenerProvider(KafkaProducer<String, String> producer, String topic) {
this.producer = producer;
this.topic = topic;
}
@Override
public void onEvent(Event event) {
// LOGIN_ERROR などのセキュリティイベントを非同期送信
try {
String payload = mapper.writeValueAsString(Map.of(
"category", "USER_EVENT",
"type", event.getType().name(),
"realmId", event.getRealmId(),
"clientId", event.getClientId(),
"userId", event.getUserId(),
"ipAddress", event.getIpAddress(),
"error", event.getError(),
"time", event.getTime(),
"details", event.getDetails()
));
producer.send(new ProducerRecord<>(topic, event.getRealmId(), payload));
} catch (Exception e) {
// 認証フローを絶対に壊さない — ログを残して続行
LoggerFactory.getLogger(getClass()).warn("event publish failed", e);
}
}
@Override
public void onEvent(AdminEvent adminEvent, boolean includeRepresentation) {
try {
String payload = mapper.writeValueAsString(Map.of(
"category", "ADMIN_EVENT",
"operation", adminEvent.getOperationType().name(),
"resourceType", String.valueOf(adminEvent.getResourceTypeAsString()),
"resourcePath", adminEvent.getResourcePath(),
"realmId", adminEvent.getRealmId(),
"authUserId", adminEvent.getAuthDetails().getUserId(),
"time", adminEvent.getTime()
));
producer.send(new ProducerRecord<>(topic, adminEvent.getRealmId(), payload));
} catch (Exception e) {
LoggerFactory.getLogger(getClass()).warn("admin event publish failed", e);
}
}
@Override
public void close() {
}
}
Factory と登録ファイルも必要です。
public class KafkaEventListenerProviderFactory implements EventListenerProviderFactory {
private KafkaProducer<String, String> producer;
private String topic;
@Override
public EventListenerProvider create(KeycloakSession session) {
return new KafkaEventListenerProvider(producer, topic);
}
@Override
public void init(Config.Scope config) {
Properties props = new Properties();
props.put("bootstrap.servers", config.get("bootstrapServers", "kafka:9092"));
props.put("key.serializer", StringSerializer.class.getName());
props.put("value.serializer", StringSerializer.class.getName());
props.put("acks", "1");
props.put("linger.ms", "20");
this.producer = new KafkaProducer<>(props);
this.topic = config.get("topic", "keycloak-events");
}
@Override
public String getId() {
return "kafka-event-listener";
}
@Override
public void close() {
if (producer != null) producer.close();
}
}
META-INF/services/org.keycloak.events.EventListenerProviderFactory
ファイル内容: com.example.keycloak.KafkaEventListenerProviderFactory
JAR を providers ディレクトリに置き、ビルド後、realm の event listener に登録します。
cp keycloak-kafka-listener.jar /opt/keycloak/providers/
/opt/keycloak/bin/kc.sh build
./kcadm.sh update events/config -r myrealm \
-s 'eventsListeners=["jboss-logging","kafka-event-listener"]'
実装には鉄則が一つあります。listener の失敗が認証を失敗させてはいけません。 onEvent は認証トランザクションの経路で呼び出されるため、外部送信は非同期・バッファリングで行い、例外は飲み込む必要があります。Kafka が落ちたから全社のログインが止まる、という設計は最悪です。
Loki へ送る軽量な代替案
カスタム JAR なしで軽く始めるなら、jboss-logging listener が残すイベントログを構造化 JSON で出力し、Promtail/Alloy が Loki へ収集する構成が実用的です。
# JSON ログ出力 + イベントログレベルの調整
bin/kc.sh start \
--log-console-output=json \
--log-level=INFO,org.keycloak.events:DEBUG
# promtail 設定の抜粋 — Keycloak イベントだけをラベリング
scrape_configs:
- job_name: keycloak
static_configs:
- targets: [localhost]
labels:
job: keycloak
__path__: /var/log/keycloak/*.log
pipeline_stages:
- json:
expressions:
logger: loggerName
message: message
- match:
selector: '{job="keycloak"} |= "org.keycloak.events"'
stages:
- labels:
logger:
Kafka 経路は SIEM・リアルタイム検知パイプラインに、Loki 経路は運用参照と中期保管に適しています。両者は排他的ではなく、併用する組織が多いです。
メトリクスエンドポイント — Micrometer と /metrics
Keycloak は micrometer ベースのメトリクスを管理ポート(デフォルト 9000)の /metrics で公開します。
bin/kc.sh start \
--metrics-enabled=true \
--event-metrics-user-enabled=true \
--event-metrics-user-tags=realm,clientId,idp \
--http-metrics-histograms-enabled=true
metrics-enabled:JVM、DB コネクションプール(Agroal)、HTTP、Infinispan キャッシュのメトリクスを公開します。event-metrics-user-enabled:26.x のユーザーイベントメトリクスです。ログイン成功・失敗のようなイベントがカウンターとして集計され、DB のイベントテーブルを叩かずにリアルタイム統計が得られます。event-metrics-user-tags:メトリクスに付くタグを制御します。clientId、idp タグはカーディナリティを増やすため、client 数が多い環境では慎重に選択してください。
Prometheus のスクレイプ設定例です。
# prometheus.yml 抜粋
scrape_configs:
- job_name: keycloak
metrics_path: /metrics
static_configs:
- targets: ['keycloak-0.mgmt:9000', 'keycloak-1.mgmt:9000']
scheme: https
tls_config:
insecure_skip_verify: false
見るべき核心メトリクス
| 領域 | メトリクス(例) | 意味とアラート基準 |
|---|---|---|
| ログイン | keycloak_user_events_total(event タグ login、login_error) | 失敗率の急増 = 攻撃または障害 |
| トークン | keycloak_user_events_total(event タグ refresh_token、code_to_token) | 発行率の急変 = クライアント異常 |
| HTTP | http_server_requests_seconds(uri、status タグ) | p99 レイテンシ、5xx 比率 |
| DB プール | agroal_available_count、agroal_blocking_time_average | プール枯渇 = 全面障害の前兆 |
| キャッシュ | vendor_statistics 系(Infinispan sessions、realms キャッシュ) | ヒット率の低下、クラスタのリバランシング |
| JVM | jvm_memory_used_bytes、jvm_gc_pause_seconds | GC pause とログイン遅延の相関 |
特に **DB コネクションプール(agroal)**は Keycloak 障害の最前線の指標です。プール待機時間が伸び始めると数分以内にログインタイムアウトへ波及することが多いため、available_count の枯渇と blocking_time の上昇にアラートを設定しておきましょう。
Grafana ダッシュボードの構成
ダッシュボードは「セキュリティ運用」と「システム健全性」の二枚に分けることを推奨します。
+----------------------------------------------------------------+
| Keycloak Security Operations |
+------------------------+---------------------------------------+
| ログイン成功/失敗 (rate) | 失敗率 % (成功+失敗 比) |
| realm/client 別スタック | しきい値ライン表示 (5%, 20%) |
+------------------------+---------------------------------------+
| LOGIN_ERROR 理由の分布 | IP 別失敗 Top 10 (Loki クエリ) |
| invalid_user_credentials| user_not_found 急増 = enumeration |
+------------------------+---------------------------------------+
| 新規登録/パスワード変更 | admin イベントのタイムライン |
+------------------------+---------------------------------------+
+----------------------------------------------------------------+
| Keycloak System Health |
+------------------------+---------------------------------------+
| HTTP p50/p95/p99 | 5xx 比率 |
+------------------------+---------------------------------------+
| Agroal プール使用率/待機 | Infinispan キャッシュヒット率/エントリ数 |
+------------------------+---------------------------------------+
| JVM ヒープ/GC pause | インスタンス別 CPU/スレッド |
+----------------------------------------------------------------+
よく使う PromQL をいくつか記しておきます。
# 5 分ウィンドウのログイン失敗率 (%)
100 *
sum(rate(keycloak_user_events_total{event="login_error"}[5m]))
/
sum(rate(keycloak_user_events_total{event=~"login|login_error"}[5m]))
# トークンエンドポイントの p99 レイテンシ
histogram_quantile(0.99,
sum by (le) (rate(http_server_requests_seconds_bucket{uri=~".*token.*"}[5m])))
# DB プール待機平均 (ms)
avg(agroal_blocking_time_average_milliseconds)
OpenTelemetry Tracing
「ログインが遅いが、どこが遅いのか分からない」への答えが分散トレーシングです。Keycloak 26.x は OpenTelemetry tracing を組み込みでサポートします。
bin/kc.sh start \
--tracing-enabled=true \
--tracing-endpoint=http://otel-collector:4317 \
--tracing-protocol=grpc \
--tracing-sampler-type=traceidratio \
--tracing-sampler-ratio=0.05 \
--tracing-service-name=keycloak-prod
- トレースには HTTP リクエスト、DB クエリ、LDAP 呼び出しなどの span が含まれ、「ログイン 2.5 秒のうち LDAP bind が 2.1 秒」を一目で把握できます。
- プロダクションではサンプリング比率を低く(1〜5%)始めてください。parentbased サンプラーを使えば、ゲートウェイから続く trace context を尊重します。
- Collector での tail-based sampling により「遅いリクエストとエラーだけを全量保存」する構成は、認証システムと特に相性が良いです。
- ログに trace_id が出力されるよう JSON ログと組み合わせれば、Grafana でメトリクス → トレース → ログとドリルダウンする三位一体が完成します。
異常検知 — Brute Force と Credential Stuffing
Keycloak 組み込みの Brute Force Detection
realm 単位で有効化する組み込みの防御機能です。
./kcadm.sh update realms/myrealm \
-s bruteForceProtected=true \
-s failureFactor=5 \
-s waitIncrementSeconds=60 \
-s maxFailureWaitSeconds=900 \
-s maxDeltaTimeSeconds=43200 \
-s permanentLockout=false
失敗回数がしきい値を超えると、アカウントを漸増的にロックします。ただし、これだけでは不十分です。組み込み機能はアカウント単位の防御だからです。
Credential Stuffing はパターンが違う
| 攻撃 | パターン | 組み込み防御の効果 | 追加対応 |
|---|---|---|---|
| Brute force | 一つのアカウントに多数のパスワード | 有効(アカウントロック) | 組み込み機能 + アラート |
| Credential stuffing | 多数のアカウントに各 1〜2 回試行 | ほぼ無力 | IP/ASN 単位の分析、WAF、ボット検知 |
| Password spraying | 多数のアカウントに同じパスワード | ほぼ無力 | 試行パスワードのパターン分析、MFA |
credential stuffing はアカウントあたりの失敗が 1〜2 回のため、アカウントロックにかかりません。検知の手がかりはグローバルなパターンです。
- LOGIN_ERROR 全体比率の急増(特に user_not_found と invalid_user_credentials の同時上昇)
- 単一 IP・レンジから多数の異なる username の試行(Loki で ipAddress 基準の集計)
- 普段と異なる地域・ASN、異常な User-Agent の分布
- 深夜時間帯の均一なリクエスト間隔(ボットのシグネチャ)
イベントを Kafka に流していれば、ストリーム処理(Flink/ksqlDB)で「IP ごとのユニーク username 数が N 分で M 個超過」のようなルールをリアルタイム評価できます。根本対応は結局、MFA/passkeys の拡大(26.6 のログインフォーム passkeys 統合が導入障壁を大きく下げました)と、漏洩パスワードのブロックポリシーです。
アラートルールの例(Prometheus Alertmanager)
groups:
- name: keycloak-security
rules:
- alert: KeycloakLoginFailureRateHigh
expr: |
100 *
sum(rate(keycloak_user_events_total{event="login_error"}[5m]))
/
sum(rate(keycloak_user_events_total{event=~"login|login_error"}[5m]))
> 20
for: 10m
labels:
severity: warning
team: identity
annotations:
summary: "ログイン失敗率が 20% 超過(10 分継続)"
description: "攻撃または認証経路の障害の可能性。セキュリティダッシュボードを確認してください。"
- alert: KeycloakLoginErrorSpike
expr: |
sum(rate(keycloak_user_events_total{event="login_error"}[5m]))
> 4 * sum(rate(keycloak_user_events_total{event="login_error"}[5m] offset 1d))
for: 15m
labels:
severity: critical
annotations:
summary: "ログイン失敗が前日同時刻比 4 倍に急増"
- name: keycloak-health
rules:
- alert: KeycloakDbPoolExhausted
expr: min(agroal_available_count) == 0
for: 2m
labels:
severity: critical
annotations:
summary: "DB コネクションプール枯渇 — ログイン全面障害が目前"
- alert: KeycloakTokenLatencyHigh
expr: |
histogram_quantile(0.99,
sum by (le) (rate(http_server_requests_seconds_bucket{uri=~".*token.*"}[5m])))
> 1
for: 10m
labels:
severity: warning
annotations:
summary: "トークンエンドポイントの p99 が 1 秒超過"
- alert: KeycloakInstanceDown
expr: up{job="keycloak"} == 0
for: 1m
labels:
severity: critical
annotations:
summary: "Keycloak インスタンスダウン"
アラート設計の原則は「セキュリティアラートと可用性アラートの受信者を分離する」ことです。失敗率の急増はセキュリティチームのチャンネルへ、プール枯渇はプラットフォームのオンコールへ届くべきであり、すべてのアラートにはダッシュボード・ランブックのリンクを添付します。
監査コンプライアンス — ISMS-P / SOC 2 の観点
可観測性スタックはそのまま監査対応の資産になります。審査で繰り返し要求される項目とマッピングすると次の通りです。
| 監査要求(ISMS-P / SOC 2 共通) | Keycloak での対応 |
|---|---|
| 認証成功・失敗記録の保持 | login events + 外部長期保管(SIEM/オブジェクトストレージ) |
| 管理者行為の追跡 | admin events(details 含む)+ 変更前後の representation |
| ログの改ざん防止 | 外部送信後に不変ストレージ(WORM、バケットロック) |
| 保持期間ポリシー | 内部短期 + 外部 1 年以上(規制により異なる)の文書化 |
| アクセス権限の定期レビュー | Admin API による role/group メンバーシップの定期抽出レポート |
| 異常アクセスのモニタリング | 失敗率・地域・IP ベースのアラートルール + 対応ランブック |
| 時刻同期 | NTP — イベントタイムスタンプの信頼性の前提 |
実務上のヒントをいくつか付け加えます。
- 監査人が要求するのは「ログがある」ことではなく、**「ログを見て行動した証跡」**です。アラート発生 → 確認 → 対処の記録が残るワークフロー(チケット連携)を作っておきましょう。
- admin events の representation には機微情報が含まれ得ます。外部送信パイプラインでマスキングポリシーを定義し、個人情報の保持期間規定とログ保持期間が衝突しないか法務との確認が必要です。
- realm 設定・ポリシーの変更自体を IaC(Terraform、keycloak-config-cli)で管理すれば、「変更管理」要件のかなりの部分が Git 履歴で立証できます。admin events はコンソール経由の非正規経路の変更を捕捉する補助手段になります。
運用ベストプラクティスまとめ
- イベントは有効化しつつ、DB 保持は短く — 長期保管は SPI・ログパイプラインで外部化。
- listener の実装は認証経路を絶対に塞がないよう、非同期 + 例外の隔離。
- metrics-enabled + event-metrics-user-enabled をデフォルトの起動オプションに。
- ダッシュボードはセキュリティ運用とシステム健全性を分離し、agroal プール指標へのアラートは必須。
- tracing は低サンプリングで始め、tail-based sampling へ高度化。
- アカウント単位の brute force 防御と、グローバルパターンに基づく stuffing 検知を分けて設計。
- アラートにはランブックのリンク、セキュリティ・可用性の受信者分離、定期的なアラート疲労レビュー。
- 監査対応は「収集」ではなく「対応の証跡」まで — ワークフローと不変保管を備えること。
おわりに
Keycloak の可観測性は、「イベント(何が起きたか)、メトリクス(今どんな状態か)、トレース(なぜ遅いか)、ログ(詳細な文脈)」という四本柱が互いを補完する構造で設計したときに完成します。26.x に入ってメトリクスと tracing が組み込まれたおかげで参入障壁は大きく下がり、残るのは組織の運用規律 — 保持ポリシー、アラート設計、対応ランブック — です。
認証システムのモニタリングは、単なるインフラ運用を超えて、セキュリティ検知とコンプライアンスの交差点にあります。本記事の構成要素を一つずつ積み上げれば、「ログイン障害をユーザーより先に知る」、そして「攻撃を攻撃者が成功する前に知る」運用体制に到達できるはずです。
参考資料
- Keycloak Server Administration Guide — Auditing and Events
- Keycloak Guides — Gaining insights with metrics
- Keycloak Guides — Monitoring user activities with event metrics
- Keycloak Guides — Root cause analysis with tracing
- Keycloak Server Developer Guide — Event Listener SPI
- Keycloak 26.6.0 Release Notes
- Prometheus Documentation — Alerting Rules
- Grafana Loki Documentation
- OpenTelemetry Documentation
- Micrometer Documentation
- OWASP — Credential Stuffing Prevention Cheat Sheet
- AICPA — SOC 2 Trust Services Criteria