安全な pg_columnmask 実装のベストプラクティス
次のセクションでは、Aurora PostgreSQL 環境で pg_columnmask を実装するためのセキュリティのベストプラクティスについて説明します。次の推奨事項に従ってください。
安全なロールベースのアクセスコントロールアーキテクチャを確立する
セキュリティの脆弱性を防ぐマスキング関数を開発する
マスクされたデータを使用してトリガーの動作を理解して制御する
ロールベースのセキュリティアーキテクチャ
データベースにアクセス制御を実装するロール階層を定義します。Aurora PostgreSQL pg_columnmask は、これらのロール内できめ細かなデータマスキング用のレイヤーを追加することで、これらのコントロールを強化します。
個々のユーザーにアクセス許可を付与するのではなく、組織の機能に沿った専用ロールを作成します。このアプローチにより、監査性が向上し、組織構造が進化するにつれてアクセス許可の管理が簡素化されます。
例組織ロール階層の作成
次の例では、さまざまな機能専用のロールを持つ組織ロール階層を作成し、個々のユーザーを適切なロールに割り当てます。この例では、組織ロール (analyst_role、support_role) が最初に作成され、個々のユーザーにこれらのロールのメンバーシップが付与されます。この構造により、個々のユーザーではなくロールレベルでアクセス許可を管理できます。
-- Create organizational role hierarchy CREATE ROLE data_admin_role; CREATE ROLE security_admin_role; CREATE ROLE analyst_role; CREATE ROLE support_role; CREATE ROLE developer_role; -- Specify security_admin_role as masking policy manager in the DB cluster parameter -- group pgcolumnmask.policy_admin_rolname = 'security_admin_role' -- Create specific users and assign to appropriate roles CREATE USER security_manager; CREATE USER data_analyst1, data_analyst2; CREATE USER support_agent1, support_agent2; GRANT security_admin_role TO security_manager; GRANT analyst_role TO data_analyst1, data_analyst2; GRANT support_role TO support_agent1, support_agent2;
各ロールに必要な最小限のアクセス許可のみを付与することで、最小特権の原則を実装します。認証情報が侵害された場合に悪用される可能性のある広範なアクセス許可を付与することは避けてください。
-- Grant specific table permissions rather than schema-wide access GRANT SELECT ON sensitive_data.customers TO analyst_role; GRANT SELECT ON sensitive_data.transactions TO analyst_role; -- Do not grant: GRANT ALL ON SCHEMA sensitive_data TO analyst_role;
ポリシー管理者には、マスキングポリシーを管理するスキーマに対する USAGE 権限が必要です。最小特権の原則に従って、これらの権限を選択的に付与します。スキーマアクセス許可を定期的に見直して、承認された担当者のみがポリシー管理機能を維持できるようにします。
ポリシー管理者ロールのパラメータ設定は、データベース管理者のみに制限されます。このパラメータをデータベースレベルまたはセッションレベルで変更することはできません。これにより、権限のないユーザーがポリシー管理者割り当てを上書きできなくなります。この制限により、マスキングポリシーの制御が一元化され、安全に維持されます。
ポリシー管理者ロールをグループではなく特定の個人に割り当てます。このターゲットを絞ったアプローチにより、ポリシー管理者はデータベース内のすべてのテーブルをマスクできるため、マスキングポリシー管理への選択的なアクセスが保証されます。
安全なマスキング関数の開発
早期バインディングセマンティクスを使用してマスキング関数を開発し、適切な依存関係の追跡を確保し、ランタイム中の検索パスの変更などの遅延バインディングの脆弱性を防止します。SQL 関数の BEGIN ATOMIC 構文を使用して、コンパイル時の検証 (早期バインディング) と依存関係管理を有効にすることをお勧めします。
-- Example - Secure masking function with early binding CREATE OR REPLACE FUNCTION secure_mask_ssn(input_ssn TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT CASE WHEN input_ssn IS NULL THEN NULL WHEN length(input_ssn) < 4 THEN repeat('X', length(input_ssn)) ELSE repeat('X', length(input_ssn) - 4) || right(input_ssn, 4) END; END;
または、すべてのオブジェクト参照を明示的にスキーマで修飾することで、検索パスの変更を許容しない関数を作成し、異なるユーザーセッション間で一貫した動作を確保します。
-- Function immune to search path changes CREATE OR REPLACE FUNCTION data_masking.secure_phone_mask(phone_number TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT AS $$ SELECT CASE WHEN phone_number IS NULL THEN NULL WHEN public.length(public.regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX' ELSE public.regexp_replace( phone_number, '([0-9]{3})[0-9]{3}([0-9]{4})', public.concat('\1-XXX-\2') ) END; $$;
マスキング関数内に入力検証を実装してエッジケースを処理し、予期しない動作を防止します。常に NULL 処理を含め、入力形式を検証して、一貫したマスキング動作を確保します。
-- Robust masking function with comprehensive input validation CREATE OR REPLACE FUNCTION secure_mask_phone(phone_number TEXT) RETURNS TEXT LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT CASE WHEN phone_number IS NULL THEN NULL WHEN length(trim(phone_number)) = 0 THEN phone_number WHEN length(regexp_replace(phone_number, '[^0-9]', '', 'g')) < 10 THEN 'XXX-XXX-XXXX' ELSE regexp_replace(phone_number, '([0-9]{3})[0-9]{3}([0-9]{4})', '\1-XXX-\2') END; END;
pg_columnmask を使用した DML トリガーの動作
テーブルトリガーの場合、移行テーブルは完全にマスク解除されます。ビュートリガー (IOT) の場合、移行テーブルは現在のユーザーのビューアクセス許可に従ってマスクされます。
- pg_columnmask を使用したテーブルトリガー
トリガーには、DML クエリの実行によって変更された行の古いバージョンと新しいバージョンを含む移行テーブルが渡されます。トリガーがいつ起動されるかに応じて、Aurora PostgreSQL は古い行と新しい行を入力します。例えば、参照する古いバージョンがないため、
BEFORE INSERTトリガーには新しいバージョンの行と空の古いバージョンのみが含まれます。pg_columnmaskは、テーブルのトリガー内の移行テーブルをマスクしません。トリガーは、マスクされた列を本文内で使用でき、マスクされていないデータが表示されます。トリガー作成者は、トリガーがユーザーに対してどのように実行されるかを確認する必要があります。この場合、次の例は正しく動作します。-- Example for table trigger uses masked column in its definition -- Create a table and insert some rows CREATE TABLE public.credit_card_table ( name TEXT, credit_card_no VARCHAR(16), is_fraud BOOL ); INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud) VALUES ('John Doe', '4532015112830366', false), ('Jane Smith', '5410000000000000', true), ('Brad Smith', '1234567891234567', true); -- Create a role which will see masked data and grant it privileges CREATE ROLE intern_user; GRANT SELECT, DELETE ON public.credit_card_table TO intern_user; -- Trigger which will silenty skip delete of non fraudelent credit cards CREATE OR REPLACE FUNCTION prevent_non_fraud_delete() RETURNS TRIGGER AS $$ BEGIN IF OLD.is_fraud = false THEN RETURN NULL; END IF; RETURN OLD; END; $$ LANGUAGE plpgsql; CREATE TRIGGER prevent_non_fraud_delete BEFORE DELETE ON credit_card_table FOR EACH ROW EXECUTE FUNCTION prevent_non_fraud_delete(); CREATE OR REPLACE FUNCTION public.return_false() RETURNS BOOLEAN LANGUAGE SQL IMMUTABLE PARALLEL SAFE STRICT BEGIN ATOMIC SELECT false; END; -- A masking policy that masks both credit card number and is_fraud column. -- If we apply masking inside trigger then prevent_non_fraud_delete trigger will -- allow deleting more rows to masked user (even non fraud ones). CALL pgcolumnmask.create_masking_policy( 'mask_credit_card_no_&_is_fraud'::NAME, 'public.credit_card_table'::REGCLASS, JSON_BUILD_OBJECT('credit_card_no', 'pgcolumnmask.mask_text(credit_card_no)', 'is_fraud', 'public.return_false()')::JSONB, ARRAY['intern_user']::NAME[], 10::INT ); -- Test trigger behaviour using intern_user BEGIN; SET ROLE intern_user; -- credit card number & is_fraud is completely masked from intern_user SELECT * FROM public.credit_card_table; name | credit_card_no | is_fraud ------------+------------------+---------- John Doe | XXXXXXXXXXXXXXXX | f Jane Smith | XXXXXXXXXXXXXXXX | f Brad Smith | XXXXXXXXXXXXXXXX | f (3 rows) -- The delete trigger lets the intern user delete rows for Jane and Brad even though -- intern_user sees their is_fraud = false, but the table trigger works with original -- unmasked value DELETE FROM public.credit_card_table RETURNING *; name | credit_card_no | is_fraud ------------+------------------+---------- Jane Smith | XXXXXXXXXXXXXXXX | f Brad Smith | XXXXXXXXXXXXXXXX | f (2 rows) COMMIT;トリガー作成者は、ユーザーがトリガー本文で使用するステートメントに気を付けないと、マスクされていないデータをユーザーに漏洩してしまいます。例えば、
RAISE NOTICE ‘%’, masked_column;を使用すると、現在のユーザーに列が出力されます。-- Example showing table trigger leaking column value to current user CREATE OR REPLACE FUNCTION leaky_trigger_func() RETURNS TRIGGER AS $$ BEGIN RAISE NOTICE 'Old credit card number was: %', OLD.credit_card_no; RAISE NOTICE 'New credit card number is %', NEW.credit_card_no; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER leaky_trigger AFTER UPDATE ON public.credit_card_table FOR EACH ROW EXECUTE FUNCTION leaky_trigger_func(); -- Grant update on column is_fraud to auditor role -- auditor will NOT HAVE PERMISSION TO READ DATA CREATE ROLE auditor; GRANT UPDATE (is_fraud) ON public.credit_card_table TO auditor; -- Also add auditor role to existing masking policy on credit card table CALL pgcolumnmask.alter_masking_policy( 'mask_credit_card_no_&_is_fraud'::NAME, 'public.credit_card_table'::REGCLASS, NULL::JSONB, ARRAY['intern_user', 'auditor']::NAME[], NULL::INT ); -- Log in as auditor -- [auditor] -- Update will fail if trying to read data from the table UPDATE public.credit_card_table SET is_fraud = true WHERE credit_card_no = '4532015112830366'; ERROR: permission denied for table cc_table -- [auditor] -- But leaky update trigger will still print the entire row even though -- current user does not have permission to select from public.credit_card_table UPDATE public.credit_card_table SET is_fraud = true; NOTICE: Old credit_card_no was: 4532015112830366 NOTICE: New credit_card_no is 4532015112830366- (トリガーの代わりに) pg_columnmask を使用したビューでのトリガー
トリガーは PostgreSQL のビューでのみ作成できます。更新できないビューで DML ステートメントを実行するために使用されます。ビュークエリ内で使用されるビューとベーステーブルの所有者が異なる可能性があるため、トランジットテーブルは常にトリガー (IOT) ではなく内部でマスクされます。この場合、ベーステーブルにはビュー所有者に適用されるマスキングポリシーがあり、ビュー所有者は常にトリガー内のベーステーブルからのマスクされたデータを表示する必要があります。これは、テーブルのトリガーとは異なります。この場合、トリガー作成者とテーブル内のデータは、ここでは存在しない同じユーザーによって所有されるためです。
-- Create a view over credit card table CREATE OR REPLACE VIEW public.credit_card_view AS SELECT * FROM public.credit_card_table; -- Truncate credit card table and insert fresh data TRUNCATE TABLE public.credit_card_table; INSERT INTO public.credit_card_table (name, credit_card_no, is_fraud) VALUES ('John Doe', '4532015112830366', false), ('Jane Smith', '5410000000000000', true), ('Brad Smith', '1234567891234567', true); CREATE OR REPLACE FUNCTION public.print_changes() RETURNS TRIGGER AS $$ BEGIN RAISE NOTICE 'Old row: name=%, credit card number=%, is fraud=%', OLD.name, OLD.credit_card_no, OLD.is_fraud; RAISE NOTICE 'New row: name=%, credit card number=%, is fraud=%', NEW.name, NEW.credit_card_no, NEW.is_fraud; RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER print_changes_trigger INSTEAD OF UPDATE ON public.credit_card_view FOR EACH ROW EXECUTE FUNCTION public.print_changes(); GRANT SELECT, UPDATE ON public.credit_card_view TO auditor; -- [auditor] -- Login as auditor role BEGIN; -- Any data coming out from the table will be masked in instead of triggers -- according to masking policies applicable to current user UPDATE public.credit_card_view SET name = CONCAT(name, '_new_name') RETURNING *; NOTICE: Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=John Doe_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Jane Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Brad Smith_new_name, credit card number=XXXXXXXXXXXXXXXX, is fraud=f name | credit_card_no | is_fraud ---------------------+------------------+---------- John Doe_new_name | XXXXXXXXXXXXXXXX | f Jane Smith_new_name | XXXXXXXXXXXXXXXX | f Brad Smith_new_name | XXXXXXXXXXXXXXXX | f -- Any new data going into the table using INSERT or UPDATE command will be unmasked UPDATE public.credit_card_view SET credit_card_no = '9876987698769876' RETURNING *; NOTICE: Old row: name=John Doe, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=John Doe, credit card number=9876987698769876, is fraud=f NOTICE: Old row: name=Jane Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Jane Smith, credit card number=9876987698769876, is fraud=f NOTICE: Old row: name=Brad Smith, credit card number=XXXXXXXXXXXXXXXX, is fraud=f NOTICE: New row: name=Brad Smith, credit card number=9876987698769876, is fraud=f name | credit_card_no | is_fraud ------------+------------------+---------- John Doe | 9876987698769876 | f Jane Smith | 9876987698769876 | f Brad Smith | 9876987698769876 | f COMMIT;- トリガーの動作を制御するデータベース/ユーザーレベルの GuC
2 つの設定パラメータは、該当するマスキングポリシーを持つユーザーのトリガー実行動作を制御します。これらのパラメータを使用して、追加のセキュリティ制限が必要な場合に、マスクされたテーブルまたはビューでトリガーが実行されないようにします。両方のパラメータはデフォルトで無効になっているため、トリガーが正常に実行されます。
最初の GUC: マスクされたテーブルの発射制限をトリガーする
仕様:
名前:
pgcolumnmask.restrict_dml_triggers_for_masked_users型:
booleanデフォルト:
false(トリガーの実行が許可されます)
TRUE に設定すると、マスクされたユーザーのマスクされたテーブルでトリガーが実行されるのを防ぎます。
pg_columnmaskはエラーを実行します。2 番目の GUC: マスクされたテーブルを持つビューで射撃制限をトリガーする
仕様:
名前:
pgcolumnmask.restrict_iot_triggers_for_masked_users型:
booleanデフォルト:
false(トリガーの実行が許可されます)
TRUE に設定すると、マスクされたユーザーの定義にマスクされたテーブルを含むビューでトリガーの実行が禁止されます。
これらのパラメータは独立して動作し、標準のデータベース設定パラメータのように設定できます。