セキュリティ調査¶
「何かおかしい」を「具体的に何が」まで絞り込むのが2本のツールです。
get_login_failures_by_ip が候補の IP を見つけ、get_ip_activity がその1つの
全体像を返します。見つけたものに対して動くツールがさらに4本あります。
候補 IP から全体像へ¶
get_login_failures_by_ip(date_from, date_to, top=20) は期間内のログイン
失敗数で送信元 IP を順位付けします。これは出発点であって結論ではありません。
単一 IP からの失敗数が多いのは、共有 NAT の出口や社内プロキシが平常の1日に
生む姿そのものですし、逆に住宅プロキシに分散したスプレー攻撃は、1 IP あたりの
失敗数が少ないまま成功することがあります。失敗数だけでは過剰検知にも
見逃しにもなります。
get_ip_activity(ip_address, event_types="LOGIN,LOGIN_ERROR", date_from, date_to, max_timeline=200)
が、1つの IP について実際に問いに答えます。単一ページを対象にクライアント側で
絞り込む get_events フィルタと違い(直近の結果の外にある活動を見逃しうる)、
一致する全イベントを絞り込む前に完全にページングするので、要約は指定した期間に
対して網羅的であり、サンプルではありません。構造化データを返します。
summary: 総イベント数、成功/失敗数、ユニークユーザー数、ユニーククライアント数、 最初/最後の検出時刻users: ユーザー別の成功/失敗内訳と個別の KeyCloak エラーコード (invalid_user_credentials・user_temporarily_disabled等)clients: クライアント(SP)別の成功/失敗内訳timeline:max_timelineで打ち切られた時系列イベント一覧
数値の読み方: スプレーか、個人の入力ミスか¶
見分ける材料は生の失敗数ではなく複数ユーザーにまたがる成功率です。
- ある IP から多数のユーザー・低い成功率(目安: ユニークユーザー10人以上・
成功率20%未満)という形は、パスワードスプレーやクレデンシャルスタッフィングの
典型です。その IP の内訳で
success > 0のユーザーは突破されたアカウントで あり、ここで調査を止めて対応に移ります。 - 1人のユーザー・多数の失敗・最終的に成功 — 特に住宅回線やモバイル回線から、 数十秒〜数分の人間的な間隔で、ID の別バリエーション(個人メールアドレス・ タイプミスしたユーザー名)を試している場合 — は普通のパスワード失念です。 インシデントではありません。
- レンズを既知の共有出口レンジ(社内 VPN・リモートデスクトップ環境)まで 広げると、「多数ユーザー・単一 IP」が疑わしい姿から日常的な姿に変わることが あります。逆に、1つのホスティング/VPS 事業者に属する多数の IP に分散した 低い成功率も、単一のノイジーな IP と同じだけの注意に値します。それが この視点から見た分散スプレーの姿だからです。
get_realm_security_defenses は KeyCloak 自身のブルートフォース検知が有効か、
その閾値はどうかを示します。今見ている事象を KeyCloak の防御が既に捕まえて
いるかどうかを判断する材料になります。get_brute_force_status は特定の
ユーザーが現在ロックされているかを確認します。
対応: 4本の書き込みツール¶
get_ip_activity が突破されたアカウントを特定したら、封じ込めを行う3本の
ツールを、順序を守って実行します。
set_user_enabled(username, enabled=False)は今後のログインをすべて ブロックします。変更するのはenabledフラグだけで、カスタム属性には 触れません。既存のセッションは終了しません。既に発行されたトークンは 自然に失効するまで有効なままです。アクティブなセッションがあるユーザーを 無効化すると、応答にはセッション数が示され、次にlogout_userを実行する よう案内されます。知っている前提ではなく、次の手順をツール自身が伝えます。logout_user(username)はそのユーザーのアクティブなセッションを全て 削除し、どこからでも再認証を強制します。削除したセッション数を報告し、 セッションが無いユーザーに対する呼び出しはエラーではなく「何もしなかった」 と報告します。reset_password(username, password, temporary=False)で新しい パスワードを設定します。temporary=Trueは次回ログイン時の変更を強制する もので、自分が使うためではなくユーザーへ渡すリセットに向いています。
reset_passwords_batch(csv_text, temporary=False) は1行あたり
username,password(CSV)で一括リセットします。パスワード欄が空なら生成されます。
複数アカウントが同時に突破されたスプレー事案向けで、日常的な用途ではありません。
この4本はいずれもライブスモークテスト(scripts/smoke_test.py)の対象外です。
実アカウントを変更するため、定期実行されるテストの場ではないからです。
スキップされ続けることはユニットテストが強制しています。
ログインループ¶
detect_login_loops(date_from, date_to, threshold=10, window_seconds=60, top=20)
は window_seconds 以内に threshold 回を超えてログインしたユーザーをフラグ
します。これは攻撃ではなく、KeyCloak とサービスプロバイダー間のリダイレクトが
壊れている兆候です。LOGIN イベントのみを走査するので、上記の失敗ベースの
スプレー検出とは別のレンズになります。
Deadline と部分結果¶
get_ip_activity・get_login_failures_by_ip・detect_login_loops・
get_totp_users はいずれも大量のデータを引く可能性があります。広い期間指定や
大規模なレルムでは多数ページのイベントになりえます。2つの独立した上限がこれを
防ぎます。
KEYCLOAK_DEADLINE(既定45秒)が1呼び出しあたりの実行時間を制限KEYCLOAK_MAX_EVENTS(既定200,000)が1呼び出しでの取得件数を制限
打ち切りをどう明示するかはツールの返り値の型によって異なります。
- テキストを返すツール(
get_login_failures_by_ip・detect_login_loops・get_login_statsなど)は出力の先頭に⚠️ PARTIAL RESULT — stopped early (window too wide / realm too large): the data below is INCOMPLETE.を付けます。 get_ip_activityは整形済みテキストではなく構造化 JSON を返すため、同じ 状態を bool フィールドevents_cappedで伝えます。trueのときはsummary・users・clients・timelineすべてが不完全です。これはtruncatedとは別の懸念です。truncatedはtimelineリストがmax_timelineで打ち切られたことだけを意味し、summary/users/clients(一致した全件から計算)はそのまま正確です。
どちらの場合も、返ってきたデータ自体は本物ですが網羅的ではありません。
打ち切られた結果ではなく完全な結果が欲しければ date_from を狭めてください。
部分結果を完全な結果と見分けがつかない形で黙って返すツールは、調査の場では
無いよりも悪いものです。「それ以上は何も起きなかった」ように見えて、実際は
「そこで見るのをやめた」だけだからです。