O slideshow foi denunciado.
Seu SlideShare está sendo baixado. ×

PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring)

Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Anúncio
Carregando em…3
×

Confira estes a seguir

1 de 73 Anúncio

PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring)

Baixar para ler offline

Webアプリケーションにおいて、マルチテナント型、つまり複数のユーザー組織がアプリケーションとデータベースを共有する構成にすることがあります。この構成の持つリスクとして、万が一バグにより他テナントの情報が見えてしまうとそれは情報漏洩となり、重大なインシデントとなってしまうことがあります。この重要性を考えると、「気を付けて実装する」だけではなく、仕組みで漏洩を防ぐような対策には価値があります。

そこで、今回はPostgresSQLの行レベルセキュリティと、SpringAOPによる処理を組み合わせて、ログインしているテナントのデータにしかアクセスできなくする仕組みを実現しました。
導入にあたり考慮した複数の選択肢、乗り越えたいくつかの壁についてご紹介します。
同様の課題を抱えている方の参考にしていただけるような情報をお伝えしたいと思います。

Webアプリケーションにおいて、マルチテナント型、つまり複数のユーザー組織がアプリケーションとデータベースを共有する構成にすることがあります。この構成の持つリスクとして、万が一バグにより他テナントの情報が見えてしまうとそれは情報漏洩となり、重大なインシデントとなってしまうことがあります。この重要性を考えると、「気を付けて実装する」だけではなく、仕組みで漏洩を防ぐような対策には価値があります。

そこで、今回はPostgresSQLの行レベルセキュリティと、SpringAOPによる処理を組み合わせて、ログインしているテナントのデータにしかアクセスできなくする仕組みを実現しました。
導入にあたり考慮した複数の選択肢、乗り越えたいくつかの壁についてご紹介します。
同様の課題を抱えている方の参考にしていただけるような情報をお伝えしたいと思います。

Anúncio
Anúncio

Mais Conteúdo rRelacionado

Diapositivos para si (20)

Semelhante a PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring) (20)

Anúncio

Mais de Koichiro Matsuoka (8)

Mais recentes (20)

Anúncio

PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する (JJUG CCC 2021 Spring)

  1. 1. JJUG CCC 2021 Spring PostgreSQLの行レベルセキュリティと SpringAOPでマルチテナントの ユーザー間情報漏洩を防止する 2021.5.23 松岡 幸一郎 (@little_hand_s) 1
  2. 2. 自己紹介 2 ● 資料作成・発表: 松岡 幸一郎 (@little_hand_s) 株式会社ログラス DDD community jp、Agile Developers Community主催 DDD周りの話をするブログ 「ドメイン駆動設計 モデリング/実装ガイド」執筆 ● 資料作成: 飯田 意己 (@ysk_118) 株式会社ログラス 一般社団法人アジャイルチームを支える会理事 SCRUM BOOT CAMP THE BOOK【増補改訂版】 コラムニスト
  3. 3. 今日お話しすること 3
  4. 4. 今日お話しすること 4 ● Webサービスにおいて、お客様間の情報漏洩を防止する必要性 ● 機械的に防止する方法 ○ 基本方針 ○ 実際に突き当たった問題と解決方法
  5. 5. 経営にまつわる数値を集約し、
 意思決定を支援する経営管理SaaS
 ログラスとはどんなサービスか 5 会計ソフト その他 数値管理ツール
  6. 6. 上場企業含む、規模の大きなお客様の経営数値をお預かりしています 6
  7. 7. 伸びてます!! 7 ● 受賞歴 ○ Incubate Camp 13th 総合優勝 (473社中1位) ○ ICC KYOTO 2020 総合3位
  8. 8. と言うのは置いといて 8
  9. 9. ● 超重要な数値を扱っている ○ 上場企業の経営数値 重要なポイント 9 → 情報漏洩、即、死
  10. 10. ● マルチテナント・単一DB構成 ● テナント=お客様の企業 ○ テナントごとに複数ユーザーが存在する システム構成 10 データベース テナント1 アプリケーション テナント2 テナント3
  11. 11. ● 異なるテナント間で情報が参照できないことを アプリケーションで担保する必要がある システム構成 -必要な制御 11 データベース お客様Aの データ お客様A アプリケーション お客様B お客様C
  12. 12. ユースケース層 ● オニオンアーキテクチャの構成 ● Kotlin + Spring + jOOQ(ORMapper) アプリケーションアーキテクチャ 12 プレゼン テーション層 インフラ層 ドメイン層 (Interface) UserRepository (Class) JooqUserRepository ● DBアクセスはインフラ層のリポジトリクラスで実装される
  13. 13. アプリケーションにおける制御 ● 全てのテーブルにテナントIDがカラムが存在 ● 毎回引数でテナントIDを渡し、SQL実行時忘れずにWhere句に渡している
  14. 14. ● 全てのテーブルにテナントIDがカラムが存在 ● 毎回引数でテナントIDを渡し、SQL実行時忘れずにWhere句に渡している ● 参照系で絞り込みを忘れると、 他テナントの情報が表示されて情報漏洩になる アプリケーションにおける制御 アクティブな お客様Aのユーザー (Class) JooqUserRepository お客様A テナントID1 SELECT * FROM users WHERE user_status = 'ACTIVE' AND tenant_id = ‘1’ 例:アクティブなユーザーを検索する処理
  15. 15. ● 全てのテーブルにテナントIDがカラムが存在 ● 毎回引数でテナントIDを渡し、SQL実行時忘れずにWhere句に渡している ● 参照系で絞り込みを忘れると、 他テナントの情報が表示されて情報漏洩になる 例:アクティブなユーザーを検索する処理 アクティブな お客様Aのユーザー アプリケーションにおける制御 お客様A テナントID1 (Class) JooqUserRepository アクティブな お客様Bのユーザー SELECT * FROM users WHERE user_status = 'ACTIVE' AND tenant_id = ‘1’
  16. 16. ● 2つの「絞り込み忘れ」 絞り込み忘れのリスク ○ テナントIDをメソッド引数として渡し忘れる可能性 ○ メソッド引数として渡してもクエリにセットし忘れる可能性 16
  17. 17. 「気を付ける」アプローチの限界 17 ● 「実装時&コードレビュー時に気を付ける」は100%ではない ● 人が増えたときにレビュー観点を継承していくことは難しい ● 静的解析でも、パラメーターの渡し漏れは検知できない
  18. 18. PostgreSQLには 行レベルセキュリティ (RLS: Row Level Security) という機能があるらしい 18
  19. 19. ● DBセッション(=コネクション)ごとにアクセスできるデータを行単位で 制限できる機能 行レベルセキュリティ(RLS)とは 19 DBセッション (テナント1のみア クセス可能) テナント1データ テーブル テナント1データ テナント2データ テナント2データ テナント3データ DBセッション (テナント2のみア クセス可能)
  20. 20. ● 方針②: ロール(≒ユーザー)で制御する方法 ○ ロールごとにアクセス範囲を設定し、 セッションごとに異なるロールを使用する ● 方針①: セッション変数で制御する方法 ○ 共通のセッションで、 実行時にセッション変数をセットしてアクセス範囲を設定 DBセッション (共通ロール) セッション変数でテナントID=1をセット → テナントID1データのみアクセス可能 DBセッション (テナントID1に アクセスできるロール ) RLSの実装方針 DBセッション (テナントID2に アクセスできるロール ) セッション変数でテナントID=2をセット → テナントID2データのみアクセス可能
  21. 21. ● Webアプリケーションでは、DBセッションは コネクションプーリングの仕組みが管理する コネクションプール RLSの実装方針 DB DBセッション DBセッション DBセッション ①アプリケーション起動時に一定数のコネクショ ン(=セッション)を作成し、プールする ②リクエストごとに、  プールからコネクションが払い出される リクエスト リクエスト リクエスト リクエスト ● コネクションをプールすることで、コネクション確立にかかる時間を削減
  22. 22. ● 方針②: ロールで制御する方法では、 テナント数=ロール数=セッション数が必要 コネクションプール RLSの実装方針 DB : ● テナントの数だけセッションをプールすることは、 DB側の最大接続数を超えるため難しい →「方針①: セッション変数で制御する方法」を採用する テナント3用 DBセッション テナント2用 DBセッション テナント1用 DBセッション
  23. 23. ③ロールの作成 アプリケーションからは権限の弱い ロールでアクセスさせる 権限の弱いロール (RLSの影響を受ける) RLS設定全体像 23 テーブル ②テーブルごとに アクセス制限するPolicyの設定 ①テーブルごとに Row Level Securityを有効化 権限の強いロール (RLSの影響を受けない) DBマイグレーションは 権限の強いロールで行う RLS設定 Policy
  24. 24. ● ②テーブルごとにアクセス制限するPolicyの設定 ● ①テーブルごとにRow Level Securityを有効化 ALTER TABLE tenants ENABLE ROW LEVEL SECURITY; やってみた:RLS設定のクエリ 24 create role appuser login password 'xxxxxxx'; alter default privileges for role appuser in schema public grant select, insert, update, delete on tables to appuser; CREATE POLICY tenant_isolation_policy ON tenants USING (tenant_id = current_setting('app.current_tenant')); ● ③ロールの作成 アプリケーションからは権限の弱いロールでアクセスさせる (AppendixにRole設定時の注意あり) 
  25. 25. ● DBアクセスする前にセッション変数を設定 ○ セッション変数をSETクエリで設定 ○ テナントIDは認証の仕組みから取得 やってみた : アプリケーションの実装 25 fun permitByTenantId(tenantId: ID<Tenant>) { jooq.execute("SET app.current_tenant = '${tenantId.value}'") }
  26. 26. ● 以下を確認できた ○ 設定するセッション変数を変更すると、 アクセスできるデータが変わる ○ ログイン情報から取得したテナントIDを使って 期待通りのデータが取得できる 検証結果 26
  27. 27. 行レベルセキュリティ、使えそう! 27
  28. 28. だが、本当の戦いはここからだった…! 28
  29. 29. ここから、 発生した課題と解決方法を 順に説明していきます 29
  30. 30. ● エンドポイントごとにセッション変数のセットを手動実装すると、 漏れるリスクがある ● 結局、実装しているかをレビューで目視確認しないといけない 導入における課題 30
  31. 31. ● パターン②本当はデータがあるのに「データなし」レスポンスで正常終了 ● パターン①データが取れる想定の処理の後続で例外発生 セッション変数のセットを忘れるとどうなるか 31 ②RLSアクセス許可 できていない状態 なので、0件が返される ③結果0件で正常終了 (正しい挙動ではない!!) ①通常なら 結果が返る条件で リクエスト UseCase Repository Client → パターン②はアラートで気づけないので、非常に困る
  32. 32. 適用時に重要なポイント 32 個別の開発時に意識せず、 自動的に機能が働く仕組みが必要不可欠
  33. 33. 最初のアプローチ 33
  34. 34. ● AOP(Aspect Oriented Programming) ○ アスペクト指向プログラミングと呼ばれ、 クラス呼び出し以外の形で、特定の条件を満たした時に処理を呼び出せる ○ 例外処理やロギングなど処理など、 横断的・暗黙的な処理に向いている AOPの検討 34 ○ その特性が今回の用途にあっていると判断した
  35. 35. ● AOPで、リポジトリクラスのメソッドを呼び出すごとに セッション変数SETクエリを実行 アプローチ①概要 35 (Interface) UserRepository (Class) JooqUserRepository ユースケース層 プレゼン テーション層 インフラ層 ドメイン層
  36. 36. ● 実行時変数のsetクエリが複数回呼ばれて、レイテンシが悪化 ○ setクエリの99%タイルレイテンシが82msec ○ 集計結果を表示するエンドポイントでは1リクエストで 複数テーブルアクセスがあるため、体感できるレベルで悪化 →SETクエリの回数は最低限にしないと レイテンシがアプリケーション全体で問題になる ● 正常に動作した! アプローチ①結果 36 ● が、処理が明らかに遅くなった!
  37. 37. ● 全コントローラーの入り口で実行 ○ AOPで 「@RestControllerがついているクラス実行直前」を指定 アプローチ②概要 37 XxxController ユースケース層 プレゼン テーション層 インフラ層 ドメイン層
  38. 38. ● 「@RestControllerがついているクラス実行直前」を指定する AOP記述 アプローチ②概要 38 AuthInfoProviderは認証情報から テナントIDを取得するクラス RlsAccessPermitterは内部で SETクエリを実行するクラス
  39. 39. ● 画面経由で動作確認できた! ● 必ずリクエストごとに1回だけ実行される!効率的! アプローチ②結果 39 ● ところが・・・ ○ 「たまに」処理が失敗して、エラーログが発生する ○ 処理が成功するケースも多いのに、なぜ?
  40. 40. ● postgresqlログを調査 ● コネクションプーリングの仕組みにより、 1リクエストのなかでDBアクセスのコネクションが変更されることがあった アプローチ②課題 40 Controllerで テナントID1にアクセスできるよ うSETクエリ実行 UseCase RepositoryA Controller テナント1の リクエスト テナント1にアクセスできる DBセッション
  41. 41. ● コネクションが変わると、DBのセッションも別のものになる ● SETクエリは、DBセッションに対して有効になるので、 DBセッションが変わるとRLS制御されたテーブルにアクセスできなくなる 別のDBセッション ● postgresqlログを調査 ● コネクションプーリングの仕組みにより、 1リクエストのなかでDBアクセスのコネクションが変更されることがあった アプローチ②課題 41 Controllerで テナントID1にアクセスできるよ うSETクエリ実行 RepositoryB UseCase RepositoryA Controller テナント1の リクエスト テナント1にアクセスできる DBセッション
  42. 42. ● トランザクションを張ると、begin, commit, rollbackするために トランザクション内では同じDBセッションが使われる ○ postgresqlのログで確認できた アプローチ②課題への対策 → トランザクションを張るタイミングで セッション変数SETクエリを実行する方針とした 42
  43. 43. ● @Transactionalがついたクラス内のメソッドが呼ばれる時にSETクエリを実行 ○ AOPで「@Transactionalがついたクラス実行直前」と指定 アプローチ③概要 43 UseCaseクラスに @Transactional ユースケース層 プレゼン テーション層 インフラ層 ドメイン層
  44. 44. アプローチ③概要 44 ● 「@Transactionalがついたクラス実行直前」を指定するAOP記述
  45. 45. ● 実行クエリも SET app.current_tenant = '${tenantId.value}' から SET local app.current_tenant = '${tenantId.value}' と変更 アプローチ③概要 45 → 変数のスコープがトランザクション内になったので安心
  46. 46. ● 動いた!! アプローチ③結果 46 ● でも・・ ○ また、特定のエンドポイントで動かない・・・
  47. 47. アプローチ③結果 47 ● そのクラスは、@Transactionalを「メソッドに」つけていた ○ @Transactionalはクラスでもメソッドでも動く ○ 実装は両パターン存在していた
  48. 48. ● @Transactionalがついたクラス・メソッドが呼ばれる時にSETクエリを実行 ○ AOP記述を2パターン定義 アプローチ④概要 48
  49. 49. ● 動いた!! アプローチ④結果 49 ● でも・・ また、また特定のエンドポイントで動かない・・・!
  50. 50. アプローチ④課題 50 ● @Transactionalをつけていないエンドポイントがあった ● トランザクションを張らなくても実行はできてしまうので、 張らなくても気づけない
  51. 51. ● 「トランザクションが張られていなかったら例外を投げる」 というチェック処理をリポジトリ用のアノテーションとして定義 アプローチ⑤概要 -AOP部分 51 51 JooqUserRepositoryに 専用の独自アノテーション付与 ユースケース層 プレゼン テーション層 インフラ層 ドメイン層 ● AOPでトランザクション有無チェック処理を呼び出し
  52. 52. ● 通常DIさせたいクラスに付与する @Componentを継承したアノテーション として実装 付与する独自アノテーション 52 @Target(AnnotationTarget.CLASS) @Retention(AnnotationRetention.RUNTIME) @Component annotation class RequireTransactionComponent
  53. 53. アプローチ⑤概要 -AOP部分 53 ● 「@RequireTransactionComponentがついたクラス実行直前」というAOP記述
  54. 54. アプローチ⑤概要 -トランザクションのチェック部分 ● TransactionManagerでトランザクション取得 ● isNewTransactioがtrueであればそのタイミングでトランザクション開始された = チェック開始時にトランザクションがなかったと判断して例外を投げる 54
  55. 55. ● このチェックはDBアクセスせずに行われ、 10msec以下で完了することを確認 アプローチ⑤概要 -トランザクションのチェック 55 ● この処理は1リクエストで何度も呼ばれても問題にならない ● やったね!
  56. 56. ● あ、でも、まだちょっと懸念が・・・ 56
  57. 57. まだあんのかい 57
  58. 58. もうちっとだけ続くんじゃ ● RLSとの戦いはこんな感じの繰り返しでした ● あと2個だけあります 58
  59. 59. → 意図せずトランザクションのチェックが効かなくなる 残ったリスク①概要 ● リポジトリ実装クラスに異なるアノテーションを張ってしまう可能性がある ● @RequireTransactionComponentではなく 通常の@Componentを付与しても動作してしまう JooqUserRepositoryに @RequireTransactionComponent ではなく @Component付与 59 ユースケース層 プレゼン テーション層 インフラ層 ドメイン層
  60. 60. 残ったリスク①対策 60 ● ArchUnitでチェック ○ パッケージ、クラス、アノテーションなどの依存関係を テストするライブラリ ● 「リポジトリを配置するパッケージにあるクラスで、   @RequireTransactionComponentがついていなかったらfail」 というテストを実装 ArchUnitドキュメントより
  61. 61. ● 新規テーブル追加時にRLSの設定を忘れてしまうリスク ● RLS設定はテーブル単位なので、 新規テーブルに忘れず設定しなければ有効にならない ● 有効になっていなくても気付くきっかけがない 残ったリスク②概要 61
  62. 62. 残ったリスク②対策 ● 自動テスト(JUnit)で、 「”対象外テーブル”以外でRLS設定されてなったらfail」というテストを実装 62 RLS除外対象テーブルを定義し、テストコー ドでここに埋める。 1件以上値が帰ってきたらFailとする。
  63. 63. ついに完成!! 63
  64. 64. ● 新規テーブル追加時にはRLS設定を行う ○ 忘れた場合、自動テストがfailするので気付ける ● エンドポイント実装時には以下2つを守ればOK ○ ユースケースクラスには@Transactionalを付与する ○ リポジトリ実装クラスには@RequireTransactionComponentを付与する ● アノテーションの付与はRLS関係なく行うので、違和感なく実装できる ● 付与しなければ例外が発生するので気付ける 実施内容まとめ 64 ● これで、「うっかり忘れ」を機械的に防げるようになった!!
  65. 65. リリース戦略 65 ● リリースをどのように行うか ● 影響範囲が大きい(DBアクセスする全エンドポイント)ので、 安全にリリースしていく
  66. 66. リリース戦略:段階的にリリース 66 ● ①1テーブルのみ対象にしてリリースして、問題ないことを確認 ○ 検証環境リリースの段階で、いろいろな問題が発生&対処 ○ 全てクリアしてから、本番リリース →ここまでくれば勝ち!! ● ②あとは念のためにテーブルを機能群ごとに何度かに分けてリリース ■ 実際は2回に分けました
  67. 67. ● RLSのpolicyの制御で、テナントIDで絞り込みが行われる ○ explainを取ったところ、 テナントIDを含んだインデックススキャンになっていた ● 計測したが、大きなパフォーマンス劣化は観測されなかった リリース後の影響 67 ● RLS導入前からテナントIDは毎回検索条件に含めており、 インデックスが張られていたため大きな影響がなかったと思われる
  68. 68. ● 「どう転んでもテナント間情報流出がない」という圧倒的安心感! ○ 実装がアノテーション付与だけで簡単、忘れたら気付けるので安心 ○ レビュー時に目を皿にしてみなくてよくなった 導入後の感想 68
  69. 69. まとめ 69 ● 人間はミスするもの、ミスを見越して機械的アプローチを! ○ 同じような課題を抱えているチームは多いはず、参考になれば幸いです
  70. 70. ご静聴ありがとうございました 70
  71. 71. sli.do回答タイム 71
  72. 72. Appendix 72
  73. 73. Postgres側の設定の注意点 ● テーブル所有者(マイグレーション実行ロール)から、アプリケーション接続ロールに対し てデフォルト権限を付与する ○ これをしないとテーブル追加時に毎回アプリケーション接続ロールにGRANTが必 要になってしまう ○ ALTER DEFAULT PRIVILEGES FOR ROLE マイグレーションロール IN SCHEMA PUBLIC GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO アプリケーション接続ロール ● ポリシー設定でSELECT権限のみの設定でもINSERT … RETURNINGコマンドなどに影 響が出る 73 https://www.postgresql.jp/document/12/html/sql-createpolicy.html

×