Security Architecture
認証だけに頼らず、API・権限・RLS・非公開schema・DB関数を重ねる多層防御を学びます。
台帳、振込、KYC、権限、監査、照合をMinibankの実装と結び付ける。
先に学ぶ内容 — Prerequisite: 基礎 / 銀行業務学ぶこと — LEARNING GOALS
このページで学ぶこと
- AuthenticationとAuthorizationの違い
- RLSとSECURITY DEFINER
- Default Denyと権限最小化
体験05 · SECURITY(多層防御)
ログインだけで、何が見えるか確かめよう
まずは予想をひと言。本文を読んだ後、自分に見える範囲と、記録される操作を確認します。
- 01まず予想する — PREDICT
まずここだけ:思ったことをひと言書く
ログインさえできれば、他の利用者の口座情報も見えると思いますか?
正解でなくて大丈夫です。「〜だと思う」から始めてみましょう。
ヒント1を見る
「本人であること」と「その操作をしてよいこと」は、同じ確認でしょうか?
ヒント2を見る
画面にボタンが無いことと、そのボタンの処理を実行できないことは、同じでしょうか?
もっと詳しく:この問いで使う専門用語
- Authentication(認証)は、あなたが誰かを確認することです。
- Authorization(認可)は、あなたがその操作をしてよいかを確認することです。
- RLS(Row Level Security、行単位の閲覧制限)は、他人のデータを見せない規則です。
- Audit(監査記録)は、誰が、いつ、何をしたかを残す記録です。
日常語で理解する — PLAIN LANGUAGE
まず、ここだけつかむ
セキュリティは1つの鍵では守れません。本人確認、操作権限、行単位の閲覧制限、関数の実行権限、DB制約を重ねます。
認証は『誰か』の確認、認可は『その人がこの操作をしてよいか』の確認です。ログイン済みでも他人の口座は操作できません。
限られた処理にだけ、一時的に強い権限を貸し出す仕組みがあります。入口・実行者・検索先・公開範囲のどこかを誤ると、意図しない権限の拡大につながります。この考え方を「SECURITY DEFINER」と呼びます。
順番に理解する — DIFFICULT PARTS, STEP BY STEP
難しいポイントを、順番にほどく
専門用語を覚えることより、「なぜ必要か」「このシステムのどこで使うか」を理解することが目標です。
認証と認可
認証は本人確認、認可は対象と操作の許可確認です。
ログイン済みというだけで管理操作や他人のデータ操作を許す事故を防ぎます。
Authがuidを確定し、RLSやRPCが所有者・role・active状態を別途確認します。
RLS
同じtableの中でも、利用者ごとに見える・変更できる行をDBが絞る仕組みです。
APIで検索条件を付け忘れても、他人の行をDBから返さない最後の境界になります。
auth.uid()とcustomerの対応を使い、本人の顧客・口座・取引だけを許可します。
SECURITY DEFINER
呼び出した人ではなく、関数所有者の権限で決められた処理を行うDB関数です。
private tableを直接公開せず金融処理を実行できますが、認可不足なら強権限の代理実行になります。
公開wrapperの中でcaller identityとroleを確認し、private schemaの処理へ接続します。
search_pathとdefault deny
search_pathは名前だけでtable/functionを探す順番、default denyは明示許可しない限り拒否する方針です。
攻撃者が同名objectを先に見つけさせたり、PUBLICの初期権限から関数を実行したりする危険を減らします。
definer関数のsearch_pathを固定し、PUBLIC EXECUTEをrevokeして必要roleだけgrantします。
安全なerror境界
内部の詳しいerrorと、利用者へ見せるmessageを分けます。
table名、constraint、SQL断片などを攻撃の手掛かりとして漏らさないためです。
RPCはSQLSTATEを返し、mapRpcError()が許可済みの日本語へ変換し、未知codeは一般errorへ畳みます。
発展 — ADVANCEDもっと詳しく確認する最初の学習では読まなくても問題ありません。仕組みをもっと深く理解したい場合に開いてください。
用語から実装へ
| 用語 | なぜ必要か | Minibank |
|---|---|---|
| Authentication | 誰かを確認する | Supabase AuthのgetUser()とauth.uid() |
| Authorization | 何をしてよいか決める | RLS、admin role、RPC内の状態検査 |
| RLS | 行単位で他人のデータを隠す | customersとbank_accountsのown-row policy |
| SECURITY DEFINER | 限定処理だけを所有者権限で実行 | public wrapperからprivate schemaへ委譲 |
| PUBLIC EXECUTE | 関数の暗黙公開を防ぐ | REVOKE後に必要roleへだけGRANT |
| Default Deny | 追加許可がない限り拒否 | private tableはzero policy・zero privilege |
| Schema separation | APIから内部オブジェクトを隔離 | ledger/core/admin/audit/reconciliation |
| Privileged operation | 高権限操作を狭い入口へ集約 | active operator確認付きadmin RPC |
直接RPCを呼ばれても守る
UIのボタン非表示は認可ではありません。顧客RPCは `auth.uid()` から呼出者を再解決し、管理RPCは `admin.admin_operators` のroleとstatusをDB内で確認します。`search_path = ''` とschema-qualified参照は、同名オブジェクトを悪用した解決先のすり替えを抑えます。
OTP・振込先も、tableを直接公開しない
transfer_requests、transfer_otp_challenges、transfer_beneficiariesはRLSを有効にしたうえで、anon・authenticated・service_roleのtable権限をすべてrevokeしています。顧客はSECURITY DEFINER RPCだけを通り、各関数がauth.uid()とownerを確認します。
OTP平文はDBへ保存せず、開発環境のdemo表示だけに限定します。production API responseには含めません。お気に入り登録では銀行・支店・口座名義をclient入力にせず、DBが実口座から解決します。
実バグ:内部DBエラーを利用者へ見せない
生のmessageにはtable名、constraint名、SQL断片、内部状態が含まれる可能性があります。server内部の診断情報と、利用者へ伝える安全なmessageは分けます。Minibankではraw messageをprivate transfer_requests.failure_reasonへ診断用として残し、customer-facing responseにはSQLSTATEをmappingした文言だけを返します。
- 01execute_transfer()内部でPostgreSQL error
- 02旧実装はmessage_textをfailure_reasonとしてRPC結果へ格納
- 03APIがその文字列をclientへ返し得た
- 04Migrationで返却列をfailure_code(SQLSTATE)へ変更
- 05TransferServiceがmapRpcError()でallowlist済み日本語へ変換
- 06未知codeは一般的なINTERNAL_ERRORへ畳む
実際のMinibankでは
- anon・authenticated・service_roleのtable/function権限をmigrationで明示
- service_roleのcustomers/bank_accounts直接DMLをrevoke
- adminとcustomerのactive actor兼務をcustomer INSERT trigger・admin作成RPC・ACTOR_COLLISION検査で防止
- OTP request/challengeとお気に入りtableへ直接Data API権限を与えない
- SQLSTATEをAPIの一般化された応答へmappingし、口座列挙やDB詳細漏えいを抑制
- privilege regressionが公開関数ACL、table privilege、RLS、公開schemaを確認
体験学習の続き · LEARNING LOOP
ここまで読めましたか?続けて確認してみましょう
認証と認可の違いを読んだところで、実際の画面と監査記録を見比べ、自分の言葉で説明してみます。
- 02実際に観察する — OBSERVE
同じ仕組みを、動く画面から観察する
まず顧客画面を開いて、自分に見える範囲を観察し、この教材へ戻ります。リンクは新しいタブで開きます。ログインが必要です。
アカウントがない場合は新規登録が必要です。本人確認・口座開設には管理者承認が必要です。承認前や操作できない場合も、この教材内の図・説明・Source Trail(教材の参照元)で代替学習し、次へ進めます。
ログイン・新規登録の入口を開く ↗操作できない場合:教材の参照元で確認する ↓もっと詳しく:管理者の画面で観察する
管理者ログインが必要です。権限がない場合は、次の「証拠を確認する」の教材による確認で進められます。
- 03証拠を確認する — EVIDENCE
何を見たか、1つずつ確かめる
Evidence(判断の根拠となる記録)を確認したらチェックします。各項目の「何を見るか」を手がかりにしてください。
画面を開けない項目は、Source Trail(教材の参照元)と教材の説明で確認できます。失敗を実際に起こす必要はありません。
- 04コードの処理経路を追う — CODE TRAIL
コードを書く必要はありません
ここでは「説明されている仕組みが、実際のMini Bankのどこに実装されているか」を確認します。まずは日本語の役割説明を読み、入力した依頼がどこへ渡るかをつかみましょう。コードの全文を読む必要はありません。
基本の順序は、画面 → API Route(依頼を受ける窓口)→ TransferService(処理をつなぐサービス)→ DB / RPC(記録の保存場所と、その処理の呼び出し)です。まず最初の2つを見て、続くサービスは「もっと詳しく」で確認します。
教材の参照元を確認する — Source Trail ↓API Route(依頼を受ける窓口)で、Bearer tokenから本人を確認する処理。まず「誰か」を確かめる箇所を探します。
src/lib/auth/getAuthContext.tsMigration(DB定義の変更履歴)。他人の行を見せないための規則(RLS)の定義を確認します。
supabase/migrations/20260819132835_customers_rls_and_privileges.sql - 05自分の言葉で説明する — EXPLAIN
LEVEL 1 — やさしい説明(まずここだけ)
ログインできることと、ある操作をしてよいことは、なぜ別の確認が必要なのでしょうか? 短い2文を目安に説明してください。
日常語だけで完了できます。専門用語の数や正誤を自動採点するものではありません。
ヒント1を見る
「認証」と「認可」という2つの言葉を使って書いてみましょう。
ヒント2を見る
もし画面のボタンを消すだけで安全になるなら、何が困るか考えてみましょう。
もっと詳しく:LEVEL 2 — 専門用語を使った説明(任意)
Authentication、Authorization、RLS、SECURITY DEFINERの4つの役割を結び付けて説明してみましょう。
挑戦する場合は、上の説明欄に追記できます。追加しなくても完了できます。
説明の確認ポイント — Checkpoint- 認証と認可が別の確認であると説明できる
- 画面の制限とDB側の制限が別の防御線であると説明できる
理解を確かめる — CHECK YOUR UNDERSTANDING
4問で理解チェック
答えを頭の中で考えてから、各項目を開いて確認してください。
Q1ログイン済みなら認可も完了していますか?
いいえ。誰かは分かっても、その口座や管理操作への権限があるとは限りません。
Q2RLSがあればSECURITY DEFINER内の認可確認は不要ですか?
不要ではありません。definer関数は強い所有者権限でRLSを迂回し得るため、関数自身の確認が重要です。
Q3UIで管理ボタンを隠すことは認可ですか?
違います。操作性の制御であり、API/RPCを直接呼ぶ攻撃はDB側の認可で止めます。
Q4DBのmessage_textをそのまま画面へ出してよいですか?
いけません。安定したcodeを安全な公開messageへ変換し、内部詳細は権限を限定した診断情報として扱います。
完全に暗記する必要はありません。大切な考え方を説明できそうなら押してください。
教材の参照元 — SOURCE TRAIL
根拠と関連教材
CURRENT IMPLEMENTATIONは確認したrepository内の実装です。HISTORICAL IMPLEMENTATIONは導入・修正の履歴で、後続migrationによる再定義があり得ます。RELATED LESSONは教材への案内で、実装の一次Sourceではありません。ASSUMPTIONは仮定、UNKNOWNは未確認を表します。
確認基点: d02eae8914f39cbf06bfe422dc39c71886f82b3e / 2026-09-11。稼働環境への適用確認: UNKNOWN。パス実在の自動検査と、記述した技術的事実の確認は別です。
- 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260819132835_customers_rls_and_privileges.sql顧客RLSとcolumn privilege - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823090001_revoke_service_role_direct_dml_on_customers_and_bank_accounts.sqlservice_role直接DMLの縮小 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823160001_enforce_actor_separation.sql顧客・active adminの役割分離 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170001_create_transfer_beneficiary_and_otp.sqlOTP tableとRPCの権限境界 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823170004_sanitize_confirm_transfer_otp_failure_reason.sql生DB error漏洩の修正 - 過去の実装 — HISTORICAL IMPLEMENTATION
supabase/migrations/20260823190001_create_transfer_beneficiaries.sqlowner-only振込先RPC - 現在の実装 — CURRENT IMPLEMENTATION
src/lib/errors/rpcErrors.tsmapRpcError()SQLSTATEから安全な公開messageへの変換 - 現在の実装 — CURRENT IMPLEMENTATION
scripts/regression/suites/privilege.mjsACLとschema公開設定の回帰 - 過去の実装 — HISTORICAL IMPLEMENTATION
scripts/regression/suites/transferOtp.mjs#128以前のデモOTP前提の回帰(OTP ownershipとraw error非漏洩)