Amazon Redshift で ダイナミックデータマスキング

顧客機微データ（PIIやPCI）をRedshift内で扱う場合の対処法には色々対策が想定される

例）
- クラスタの暗号化（AWS KMS）
- [行レベル](https://aws.amazon.com/jp/about-aws/whats-new/2022/07/amazon-redshift-row-level-security/)セキュリティや[列レベル](https://aws.amazon.com/jp/blogs/news/achieve-finer-grained-data-security-with-column-level-access-control-in-amazon-redshift/)セキュリティで対象データへのアクセス制御
- 対象データをマスクしたビューを作成
- 対象データをハッシュ化
- 対象データを[トークン化](https://docs.aws.amazon.com/ja_jp/redshift/latest/mgmt/data-tokenization.html)
- ダイナミックデータマスキング
- など

ここでは、ダイナミックデータマスキングを試してみる

# ダイナミックデータマスキングとは？

権限のないユーザに対してのみ、機密データをマスクした状態でしか見せないようにすること。
具体的には、ポリシーに従って電話番号やメールアドレスなど個人情報を＊＊＊とマスクして表示させるなどのイメージ。
そもそも見せたくない列や行ごと固定的に非表示にする方法（列レベル/行レベルセキュリティ）でも良いユースケースが多そうだが、生データは見せたくないけどJOINや集計には使いたい場合など、生ではない状態でも一意性を識別したい場合などが想定される。

# どうやる？

- その1. ビルトイン機能としてサポートされた方法 2022/12時点プレビュー
https://docs.aws.amazon.com/redshift/latest/dg/t_ddm.html
詳細な使い方はこちら
https://qiita.com/suzukihi724/items/12822a1e14f1cc1d31d1


- その2. 公式GitHubで公開されているUDFを利用した方法（上記がGAになれば上が推奨）

以下はその２の方法

# 実現方法
その２のGitHubのaws-samplesで紹介されている方法を試してみる
https://github.com/aws-samples/amazon-redshift-dynamic-data-masking#masking-privileges

### ざっくりした仕組み
- マスキング処理はPython UDF（f_mask_varchar）を利用
- 生データはtableで持ち、ユーザによってマスクされた情報を動的にViewで出し分ける
- 誰にどのような方法で見せるかのポリシーは、管理テーブル（user_entitle）で設定

### 細かい要件

以下の要件を満たす実装としている。

- マスキング ルールは、タイプ (電子メール、SSN、汎用、生年月日など) に基づいて列ごとに異なる場合がある。各列にタイプのタグを付けて、使用するルールを適応できる。
- マスキング権限はユーザー レベルで割り当てることができ、ユーザーがアクセスできるすべての DB オブジェクトに適用できる。ただし、DB オブジェクトへのアクセスは引き続き DB ユーザー/グループ レベルで制御される。フィールドが PII としてタグ付けされている場合、次の権限が含まれる。
    - FullMask – データは難読化されて返されますが、元の値を特定することはできない。
    - PartialMask – 入力値の一部はマスクされますが、一部はマスクされない。
    - NoMasking – 入力値が返される。
    - 未定義 - ユーザーには上記の権限が割り当てられておらず、NULL が返される。
- クエリのパフォーマンスは、元のパフォーマンスにできるだけ近づける。
- アプリケーションへの影響は最小限に抑える。
- コンプライアンス、監査、および管理が実施されており、監査人は次の人物を決定できる。
    - 修正されたマスキング ルール
    - 変更されたマスキング権限
    - マスクされたデータ

### やってみた　〜セットアップ

**1) RedshiftにUDF関数作成**

あらかじめRedshift Serverless（ProvisionedでもOK）を準備。
AWS Cloud9などでGitHubからクローンする
```git clone https://github.com/aws-samples/amazon-redshift-dynamic-data-masking.git```

UDF作成用のpythonコードがあるので実行する

```python
pip3 install redshift_connector
python3 create_udfs.py
# 以下を入力
Cluster Host:
Database Name:
User:
Port:
Password:
```

**2) サンプル生データ登録**

Query Editor v2などを使って、Redshiftにクエリ実行。
サンプル生データテーブル作成

```SQL
create table public.customer_raw(id int, first_name varchar(100), last_name varchar(100), login varchar(100), email_address varchar(100));
insert into public.customer_raw values
 (1,'Jane','Doe','jdoe','jdoe@org.com'),
 (2,'John','Doe','jhndoe','jhndoe@org.com'),
 (3,'Edward','Jones','ejones','ejones@org.com'),
 (4,'Mary','Contrary','mcontrary','mcontrary@org.com');
```

