コンテンツにスキップ

セキュリティ調査

「何かおかしい」を「具体的に何が」まで絞り込むのが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_credentialsuser_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本の ツールを、順序を守って実行します。

  1. set_user_enabled(username, enabled=False) は今後のログインをすべて ブロックします。変更するのは enabled フラグだけで、カスタム属性には 触れません。既存のセッションは終了しません。既に発行されたトークンは 自然に失効するまで有効なままです。アクティブなセッションがあるユーザーを 無効化すると、応答にはセッション数が示され、次に logout_user を実行する よう案内されます。知っている前提ではなく、次の手順をツール自身が伝えます。
  2. logout_user(username) はそのユーザーのアクティブなセッションを全て 削除し、どこからでも再認証を強制します。削除したセッション数を報告し、 セッションが無いユーザーに対する呼び出しはエラーではなく「何もしなかった」 と報告します。
  3. 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_activityget_login_failures_by_ipdetect_login_loopsget_totp_users はいずれも大量のデータを引く可能性があります。広い期間指定や 大規模なレルムでは多数ページのイベントになりえます。2つの独立した上限がこれを 防ぎます。

  • KEYCLOAK_DEADLINE(既定45秒)が1呼び出しあたりの実行時間を制限
  • KEYCLOAK_MAX_EVENTS(既定200,000)が1呼び出しでの取得件数を制限

打ち切りをどう明示するかはツールの返り値の型によって異なります。

  • テキストを返すツール(get_login_failures_by_ipdetect_login_loopsget_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 のときは summaryusersclientstimeline すべてが不完全です。これは truncated とは別の懸念です。truncatedtimeline リストが max_timeline で打ち切られたことだけを意味し、summary/users/clients (一致した全件から計算)はそのまま正確です。

どちらの場合も、返ってきたデータ自体は本物ですが網羅的ではありません。 打ち切られた結果ではなく完全な結果が欲しければ date_from を狭めてください。 部分結果を完全な結果と見分けがつかない形で黙って返すツールは、調査の場では 無いよりも悪いものです。「それ以上は何も起きなかった」ように見えて、実際は 「そこで見るのをやめた」だけだからです。