まとめ
一定の運用負荷はかかるが、複数のアプリケーションからアクセスされるため
テナント検証が十分に行われていないアプリからアクセスされてもDB側単体で防御できることを優先しRLSを採用。
背景
アーキテクチャの見直しでセキュリティの確保を行っている中、他テナントの情報が見えてしまわないようRowLevelSecurityを採用するか検討を行うこととなった。
検討開始時点の状態
単純な3層アプリケーション。
この状態でbackendではテナント分離のテストが実装されており、アプリケーションレベルでのテナント分離は実現できていた。
RLSはDBレベルのテナント分離
DCLでDBに設定するもの。
RLSを採用する際に意識したポイント
新しい仕組みを導入するときには仕組みによる運用負荷・移行コストを検討する必要があるため採用時に検討したポイントを紹介します。
枯れた機能であるか
プロダクションに適用した後で重大なバグがあったとわかった場合、特にDBに関わる部分のため影響が大きくなると判断し、(デファクトと知りつつも)念の為どの程度前のPostgreバージョンから存在するかなど、簡単に確認をとりました。
適用もれ確認の運用負荷
RLSはDBレベルでのテナント分離であるため、ORMのマイグレーションファイルにRLSの設定DCL(Data Control Language)を書く必要がある。
また、書く以上はそれが正しく適用できているかのテストや確認が必要になる。
さらに、マイグレーション後に毎回DBの設定が正しいかを「どう確認すれば良いのか?」が見えていなかったのも運用負荷がかかるのではと不安なポイントでした。
ユーザ管理の運用負荷
また、当時は運用上スーパーユーザをそのまま利用しており、スーパーユーザにRLSが適用されないということに気づきました。
そのためアプリケーション用のユーザを作成・管理する必要があり、(スーパーユーザのままはセキュリティ的によろしくないのは認識しつつ、)実運用として必要の薄いアプリケーション用ユーザを作成・運用すべきかも焦点になりました。
テナント増加時の運用負荷
テナント数nに対して個別にやらなければならない設定がある場合、作業量も比例して増加するため、作業量の確認は焦点になりました。
自動テストの移行コスト
当時自動テストにおいて、ORM接続ロジックをテストするために実際にローカルのPostgreに接続してデータ操作のテストを行っていました。
このとき、スーパーユーザにはRLSが適用されないため、DBにRLSを適用しても意味がなく、以下のようにユーザを使い分けるように修正する必要がありました。
-
アプリケーション用DBユーザ: バックエンドのテストで利用するユーザ -
スーパーユーザ: テストデータの作成やデータのクリーンアップ
RLS採用に至ったポイント
ここでは上記ポイントを意識しつつRLSを採用したポイントを書きます。
(大前提スーパーユーザとアプリケーションユーザを分離すべきみたいなことはこの際対応しようと言うことで対応)
RLSの設定はテナント増加ごとにやらなくて良い点
マルチテナントSaaSであるため、テナントが増えるたびに新規テナントに対して設定を追加する必要があるかはポイントに書いた点ですが、結論テナントごとの設定は行わなくて良いことがわかったため、安心したポイントでした。
設定内容は各テーブルにおいて各テナントが自分のテナントしか操作できないようにするというもの。
実際はシステム管理者はすべてのテナントのデータを取得できるという内容も含まれますが、これによって個別テナントごとに対応する必要はないことがわかりました。
以下アプリケーションユーザにセットされたテナントしか操作できないように設定するDCLの例
-- RLSをテーブル単位に有効化
ALTER TABLE product.items ENABLE ROW LEVEL SECURITY;
-- RLSポリシーの記載
CREATE POLICY product_tenant_isolation
ON product.items
FOR ALL
TO application_user
USING (tenant_id = current_setting('app.current_tenant')::uuid)
WITH CHECK (tenant_id = current_setting('app.current_tenant')::uuid);
アプリでテナント制御が漏れた場合に何も見えない状態を実現できる
今まではアプリケーションの自動テストで他のテナントが見えないことをテストしていましたが、仮にここが漏れる(Where句でtenantのフィルタが行われない)と他のテナントの情報が漏洩してしまいます。
ただし、RLSを設定しておくとテナントが設定されていない場合はデータを取得できないようにする制御が可能です。
改めて多層防御の重要性を再認識したポイントです。
-- DML実行前に自分のテナントをセットしないとデータ操作できない
SET app.current_tenant = '7d66c02d-5069-4b98-8cbe-5ac0dc7f1d0f';
INSERT INTO product.items (id, tenant_id, name)
Backend以外からもアクセスされることとなった
実はこの検討期間中、新規要件にてECS以外のLambdaからAuroraへアクセスされることとなりました。
こうなると視点が変わるとともにだいぶ話が変わってきます。
まず今まではDB到達前に必ずバックエンドのECSを通過するためそこさえテストで固めておけば良いように見えていました。
しかしLambdaが増えることで、今後DBアクセスするクライアントが無数に増えうることが示唆されたため、いずれの経路でも必ず通過するDBの制御ロジックという層で防御する必要性が明確になりました。
これもまたテストや確認が漏れ、意図せず不正なアクセスを許した場合を想定した多層防御の重要性が見えたポイントだと思います。
適用もれ確認の運用
先ほど運用負荷の点でマイグレーション後に毎回DBの設定が正しいかを「どう確認すれば良いのか?」が見えていなかったと記載しましたが、これもGitHubActionsなどでチェック用SQLを定期実行することで解決できるのではと考えています。
最後に
今回はRLSを採用した経緯や検討ポイントを紹介しました。
DBに関連する部分はマイグレーションやデータの管理にかなり気を使うポイントであるために慎重な検討が必要になるとともに、チームにRLS運用経験者がいなかったため、手元でのテストや上位の説明も兼ねて色々と調査しました。
この記事が何らかの参考になると良いなと思っています。