**3) データマスク用ビュー作成**

生データをそのまま見せるのではなく、権限に応じてUDFでマスク処理を動的に適応したビューを生成する

```SQL
create or replace view public.customer as (
  select c.id,
    f_mask_varchar(c.first_name, 'name', e.priv) first_name,
    f_mask_varchar(c.last_name, 'name', e.priv) last_name,
    f_mask_varchar(c.login, 'login', e.priv) login,
    f_mask_varchar(c.email_address,'email', e.priv) email
  from  public.customer_raw c
  left join public.user_entitle e on (current_user = e.username)
) with no schema binding;
```

生データのテーブルと、マスキングポリシーの権限管理テーブル（user_entitle）とのJOINで、かつuser_entitleテーブルの権限設定（priv）に応じて、各データの表示形式をUDFで切り替える


**4) ビューから呼ばれるUDFを定義**

```SQL
create or replace function f_mask_varchar (varchar, varchar, varchar)
  returns varchar
immutable
as $$
  select case
    when $3 is null then null
    when $3 = 'N' then $1
    when $3 = 'F' then md5($1)
    else case $2
      when 'ssn' then substring($1, 1, 7)||'xxxx'
      when 'email' then substring(SPLIT_PART($1, '@', 1), 1, 3) + 'xxxx@' + SPLIT_PART($1, '@', 2)
      else substring($1, 1, 3)||'xxxxx' end
    end
$$ language sql;
```

処理内容）
- 未登録ユーザの場合は NULL
- N (NoMasking）：マスクしない（そのまま）
- F（FullMask）：md5でハッシュ化　
- nullの場合；NULLを返却
- その他：PartialMaskの扱いで部分的にマスク処理

ここは要件に応じて好きに変えれば良い。（例えばmd5をSHA1やその他に変えるなど）

**5) 権限管理テーブル作成**

```
create table public.user_entitle (username varchar(25), priv varchar(1));
```

テスト用に複数の権限のユーザを作成して登録

```SQL
create user u_fullmask password disable;
create user u_partialmask password disable;
create user u_nomask password disable;
create user u_newuser password disable;

grant select on customer to u_fullmask;
grant select on customer to u_partialmask;
grant select on customer to u_nomask;
grant select on customer to u_newuser;

grant select on user_entitle to u_fullmask;
grant select on user_entitle to u_partialmask;
grant select on user_entitle to u_nomask;
grant select on user_entitle to u_newuser;

insert into public.user_entitle values
 ('u_fullmask', 'F'),
 ('u_partialmask', 'P'),
 ('u_nomask', 'N');
```

- u_fullmaskユーザには全マスク
- u_partialmaskユーザには一部マスク
- u_nomaskユーザにはマスクしない

### 動作確認

### 全マスクユーザの場合

```
SET SESSION AUTHORIZATION 'u_fullmask';
select * from customer;
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112537/19d19a42-ac44-9bb7-b9b5-5700a9a66e53.png)

全部ハッシュ化される

### 一部マスク対象ユーザの場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112537/a09a51fd-fb69-231d-8e01-3fdc595148ff.png)

一部がxxxxに
※どこまでどうマスクするかは、f_mask_varcharで自由に変えればよい

### 全表示対象ユーザの場合


![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112537/8a23ea5e-8bed-b040-1b0a-79ac327408b8.png)

マスクされない

### 権限管理テーブル（user_entitle）未登録ユーザの場合

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/112537/3e194b97-b20c-4d72-cc0c-9b409041adda.png)

NULLで表示

# まとめ

UDFとビューを使って、どのユーザに、どのカラムを、どのような表示方式（マスク）かを動的に切り替えて表示させるダイナミックデータマスキングを割と簡単に実装できた。
特に自動的に氏名やメールアドレスを識別するわけではないが、自動検知は検知漏れのリスクも考慮すると、手堅く変換ルールを規定した方が安心できそう。
※ちなみに自動検知も正規表現を組み合わせばある程度実装できそうなのと、Glue/Glue DataBrewにはPII検知機能があるので、それらを使って前処理で対応する方法も考えられる。

要件次第では、列レベル/行レベルのアクセス制御や、単純な一律ハッシュ化、必要カラムのみ表示するビューを作るだけでも良さそうな気がするが、ダイナミックデータマスキングを実装したい場合はこんな実装もできることがわかる。

ちなみに、以下のblogでは、リッチに3rdパーティツールを活用した例も紹介されている
https://aws.amazon.com/jp/blogs/news/protect-and-audit-pii-data-in-amazon-redshift-with-datasunrise-security/