数日前、セキュリティレビュー中に発見した役割と特権に関する一般的な問題についてブログを書きました。
もちろん、PostgreSQLは多くの高度なセキュリティ関連機能を提供します。そのうちの1つは、PostgreSQL 9.5以降で利用可能な行レベルセキュリティ(RLS)です。
9.5は2016年1月にリリースされたため(ほんの数か月前)、RLSはかなり新しい機能であり、まだ多くの本番環境への導入を扱っていません。代わりに、RLSは「実装方法」の議論の一般的な主題であり、最も一般的な質問の1つは、アプリケーションレベルのユーザーとRLSをどのように機能させるかです。では、考えられる解決策を見てみましょう。
RLSの紹介
まず、RLSとは何かを説明する、非常に簡単な例を見てみましょう。 chat
があるとしましょう ユーザー間で送信されたメッセージを格納するテーブル–ユーザーは、他のユーザーにメッセージを送信するために行を挿入し、他のユーザーによって送信されたメッセージを確認するためにクエリを実行できます。したがって、テーブルは次のようになります。
CREATE TABLE chat ( message_uuid UUID PRIMARY KEY DEFAULT uuid_generate_v4(), message_time TIMESTAMP NOT NULL DEFAULT now(), message_from NAME NOT NULL DEFAULT current_user, message_to NAME NOT NULL, message_subject VARCHAR(64) NOT NULL, message_body TEXT );
従来の役割ベースのセキュリティでは、テーブル全体またはテーブルの垂直スライス(列)へのアクセスのみを制限できます。そのため、ユーザーが他のユーザー向けのメッセージを読んだり、偽のmessage_from
を使用してメッセージを送信したりするのを防ぐために使用することはできません。 フィールド。
そして、まさにそれがRLSの目的です。これにより、行のサブセットへのアクセスを制限するルール(ポリシー)を作成できます。たとえば、これを行うことができます:
CREATE POLICY chat_policy ON chat USING ((message_to = current_user) OR (message_from = current_user)) WITH CHECK (message_from = current_user)
このポリシーにより、ユーザーは自分から送信されたメッセージまたは自分宛てのメッセージのみを表示できます。これがUSING
の条件です。 節はありません。ポリシーの2番目の部分(WITH CHECK
)ユーザーがmessage_from
に自分のユーザー名のメッセージのみを挿入できることを保証します 列、偽造された送信者とのメッセージを防ぎます。
また、RLSは、追加のWHERE条件を自動的に追加する方法として想像することもできます。これはアプリケーションレベルで手動で行うこともできますが(RLSの人々が頻繁に行う前に)、RLSは信頼性が高く安全な方法でそれを行います(たとえば、さまざまな情報漏えいを防ぐために多大な努力が払われました)。
注 :RLS以前は、同様のことを実現するための一般的な方法は、テーブルに直接アクセスできないようにし(すべての特権を取り消す)、それにアクセスするための一連のセキュリティ定義関数を提供することでした。これはほぼ同じ目標を達成しましたが、関数にはさまざまな欠点があります。オプティマイザーを混乱させ、柔軟性を大幅に制限する傾向があります(ユーザーが何かをする必要があり、それに適した関数がない場合、彼は運が悪い)。そしてもちろん、それらの関数を作成する必要があります。
アプリケーションユーザー
RLSに関する公式ドキュメントを読むと、1つの詳細に気付くかもしれません。すべての例でcurrent_user
が使用されています。 、つまり現在のデータベースユーザー。しかし、それは最近のほとんどのデータベースアプリケーションの仕組みではありません。多くの登録ユーザーがいるWebアプリケーションは、データベースユーザーへの1:1マッピングを維持しませんが、代わりに単一のデータベースユーザーを使用してクエリを実行し、アプリケーションユーザーを自分で管理します(おそらくusers
) テーブル。
技術的には、PostgreSQLで多くのデータベースユーザーを作成することは問題ではありません。データベースは問題なくそれを処理する必要がありますが、アプリケーションは多くの実用的な理由でそれを処理しません。たとえば、各ユーザーの追加情報(部門、組織内の位置、連絡先の詳細など)を追跡する必要があるため、アプリケーションにはusers
が必要になります。 とにかくテーブル。
もう1つの理由は、接続プールです。単一の共有ユーザーアカウントを使用しますが、継承とSET ROLE
を使用して解決できることはわかっています。 (前の投稿を参照してください。)
ただし、個別のデータベースユーザーを作成するのではなく、単一の共有データベースアカウントを引き続き使用し、アプリケーションユーザーでRLSを使用するとします。どうすればいいですか?
セッション変数
基本的に必要なのは、データベースセッションに追加のコンテキストを渡して、後でセキュリティポリシーから(current_user
の代わりに)使用できるようにすることです。 変数)。そして、PostgreSQLでそれを行う最も簡単な方法はセッション変数です:
SET my.username = 'tomas'
これが通常の構成パラメーターに似ている場合(例:SET work_mem = '...'
)、あなたは絶対に正しいです–それはほとんど同じことです。このコマンドは、新しい名前空間(my
)を定義します )、username
を追加します それに変数。グローバル名前空間はサーバー構成用に予約されており、新しい変数を追加できないため、新しい名前空間が必要です。これにより、次のようにセキュリティポリシーを変更できます。
CREATE POLICY chat_policy ON chat USING (current_setting('my.username') IN (message_from, message_to)) WITH CHECK (message_from = current_setting('my.username'))
私たちがする必要があるのは、接続プール/アプリケーションが新しい接続を取得してユーザータスクに割り当てるたびにユーザー名を設定することを確認することです。
ユーザーが接続で任意のSQLを実行できるようにすると、またはユーザーが適切なSQLインジェクションの脆弱性を発見できた場合、このアプローチは崩壊することを指摘しておきます。その場合、彼らが任意のユーザー名を設定するのを妨げるものは何もありません。しかし、絶望しないでください。その問題にはたくさんの解決策があり、すぐに解決します。
署名されたセッション変数
最初の解決策は、セッション変数の単純な改善です。ユーザーが任意の値を設定するのを実際に防ぐことはできませんが、値が破壊されていないことを確認できたらどうでしょうか。これは、単純なデジタル署名を使用して行うのはかなり簡単です。ユーザー名を保存するだけでなく、信頼できる部分(接続プール、アプリケーション)は次のようなことを実行できます。
signature = sha256(username + timestamp + SECRET)
次に、値と署名の両方をセッション変数に格納します。
SET my.username = 'username:timestamp:signature'
ユーザーがSECRET文字列(たとえば、128Bのランダムデータ)を知らないと仮定すると、署名を無効にせずに値を変更することはできません。
注 :これは新しいアイデアではありません。基本的に、署名されたHTTPCookieと同じです。 Djangoには、それに関する非常に優れたドキュメントがあります。
SECRET値を保護する最も簡単な方法は、ユーザーがアクセスできないテーブルに値を格納し、security definer
を提供することです。 機能、パスワードが必要です(ユーザーが任意の値に単純に署名できないようにするため)。
CREATE FUNCTION set_username(uname TEXT, pwd TEXT) RETURNS text AS $ DECLARE v_key TEXT; v_value TEXT; BEGIN SELECT sign_key INTO v_key FROM secrets; v_value := uname || ':' || extract(epoch from now())::int; v_value := v_value || ':' || crypt(v_value || ':' || v_key, gen_salt('bf')); PERFORM set_config('my.username', v_value, false); RETURN v_value; END; $ LANGUAGE plpgsql SECURITY DEFINER STABLE;
この関数は、テーブル内の署名キー(秘密)を検索し、署名を計算してから、その値をセッション変数に設定するだけです。また、主に便宜上、値を返します。
したがって、信頼できる部分は、ユーザーに接続を渡す直前にこれを行うことができます(明らかに、「パスフレーズ」は本番環境に適したパスワードではありません):
SELECT set_username('tomas', 'passphrase')
そしてもちろん、署名を検証し、エラーが発生するか、署名が一致した場合にユーザー名を返す別の関数が必要です。
CREATE FUNCTION get_username() RETURNS text AS $ DECLARE v_key TEXT; v_parts TEXT[]; v_uname TEXT; v_value TEXT; v_timestamp INT; v_signature TEXT; BEGIN -- no password verification this time SELECT sign_key INTO v_key FROM secrets; v_parts := regexp_split_to_array(current_setting('my.username', true), ':'); v_uname := v_parts[1]; v_timestamp := v_parts[2]; v_signature := v_parts[3]; v_value := v_uname || ':' || v_timestamp || ':' || v_key; IF v_signature = crypt(v_value, v_signature) THEN RETURN v_uname; END IF; RAISE EXCEPTION 'invalid username / timestamp'; END; $ LANGUAGE plpgsql SECURITY DEFINER STABLE;
また、この関数はパスフレーズを必要としないため、ユーザーは次のようにするだけで済みます。
SELECT get_username()
ただし、get_username()
機能はセキュリティポリシーを対象としています。このように:
CREATE POLICY chat_policy ON chat USING (get_username() IN (message_from, message_to)) WITH CHECK (message_from = get_username())
単純な拡張機能としてパックされた、より完全な例は、ここにあります。
すべてのオブジェクト(テーブルと関数)は、データベースにアクセスするユーザーではなく、特権ユーザーによって所有されていることに注意してください。ユーザーはEXECUTE
しか持っていません 関数に対する特権。ただし、SECURITY DEFINER
として定義されています。 。これが、ユーザーから秘密を保護しながらこのスキームを機能させる理由です。関数はSTABLE
として定義されています 、crypt()
への呼び出しの数を制限します 機能(ブルートフォーシングを防ぐために意図的に費用がかかります)。
サンプル関数は間違いなくさらに作業が必要です。ただし、保護されたセッション変数に追加のコンテキストを格納する方法を示す概念実証には十分であるといいのですが。
あなたが尋ねる修正が必要なものは何ですか?まず、関数はさまざまなエラー状態をうまく処理しません。次に、署名された値にはタイムスタンプが含まれていますが、実際には何もしていません。たとえば、値を期限切れにするために使用される場合があります。値にビットを追加することができます。例:ユーザーの部門、またはセッションに関する情報(たとえば、他の接続で同じ値を再利用しないようにするためのバックエンドプロセスのPID)。
暗号
2つの関数は暗号化に依存しています。いくつかの単純なハッシュ関数を除いてあまり使用していませんが、それでも単純な暗号化スキームです。そして、誰もがあなたがあなた自身の暗号をするべきではないことを知っています。そのため、pgcrypto拡張機能、特にcrypt()
を使用しました。 この問題を回避するための関数。しかし、私は暗号学者ではないので、スキーム全体は問題ないと思いますが、何かが足りない可能性があります。何かを見つけたらお知らせください。
また、署名は公開鍵暗号化に最適です。署名にはパスフレーズ付きの通常のPGP鍵を使用し、署名検証には公開部分を使用できます。残念ながら、pgcryptoは暗号化のためにPGPをサポートしていますが、署名はサポートしていません。
代替アプローチ
もちろん、さまざまな代替ソリューションがあります。たとえば、署名シークレットをテーブルに保存する代わりに、関数にハードコーディングすることができます(ただし、ユーザーがソースコードを表示できないようにする必要があります)。または、C関数で署名を行うこともできます。その場合、メモリにアクセスできないすべての人からは非表示になります(この場合、とにかく失われます)。
また、署名のアプローチがまったく気に入らない場合は、署名された変数を従来の「ボールト」ソリューションに置き換えることができます。データを保存する方法が必要ですが、定義された方法を除いて、ユーザーがコンテンツを任意に表示または変更できないようにする必要があります。しかしねえ、それはsecurity definer
を使用して実装されたAPIを備えた通常のテーブルです 関数ができる!
ここでは、作り直した例全体を紹介するつもりはありませんが(完全な例については、この拡張機能を確認してください)、必要なのはsessions
です。 ボールトとして機能するテーブル:
CREATE TABLE sessions ( session_id UUID PRIMARY KEY, session_user NAME NOT NULL )
通常のデータベースユーザーがテーブルにアクセスできないようにする必要があります–単純なREVOKE ALL FROM ...
その世話をする必要があります。そして、2つの主要な機能で構成されるAPI:
-
set_username(user_name, passphrase)
–ランダムなUUIDを生成し、データをボールトに挿入し、UUIDをセッション変数に格納します -
get_username()
–セッション変数からUUIDを読み取り、テーブル内の行を検索します(一致する行がない場合はエラー)
このアプローチは、署名保護をUUIDのランダム性に置き換えます。ユーザーはセッション変数を微調整できますが、既存のIDにヒットする可能性はごくわずかです(UUIDは128ビットのランダム値です)。
これは、従来の役割ベースのセキュリティに依存する、もう少し従来のアプローチですが、いくつかの欠点もあります。たとえば、実際にはデータベースの書き込みを行うため、ホットスタンバイシステムと本質的に互換性がありません。
パスワードを削除する
パスフレーズが不要になるようにボールトを設計することもできます。 set_username
を想定したため、導入しました 同じ接続で発生します–関数を実行可能に保つ必要があり(したがって、役割や特権をいじることは解決策ではありません)、パスフレーズは信頼できるコンポーネントのみが実際にそれを使用できることを保証します。
しかし、署名/セッションの作成が別の接続で行われ、結果(署名された値またはセッションUUID)のみがユーザーに渡される接続にコピーされる場合はどうなりますか?そうですね、パスフレーズはもう必要ありません。 (これはKerberosが行うことと少し似ています。信頼できる接続でチケットを生成し、そのチケットを他のサービスに使用します。)
概要
それでは、このブログ投稿を簡単に要約します:
- すべてのRLSの例ではデータベースユーザーを使用していますが(
current_user
を使用) )、RLSをアプリケーションユーザーと連携させることはそれほど難しくありません。 - セッション変数は、ユーザーに接続を渡す前に変数を設定できる信頼できるコンポーネントがシステムにあると仮定すると、信頼性が高く非常に単純なソリューションです。
- ユーザーが任意のSQLを実行できる場合(設計上または脆弱性のおかげで)、符号付き変数はユーザーが値を変更できないようにします。
- 他の解決策も可能です。セッション変数を、ランダムなUUIDで識別されるセッションに関する情報を格納するテーブルに置き換えます。
- セッション変数はデータベースへの書き込みを行わないため、このアプローチは読み取り専用システム(ホットスタンバイなど)で機能します。
このブログシリーズの次のパートでは、システムに信頼できるコンポーネントがない場合のアプリケーションユーザーの使用について説明します(そのため、セッション変数を設定したり、sessions
に行を作成したりすることはできません。 表)、またはデータベース内で(追加の)カスタム認証を実行する場合。