Omni Hub チームの shunten31 です. 年明けに朝の運動習慣を取り戻したと思ったら、 寒波のせいでまたやめてしまっています. 早く暖かくなってほしいです.
はじめに
pg_columnmask とは、 AWS Aurora PostgreSQL に対して 昨年末に導入された新機能で、 データベースの動的データマスキングを適用することができる拡張機能です. 特徴として、 下記のような点が挙げられます.
- 動的データマスキングのため、 マスクされた別の Table や View 等を作成する必要がない
- ユーザーごとにマスキングポリシーを変更できる
- 同一カラムに複数のマスキングポリシーを定義し、 重み (
weight) を定義することで、 条件によって実際に適用されるマスクを決定できる
Amazon Aurora PostgreSQL introduces dynamic data masking - AWS
Omni Hub では、 チームメンバーが個人情報をマスクした状態で、 データベースの中身を Redash などの BI ツールからクエリできる環境を整えたいと考えていました. ちょうどそのタイミングでこの新機能が追加されたため、 導入に向けた調査を進めています.
今回は、 pg_columnmask を実運用するにあたり、 マスク対象テーブルのスキーマ変更にどの程度耐性があるのかを調べたので、 その結果を共有します.
マスクの作成方法
以下の syntax で、 マスキングポリシーを作成できます.
create_masking_policy( policy_name, table_name, masking_expressions, roles, weight )
ここでは、 次のテーブルを作成し、 sha256 によるハッシュ化を行うマスクを適用してみます.
CREATE TABLE pets ( id BIGSERIAL PRIMARY KEY, name TEXT, owner TEXT, age INT ); INSERT INTO pets (name, owner, age) VALUES ('Pochi', 'Taro', 3), ('Mimi', 'Hanako', 5);
以下では、 member ユーザーに対して、 owner カラムを sha256 でハッシュ化するマスキングポリシーを設定しています.
CALL pgcolumnmask.create_masking_policy( 'pets_mask_policy', 'public.pets', JSON_BUILD_OBJECT( 'owner', 'encode(digest(owner::text, ''sha256''), ''hex'')' )::JSONB, ARRAY['member'] );
この状態で member ユーザーが pets テーブルの中身を確認すると、 以下のように owner がハッシュ化されていることがわかります.
postgres=> select * from pets; id | name | owner | age ----+-------+------------------------------------------------------------------+----- 1 | Pochi | 3664bbe9be5f347951df653762d15942f892fe4af7a515ea53f16605c8861bc6 | 3 2 | Mimi | bffd8c034536667634381acc56074879167b89a01410312579296aca2887e180 | 5 (2 rows)
スキーマ変更との関連性
運用を想定すると、 マスクを適用しているテーブルに対してスキーマ変更が発生することは避けられません. そこで、 スキーマ変更時にどのような挙動になるのかを確認しました.
今回は、 以下の 5 ケースについて調査します.
- カラムを追加したとき
- マスク対象でないカラムを削除したとき
- マスク対象カラムを削除したとき
- マスク対象カラムの型を変更したとき
- マスク対象カラムをリネームしたとき
ケース1 カラムを追加したとき
先ほどの pets テーブルに対して、 カラムを追加します.
ALTER TABLE pets ADD COLUMN color TEXT; UPDATE pets SET color = 'brown' WHERE name = 'Pochi'; UPDATE pets SET color = 'white' WHERE name = 'Mimi';
問題なくカラム追加が行われ、 member ユーザーとして読み取ると、 次の結果になります.
postgres=> select * from pets; id | name | owner | age | color ----+-------+------------------------------------------------------------------+-----+------- 1 | Pochi | 3664bbe9be5f347951df653762d15942f892fe4af7a515ea53f16605c8861bc6 | 3 | brown 2 | Mimi | bffd8c034536667634381acc56074879167b89a01410312579296aca2887e180 | 5 | white (2 rows)
ケース2 マスク対象でないカラムを削除したとき
マスク対象でないカラムである age を削除してみます.
ALTER TABLE pets DROP COLUMN age;
こちらも問題なく読み取れます.
postgres=> select * from pets; id | name | owner | color ----+-------+------------------------------------------------------------------+------- 1 | Pochi | 3664bbe9be5f347951df653762d15942f892fe4af7a515ea53f16605c8861bc6 | brown 2 | Mimi | bffd8c034536667634381acc56074879167b89a01410312579296aca2887e180 | white (2 rows)
ケース3 マスク対象カラムを削除したとき
次に、 マスキングポリシーを適用しているカラム owner を削除してみます.
postgres=> ALTER TABLE pets DROP COLUMN owner; ERROR: cannot drop column owner of table pets because other objects depend on it DETAIL: policy pets_mask_policy on table pets depends on column owner of table pets HINT: Use DROP ... CASCADE to drop the dependent objects too.
失敗しました. マスク対象カラムを削除する場合は、 事前にマスキングポリシーの更新が必要なようです.
そこで、 代わりに color カラムをマスク対象とするようマスキングポリシーを変更してみます.
alter_masking_policy を使えば、 既存のポリシーを更新できます. NULL を渡している箇所は、 既存の設定を引き継いでいます.
CALL pgcolumnmask.alter_masking_policy( 'pets_mask_policy', 'public.pets', JSON_BUILD_OBJECT( 'color', 'encode(digest(color::text, ''sha256''), ''hex'')' )::JSONB, NULL, NULL );
再度、 owner カラムを削除してみたところ、 成功しました.
ケース4 マスク対象カラムの型を変更したとき
次に、 マスク対象となっている color カラムの型を変更します.
postgres=> ALTER TABLE pets ALTER COLUMN color TYPE JSONB USING to_jsonb(color); ERROR: cannot alter type of a column used in a policy definition DETAIL: policy pets_mask_policy on table pets depends on column "color"
こちらも失敗しました. マスク対象カラムの型を変更する場合も、 事前にマスキングポリシーの更新が必要になります.
ケース5 マスク対象カラムをリネームしたとき
最後に、 マスク対象カラムをリネームした場合の挙動を確認します.
postgres=> ALTER TABLE pets RENAME COLUMN color TO color_hex; ALTER
こちらは成功しました.
member ユーザーとして中身を確認すると、 リネーム後の color_hex カラムがマスキングされていることがわかります.
postgres=> select * from pets; id | name | color_hex ----+-------+------------------------------------------------------------------ 1 | Pochi | 5eb67f9f8409b9c3f739735633cbdf92121393d0e13bd0f464b1b2a6a15ad2dc 2 | Mimi | 018fa96a44715c90bf93be148069cb28dd45d398f2cc75aa1565311f6e55d174 (2 rows)
まとめ
以上をまとめると、 スキーマ変更時の挙動は次のとおりでした.
| スキーマ変更内容 | 挙動 |
|---|---|
| カラム追加 | 影響なし |
| 非マスク対象カラムの変更 | 影響なし |
| マスク対象カラムの DROP | 事前にマスキングポリシーの更新が必要 |
| マスク対象カラムの型変更 | 事前にマスキングポリシーの更新が必要 |
| マスク対象カラムの RENAME | 影響なし (リネーム後のカラムがマスキングされる) |
既存のマスキングポリシーを確認する
このように、 pg_columnmask を利用している場合、 スキーマ変更が失敗するケースがあります. そのため、 スキーマ変更を行う前に、 どのようなマスキングポリシーが設定されているかを事前に確認しておくことが重要です.
以下のクエリで、 既存のマスキングポリシー一覧を確認できます.
postgres=> select * from pgcolumnmask.ddm_policies;
schemaname | tablename | policyname | roles | masked_columns | masking_functions | weight | predicate_allow_list
------------+-----------+--------------------+-----------------+----------------+-----------------------------------------------------------------------------------------------------------------+--------+----------------------
public | pets | pets_mask_policy | {member} | {color} | {"encode(digest(color_hex, 'sha256'::text), 'hex'::text)"} | 0 | {}
(2 rows)
tablename や masked_columns といったカラムを用いて絞り込みを行い、 影響を受けるマスキングポリシーが存在しないかを確認したうえでスキーマ変更を実施すると、 より安全に運用できるでしょう.
注意点
ただし、 上記のクエリ結果において、 masked_columns の値は. リネーム後の color_hex ではなく、 color となっています. masking_functions は、 color_hex の新しい名前に追従していますが、 masked_columns は追従しないようです.
おわりに
今回は、 pg_columnmask の導入にあたり、 マスキングポリシーがスキーマ変更にどのような影響を与えるかを調査しました.
マスキングポリシーが壊れるようなスキーマ変更は事前に弾かれるということがわかりました. 結果として、 マスキングポリシーを見落としたスキーマ変更によって機密データが意図せず閲覧可能な状態になってしまう、といった事故を防ぐ設計になっていると解釈できます.
なお、 pg_columnmask は、 AWS のプロプライエタリソフトウェアであり、 Aurora PostgreSQL 以外の環境では利用できません. たとえば、 ローカル開発環境の docker 上で PostgreSQL を走らせて開発している場合などは、アプリケーションのマイグレーションファイルにマスキングの定義を含めてしまうと動かなくなってしまうので、 まだ運用上の仕組みは考えることがありそうです.
今後、チームへの導入が済んだ段階でノウハウがあれば共有していきたいと考えています.