コンテンツにスキップ

ADR 0007: MFA の責任分界(Entra 連携社員 / Keycloak ローカルアカウント)と v1 のスコープ確定

  • Status: Proposed
  • Date: 2026-10-14(Issue #121・Epic #4 で 2026-10-13 に2ラウンド行った議論の結論をまとめたもの。2026-10-14 レビュー反映: PR #123 と本 Decision の関係の訂正、ADR 0003 §5 の解消範囲の訂正、未検証のまま断定していた箇所に PoC 前提を復元、ロールアウト順序の明記、日本語表記の修正)

gcu realm には約 18,000 名のユーザーがおり、大きく3種類に分かれる: Entra 連携(entra IdP 経由のブローカーログイン)で入ってくる関電工社員、Keycloak のローカルパスワードでログインする協力会社の従業員、みまもり・しらべ・ポートフォリオから移行したローカルパスワードユーザー。ほかに少数のローカル管理者アカウント(Console の緊急管理者=break-glass、platform-admin を持つ)と、master realm で Keycloak 管理コンソールにログインする管理者がいる。

現状、ローカルパスワードアカウントには第二因子が一切ない。Keycloak 側に MFA 設定はなく、移行で旧システムの OTP は全て失われ、ユーザーが MFA を登録する入口もない。ログインページは全員に対してパスワードフォームを表示しており、パスワードが漏れればそのままアカウントを乗っ取られる——緊急管理者や Keycloak 管理者も例外ではない。

社員側は本来 Entra が MFA を担うはずだったが、この前提は既に崩れている:

  • ログインページには社員を Entra 経由に強制する仕組みがない。
  • 移行時に全新規アカウントへパスワード設定メールが送られ、社内ドメインも除外されていない。
  • 「パスワードを忘れた」から社員はいつでも自分でローカルパスワードを設定できる。

結果として社員は Entra とその MFA を迂回できる。退職して Entra 側が無効化されても、残存するローカルパスワードでログインできてしまう。

回復・運用面も空白で、協力会社の従業員が端末を紛失しても回復手段がなく、Console からはユーザーの MFA 状態が見えず、リセットもできない。MFA の変更はユーザー向け通知も Console 側の監査も存在しない。

さらに device-reputation には一度も有効化されたことのない reviewAction=otp という選択肢があり、動作も設計意図とずれている: これは単にユーザーに Keycloak の TOTP を登録させるだけで、その後のログインでは一度も再挑戦させない。「社員の MFA は Entra が担う」という分担と矛盾しており、旧ドキュメントはこれをデフォルト値として記載していた。

  • 社員(Entra 連携ログイン): MFA の登録・検証・リカバリは全て Entra が担う。Keycloak は Entra 経由のユーザーに対して一切の因子を設定しない。社員はローカルパスワードでのログインを許可されない(緊急管理者を除く)——ただし、この強制は §2 で定義する「Entra 連携済み」ロールの有無を根拠にする。ロールは実際に一度でも Entra 経由でログインしたか、§7 のツールで補完されたユーザーにしか付かない。したがって ADR 0003 §5 の未決事項「連携関連付けはローカル資格情報が存在しないことを証明しない」は、本 ADR で完全に解消されるわけではない——強制の対象は「Entra 連携済みと確認できたユーザー」であって「社員であるユーザー」ではなく、一度も Entra でログインしたことがない社員のローカルパスワードは、§7 のツールで検出・補完されるまで有効なまま残りうる。ADR 0003 §5 には、この限定付きで「方針は ADR 0007 で決定済み」と注記する(解消ではなく決定)。
  • ローカルアカウント: gcu realm でパスワードログインする全アカウント(協力会社、3つの旧システムからの移行ユーザー、緊急管理者)に加え、master realm の管理者が対象。因子は TOTP + 回復コードとし、強制する。
  • device-reputation の CONFIGURE_TOTP をプラットフォーム全体の MFA 機構に拡張することはしない。reviewAction=otp オプション自体は削除するが、既存設定に残っている otp 値は起動エラーにはせず flag(fail-open、何もしない)として扱う——この provider の「採点できなければログインを止めない」という既定方針に合わせる。
  • Console では Optional / Required / Adaptive のようなポリシー切り替え UI は提供しない。ポリシーは Keycloak の認証フロー設定そのものであり、runbook に基づく手動運用とする——本番 realm の設定は従来から手動運用である。これは Epic #4 の DoD が当初掲げていた「管理者が Console から Optional/Required/Adaptive を切り替えられる」という項目を v1 では満たさないことを意味する。 Adaptive 相当は Risk Engine(#13)側の step-up に委ね、Epic #4 の DoD は本 ADR の決定で更新されたものとして扱う。

2. Keycloak 認証フロー(gcu realm)——設計はここで決定、実装・実機検証は別タスク

Section titled “2. Keycloak 認証フロー(gcu realm)——設計はここで決定、実装・実機検証は別タスク”

この節の内容はまだ実装されていない。 PR #123(gcu-browser-local-otp)は本節の設計とは異なる暫定形(§8「PR #123 との関係」参照)であり、§2 の設計そのものを実装・PoC 検証するのは別の sub-issue の仕事になる。

  • browser flow のパスワード分岐は次の順になる: ユーザー名パスワードフォーム → 条件判定(ユーザーが「Entra 連携済み」ロールを持つ場合はアクセスを拒否し、Microsoft ログインへの案内を表示)→ 2FA サブフロー(TOTP と回復コードを alternative として並べる)。
  • 2FA サブフローのモードは Stage 2(§6 ロールアウト順序を参照)までは conditional(設定済みユーザーのみ挑戦)を維持し、Stage 2 で required に切り替える。この切り替えがパスワード分岐にのみ影響し Entra ボタン経由のログインを通らないことは、実装時の PoC で検証する(PR #123 が検証したのは別の形——OTP Form を forms 直下に無条件 required で置く形——であり、この条件付き→必須の切り替えそのものはまだ検証されていない)。
  • 資格情報リセットフロー(パスワード忘れ)にも同じ拒否条件を追加する。第二因子ステップで TOTP と回復コードの両方を受理できるか(Keycloak 26.7.5 の既定リセットフローの第二因子ステップが TOTP のみを前提にしている可能性がある)は、PoC で確認し、必要なら回復コードの受理を追加実装する。
  • 「TOTP の設定」「回復コードの設定」の両方を必須アクションとして有効化する。両者が 2FA サブフロー内で alternative として並ぶ場合、ユーザーはどちらか一方しか設定を求められない可能性があるため、「TOTP 登録の直後に必ず回復コードも生成される」ことを担保する具体的な仕組み(別の必須アクションとして直列に並べる、等)は PoC の結果に基づいて決定する。
  • entra IdP に Hardcoded Role mapper(同期モード FORCE)を追加し、Entra 経由でログインした全ユーザーに出所ベースの realm ロール(例: 「Entra 連携済み」)を付与する。「社員」ロールと呼ばないのは、Entra 側に B2B ゲストが含まれ得るため。

3. Keycloak 認証フロー(master realm、Stage 1)

Section titled “3. Keycloak 認証フロー(master realm、Stage 1)”
  • master realm のユーザーは全員管理者なので、browser flow の 2FA サブフローは直接 required にする。
  • ロックアウト対策として回復コードをオフライン保管し、最後の手段として Keycloak の bootstrap-admin(service モード)を ECS 上のワンショットタスクとして用意する。
  • 既存ユーザー移行ツールは master 管理者のパスワード(admin-cli password grant)で Admin API を呼んでいるため、master の MFA 有効化前に §7 のレポート・ロール補完ツールを実行する必要がある(実行順序は §6 を参照)。以後の再移行では一時的な service-account client(client_credentials)に切り替える。
  • gcu / master 両 realm で audit-log リスナーおよび user/admin events を有効化し、CloudWatch に出力する(本番は runbook に基づく手動操作)。実装・検証は issue #144。
  • 2026-10-14、issue #144 の PoC で実機確認済み(Keycloak 26.7.5、javap/cfr でのデコンパイルも併用): 当初の想定は誤りだった。 UPDATE_TOTP/REMOVE_TOTP は 26.x でも非推奨になっておらず、TOTP の登録時には UPDATE_TOTP と UPDATE_CREDENTIAL(details.credential_type = "otp")の両方が発火する。回復コードの登録には対応する legacy イベントが存在せず UPDATE_CREDENTIAL(credential_type = "recovery-authn-codes")のみ。管理者によるクレデンシャル削除は user event ではなく admin event(operationType=ACTION, resourceType=USER, resourcePath=users/{id}/credentials/{credId})として記録される。runbook の CloudWatch クエリと email リスナーの除外リストは、この実機確認結果(UPDATE_CREDENTIAL/REMOVE_CREDENTIAL だけでなく UPDATE_TOTP/REMOVE_TOTP も対象に含める)に基づいて書く。
  • ユーザー向け通知は Keycloak 標準の email イベントリスナーを使い、資格情報変更のみを対象として LOGIN_ERROR は除外する(ログイン失敗を悪用したメール連打を防ぐ)。同じく issue #144 で確認済み: Keycloak 標準 email リスナーの既定通知対象は LOGIN_ERROR・UPDATE_PASSWORD・UPDATE_TOTP・REMOVE_TOTP・UPDATE_CREDENTIAL・REMOVE_CREDENTIAL の6種類(SUPPORTED_EVENTS、upstream 非公開の実装詳細)。LOGIN_ERROR だけ除外すると TOTP 変更のたびに二重通知になるため、UPDATE_TOTP/REMOVE_TOTP も合わせて除外し、UPDATE_CREDENTIAL/REMOVE_CREDENTIAL/UPDATE_PASSWORD だけを有効のまま残す。 また EmailEventListenerProvider は admin event を一切処理しない(no-op)ため、管理者によるリセット(Console の MFA リセット、§5)の通知は構造的にこのリスナーでは実現できず、引き続き #117 待ちとする。
  • v1 では Console に Keycloak イベント閲覧 UI は作らない。runbook に CloudWatch Logs Insights クエリを用意する。
  • 読み取り: ユーザーの MFA 状態(資格情報の種別と作成日時のみ)を返す。秘密情報は一切返さない。
  • 書き込み: 対象ユーザーの TOTP・回復コード資格情報を全て削除し、全セッションを無効化する。パスワードや有効/無効状態は変更しない。
  • 権限は新設の users:reset-mfa とし、ADR 0005 の方針に従い初版は PLATFORM_ADMIN のみに付与する。読み取りは既存の users:read で足りる(書き込みだけを狭く絞る、既存の users:read/users:update-state と同じ分割)。
  • 書き込みゲートは既存の操作(CSRF、セッション、ライブロール再確認、5分以内の auth_time、有効な緊急管理者の存在、Idempotency-Key、理由必須)を踏襲する。TargetPolicy も同様(自分自身・サービスアカウント・保護対象・緊急管理者は 409)。管理者アカウントのリセットは Keycloak 管理コンソール側で行う。
  • 本人確認の方法は必須入力の reason に記録する運用上の約束であり、専用フィールドは追加しない(#121 の「IT の平台管理者が登録済みの連絡先に折り返して本人確認する」に対応)。協力会社の従業員については当該組織に登記された連絡先への折り返し、組織に属さない移行ユーザーについては対応システムの業務責任者経由で確認し、その確認方法を reason に書く。

6. ロールアウト順序(本番、権限を持つ運用者が手動実行)

Section titled “6. ロールアウト順序(本番、権限を持つ運用者が手動実行)”
  1. §2 の設計の PoC 検証(開発環境、本 ADR の開発側の交付物)。
  2. §7 のレポート・ロール補完ツールを実行する。master realm の MFA 化(ステップ3)より前に実行しなければならない(§3 参照)。
  3. Stage 1: gcu の緊急管理者などローカル管理者に TOTP + 回復コードの必須アクションを個別に追加し、master realm の 2FA を required にする。回復コードはオフライン保管。
  4. 事前に社員へ周知した上で、社員のローカルパスワードログイン拒否(§1・§2 の role 条件)を有効化する。
  5. Console の MFA リセット(§5)を本番適用する。これは Stage 2 の前提。
  6. Stage 2 pilot: 協力会社1社と旧3システムの一部ユーザーに登録案内メールを送る(認証フローは変えない)。
  7. 公告日に切り替え: gcu の 2FA サブフローを required にする(Stage 2)。

7. 社員ローカルパスワードの検出・ロール補完ツール

Section titled “7. 社員ローカルパスワードの検出・ロール補完ツール”
  • 既存のユーザー移行ツールのサブコマンドとして実装する。レポート(読み取り専用)は社内ドメインかつパスワード資格情報を持ち entra 連携のないアカウントを列挙する。ロール補完(書き込み)は entra 連携済みの全アカウントに「Entra 連携済み」ロールを補完する。デフォルトは dry-run。両方とも緊急管理者 ID を除外する。master realm の MFA 有効化前に本番で実行する必要がある(§6 参照)。

PR #123(gcu-browser-local-otp、develop にマージ済み)は、本 ADR の §2 を実装したものではない。実際に検証したのは「Keycloak 26.7.5 の browser flow で forms サブフローと IdP リダイレクト/ブローカー経路が相互排他な alternative である」という構造的性質(これは §2 の設計でも前提にしている)であり、それ以外の点——2FA サブフローを conditional のまま残すか、回復コードを alternative に含めるか、role 条件による拒否を入れるか、開発環境の既定フローを変えずにオプトインのスクリプトにするか——は PR #123 と §2 で結論が異なる(PR #123 は OTP Form を forms 直下に無条件 required で置き、回復コードや role 条件はなく、開発環境の既定 browser flow 自体を書き換える)。§2 の設計を実装する際は、PR #123 の flow を土台に改修するのではなく、検証済みの構造的性質だけを引き継ぎ、conditional サブフロー・回復コード・role 条件・オプトイン化を新たに組み込む前提で見積もる。

後続の PR #132(issue #127)が PR #123 の flow の上に role 条件(Entra 連携済みロールを持つユーザーを拒否)と entra IdP の Hardcoded Role mapper、および reset-credentials flow への同条件の追加を実装した。これは §1・§2 で決めた「社員のローカルパスワードログイン拒否」そのものに相当するが、§2 のその他の決定(2FA サブフローを Stage 2 まで conditional に保つこと、回復コードを alternative に含めること、開発環境の既定フローを変えずオプトインにすること)はまだ実装されていない——PR #132 も PR #123 の無条件 required な土台の上に乗っている。

  • 社員自身の MFA(Entra が担う)および本番 Entra 側の証跡確認(#113)。
  • Passkey / WebAuthn(v2、Epic #2 のパスキー主ログイン化と合わせて設計)。
  • Email OTP、SMS OTP、自建認証器アプリ(#105)。
  • Step-up、LoA / acr 検証、Adaptive MFA(#13)、Console 書き込み再認証の強度見直し。
  • MyPage での MFA 自己登録・管理(MyPage 第2フェーズ、#64)。
  • 協力会社代表者による自社員工の MFA リセット(#8 の組織スコープ権限待ち)。
  • Console での Keycloak イベント閲覧・統合監査(#6)。
  • リセット後の自動通知メール(#117 待ち)。
  • Console での MFA ポリシー切り替え UI。
  • STG・本番への自動変更。本番設定は全て権限を持つ運用者が runbook に基づき手動で行う。
  • Keycloak のブラウザフロー構造(forms サブフローと IdP リダイレクト/ブローカー経路が相互排他な alternative である性質)に依存する設計になる。将来 Keycloak がこの構造を変更した場合は再検証が必要——ただし 26.7.5 で実機確認済み。
  • ログイン時点で強制登録するため、パスワードを窃取した攻撃者が本人より先に TOTP を登録するリスクがある。pilot・事前周知・本人への変更通知メールで低減する。
  • master realm の MFA 化は既存ユーザー移行ツールの認証方式(password grant)と衝突するため、運用上の実行順序(レポート・ロール補完 → master Stage 1、§6)を厳守する必要がある。
  • Console の MFA リセットは新しい高権限操作であり、ADR 0005 のパターン(最小権限から開始)を踏襲するが、users:reset-mfa という新しい攻撃対象を追加する。
  • §2 の設計は次の未検証の前提に依存している。実装 PoC で確認し、崩れていた場合は設計を見直す: 本番 gcu realm の browser flow が組み込みの既定フロー(conditional OTP サブフロー込み)のままであること、direct access grants を有効化した client が存在しないこと(存在すれば OTP 必須化でその client のパスワードログインが壊れる)、resetPasswordAllowed が有効であること、audit-log リスナーが既に有効化されているかどうか(リポジトリ内に記録がない)。
  • §1 の強制は「Entra 連携済みロールを持つユーザー」を根拠にしており「社員であること」を根拠にしていない。一度も Entra でログインしていない社員のローカルパスワードは、§7 のツールで検出・補完されるまで有効なまま残る——これは解消した脆弱性ではなく、ツールの実行順序(§6)に依存する運用上の前提である。
  • device-reputation の CONFIGURE_TOTP をプラットフォーム全体の MFA に拡張する: 却下。実行後に一度も再挑戦しない設計であり、「社員の MFA は Entra」という分担とも矛盾する。該当オプションは削除する。
  • 社員の OTP 挑戦を止めるために、Condition や専用 Authenticator で IdP 経由のログインを除外する: 不要と判断。Keycloak 26.7.5 の browser flow は forms(ローカル)と IdP リダイレクト/ブローカー経路がもともと相互排他な alternative であり、forms 側に OTP 要件を置くだけで同じ効果が得られる(PR #123 で実機検証済み)。ただしこれは「社員に OTP を挑戦させない」ことの代替案であり、「社員のローカルパスワードログインを拒否する」こととは別の問題——後者は引き続き §2 の Condition - User Role(Entra 連携済みロール)が必要で、§2 で決定している。
  • Console で MFA ポリシーを Optional/Required/Adaptive と切り替えられる UI を作る: 却下。ポリシーは realm 全体の認証フロー設定そのものであり、Console からの中途半端な抽象化は実態とずれるリスクがある。runbook による手動運用に統一する。Epic #4 の DoD にあった同項目は、本 ADR の決定により v1 のスコープから外れる。
  • Epic #4(MFA。本 ADR により DoD の一部——MFA ポリシーの Console 切り替え UI——が v1 スコープ外になる)、#2(基礎認証)、#6(監査ログ)、#8(Organizations)、#13(Risk Engine)、#64(MyPage)、#105(認証器アプリ)、#113(Entra 証跡)、#117(Console 通知メール)、#121(本 ADR のもとになった User Story)
  • ADR 0002、0003、0005、0006
  • PR #123(gcu-browser-local-otp。§2 の設計とは異なる暫定形。関係の詳細は §8)、PR #132(issue #127。§1・§2 の「社員のローカルパスワード拒否」部分を実装。§8 参照)