PostgreSQL Anonymizer でデータの匿名化・マスキングをしてみた

この記事は [PostgreSQL Advent Calendaer 2019](https://qiita.com/advent-calendar/2019/postgresql) の 16 日目です。昨日の記事は @masayuki038@github さんの「[PostgreSQL の JIT が生成するコードを眺めてみる](https://masayuki038.github.io/blog/2019/12/15/postgresql-jit/)」でした。この記事では PostgreSQL でデータの匿名化・マスキングを行う拡張 PostgreSQL Anonymizer について紹介します。

# 匿名化・マスキングとは

データベースには個人情報や企業の機密情報など、取り扱いに注意が必要なデータが含まれています。そのようなデータを開発やテスト、分析でそのまま使っては情報漏洩のリスクがあります。そのような場合にデータを安全に取り扱う方法の 1 つに匿名化があります。

匿名化 (anonymization) はデータから個人を特定できる情報を取り除くことです。マスキング (masking) は匿名化の一種で、情報を隠して匿名化することです。ただ、匿名化とマスキングはほぼ同じ意味で使われることが多いようです。

# PostgreSQL Anonymizer とは

[PostgreSQL Anonymizer](https://postgresql-anonymizer.readthedocs.io/en/latest/) は PostgreSQL でデータの匿名化・マスキングを行う拡張です。開発元はフランスの [Dalibo](https://www.dalibo.com/en/) 社です。以前は Ora2Pg の開発者がいたことでも有名でした。ライセンスは PostgreSQL と同じく [PostgreSQL ライセンス](https://gitlab.com/dalibo/postgresql_anonymizer/blob/master/LICENSE.md) です。

PostgreSQL Anonymizer では、PostgreSQL のデータ定義言語 (DDL) を拡張し、データをどのようにマスキングするかのルールを定義します。それにより、データをマスキングして更新したり、SQL 形式でダンプしたり、ユーザにマスキングしたデータをアクセスさせたりできます。

PostgreSQL Anonymizer は PostgreSQL 9.6 以降に対応しています。バージョン 9.5 でも使えなくはないですが、パッケージが提供されておらず、[いくつか問題](https://postgresql-anonymizer.readthedocs.io/en/latest/NOTES/#support-for-postgresql-95) もあります。また、まだ開発途上にあるプロジェクトなので、それを分かった上で使うのがいいでしょう。

# PostgreSQL Anonymizer のインストール

PostgreSQL Anonymizer は PostgreSQL 開発元の Yum リポジトリに含まれているので、CentOS などの Red Hat 系 OS を使っていて、Yum リポジトリを登録済みであれば、簡単にインストールできます。PostgreSQL のインストールについては [こちらの記事](https://qiita.com/tom-sato/items/e1903cb974fb6c6d5664) を参考にしてください。

ここでは、CentOS 8 に PostgreSQL 12 をインストールした状態を前提にします。

まず、PostgreSQL Anonymizer のパッケージ `postgresql_anonymizer12` をインストールします。パッケージ名の最後の数字 `12` は PostgreSQL のバージョンを表します。お使いの PostgreSQL のバージョンに合わせて変更してください。

```shell-session
# yum -y install postgresql_anonymizer12
(省略)
Installed:
  postgresql_anonymizer12-0.5.0-1.rhel8.x86_64
  perl-Carp-1.42-396.el8.noarch
  perl-Exporter-5.72-396.el8.noarch
  perl-libs-4:5.26.3-416.el8.x86_64
  ddlx_12-0.15-1.rhel8.noarch
  postgresql12-contrib-12.1-2PGDG.rhel8.x86_64

Complete!
```

`postgresql_anonymizer12` パッケージをインストールすると、依存関係で `perl` 関連、`ddlx`、`postgresql12-contrib` パッケージがインストールされます。[`ddlx`](https://pgxn.org/dist/ddlx/) パッケージはデータベースオブジェクトから DDL を抽出する拡張です。`postgresql12-contrib` パッケージは PostgreSQL の追加モジュールで、必要なのはそれに含まれる拡張 `tsm_system_rows` です。[`tsm_system_rows`](https://www.postgresql.jp/document/current/html/tsm-system-rows.html) 拡張は最大行数を指定してテーブルからサンプリングする拡張です。`perl` 関連パッケージは追加モジュールの一部が必要としています。

次に、PostgreSQL の設定ファイル `postgresql.conf` を編集し、PostgreSQL サーバの起動時に読み込む共有ライブラリを指定するパラメータ `shared_preload_libraries` に PostgreSQL Anonymizer を表す `anon` をカンマ区切りで追加します。

```config:$PGDATA/postgresql.conf
# - Shared Library Preloading -

shared_preload_libraries = 'anon'       # (change requires restart)
#local_preload_libraries = ''
#session_preload_libraries = ''
#jit_provider = 'llvmjit'               # JIT library to use
```

パラメータの設定を反映させるため、PostgreSQL のサービスを再起動します。

```shell-session
# systemctl restart postgresql-12.service
```

最後に、PostgreSQL Anonymizer のテスト用にデータベース `anondb` を作成して接続し、PostgreSQL Anonymizer の拡張 `anon` をインストールし、`anon.load` 関数でマスキングに使う偽装データをロードします。

```
# su - postgres
Last login: Tue Dec  3 21:10:31 UTC 2019 on pts/0
$ createdb anondb
$ psql anondb
=# CREATE EXTENSION anon CASCADE;
NOTICE:  installing required extension "tsm_system_rows"
NOTICE:  installing required extension "ddlx"
CREATE EXTENSION
=# SELECT anon.load();
SELECT
 load
------
 t
(1 行)

=# \q
```

`CREATE EXTENSION` コマンドの実行時に `CASCADE` を指定するのは、依存関係のある拡張をいっしょにインストールするためです。

これで、PostgreSQL Anonymizer のインストールは完了です。

# マスキングルールの定義

テストデータを生成し、マスキングルールを定義します。

まず、テストデータを生成します。部署と社員情報を格納するテーブル `departments`、`employees` を作成し、データを登録する SQL ファイル `init.sql` を用意します。

```sql:init.sql
CREATE TABLE departments (
    id serial PRIMARY KEY,
    name text NOT NULL
);

CREATE TABLE employees (
    id serial PRIMARY KEY,
    last_name text NOT NULL,
    first_name text NOT NULL,
    email text NOT NULL,
    salary int NOT NULL,
    department_id int NOT NULL REFERENCES departments (id),
    birth date NOT NULL
);

INSERT INTO departments (name) VALUES
    ('営業部'),
    ('技術部');

INSERT INTO employees (last_name, first_name, email, salary, department_id, birth) VALUES
    ('佐藤', '太郎', 'sato@example.com', 300000, 1, '1987-01-02'),
    ('鈴木', '次郎', 'suzuki@example.com', 300000, 2, '1988-03-04'),
    ('高橋', '三郎', 'takahashi@example.com', 350000, 1, '1989-05-06'),
    ('田中', '花子', 'tanaka@example.com', 350000, 2, '1990-07-08'),
    ('伊藤', '雪子', 'ito@example.com', 400000, 1, '1991-09-10'),
    ('渡辺', '月子', 'watanabe@example.com', 400000, 2, '1992-11-12');
```

次に、`init.sql` ファイルを読み込んで実行します。

```shell-session
$ psql -f init.sql anondb
CREATE TABLE
CREATE TABLE
INSERT 0 2
INSERT 0 6
$ psql anondb
=# SELECT * FROM departments;
 id |  name
----+--------
  1 | 営業部
  2 | 技術部
(2 行)

=# SELECT * FROM employees;
 id | last_name | first_name |         email         | salary | department_id |   birth
----+-----------+------------+-----------------------+--------+---------------+------------
  1 | 佐藤      | 太郎       | sato@example.com      | 300000 |             1 | 1987-01-02
  2 | 鈴木      | 次郎       | suzuki@example.com    | 300000 |             2 | 1988-03-04
  3 | 高橋      | 三郎       | takahashi@example.com | 350000 |             1 | 1989-05-06
  4 | 田中      | 花子       | tanaka@example.com    | 350000 |             2 | 1990-07-08
  5 | 伊藤      | 雪子       | ito@example.com       | 400000 |             1 | 1991-09-10
  6 | 渡辺      | 月子       | watanabe@example.com  | 400000 |             2 | 1992-11-12
(6 行)
```

次に、マスキングルールを定義します。社員の E メールアドレスを格納した列 `email` を `anon.partial_email` 関数でマスキングすようにルールを定義します。

```shell-session
=# SECURITY LABEL FOR anon ON COLUMN employees.email
-#     IS 'MASKED WITH FUNCTION anon.partial_email(email)';
SECURITY LABEL
```

マスキングルールの定義には `SECURITY LABEL` コマンドを使います。`SECURITY LABEL` コマンドはセキュリティラベルを定義するコマンドです。私は SELinux 専用のコマンドだと勘違いしていましたが、ラベルプロバイダを指定することにより、任意のアクセス制御を適用できるそうです。`FOR` の後にはラベルプロバイダとして `anon`、`ON COLUMN` の後にはマスキング対象の列として `employees.email`、`IS` の後にはラベルとしてマスキングルールを指定します。

マスキングルールは `anon.partial_email` 関数を使うというものです。`anon.partial_email` 関数は PostgreSQL Anonymizer が提供するマスキング関数の 1 つで、E メールアドレスのローカル部、ドメインの最初の 2 文字と最後のサブドメインを残して `******` に置き換える関数です。例えば、E メールアドレス `sato@example.com` は `sa******@ex******.com` にマスキングされます。

定義したマスキングルールはシステムビュー `pg_seclabels` で確認できます。また、マスキングルールを削除するには、ラベルに `NULL` を指定して `SECURITY LABEL` コマンドを実行します。

```shell-session
=# SELECT * FROM pg_seclabels;
 objoid | classoid | objsubid | objtype | objnamespace |     objname     | provider |                     label
--------+----------+----------+---------+--------------+-----------------+----------+------------------------------------------------
  16645 |     1259 |        4 | column  |         2200 | employees.email | anon     | MASKED WITH FUNCTION anon.partial_email(email)
(1 行)
```

# インプレース匿名化

インプレース匿名化 (in-place anonymization) はデータをマスキングして更新する機能です。

インプレース匿名化を行うには `anonymize_database` または `anonymize_table`、`anonymize_column` 関数を使います。関数名を見れば分かると思いますが、`anonymize_database` 関数はデータベース内のすべてのテーブル、`anonymize_table` 関数は指定したテーブル、`anonymize_column` は指定したテーブルの列がマスキング対象になります。

```shell-session
=# BEGIN;
BEGIN
=# SELECT anon.anonymize_database();
 anonymize_database
--------------------
 t
(1 行)

=# SELECT * FROM employees;
 id | last_name | first_name |         email         | salary | department_id |   birth
----+-----------+------------+-----------------------+--------+---------------+------------
  1 | 佐藤      | 太郎       | sa******@ex******.com | 300000 |             1 | 1987-01-02
  2 | 鈴木      | 次郎       | su******@ex******.com | 300000 |             2 | 1988-03-04
  3 | 高橋      | 三郎       | ta******@ex******.com | 350000 |             1 | 1989-05-06
  4 | 田中      | 花子       | ta******@ex******.com | 350000 |             2 | 1990-07-08
  5 | 伊藤      | 雪子       | it******@ex******.com | 400000 |             1 | 1991-09-10
  6 | 渡辺      | 月子       | wa******@ex******.com | 400000 |             2 | 1992-11-12
(6 行)

=# ROLLBACK;
ROLLBACK
=# \q
```

なお、インプレース匿名化はデータ自体を更新するので、元のデータが失われてしまいます。また、テーブル内のすべてのデータを更新するので、実行に時間がかかります。後述の [匿名ダンプ](#匿名ダンプ) でデータをマスキングしてダンプし、リストアしなおしたほうが速い場合があります。

# 匿名ダンプ

匿名ダンプ (anonymous dump) はデータをマスキングして SQL 形式でダンプする機能です。

匿名ダンプを行うには `anon.dump` 関数を実行します。ファイルにダンプする場合には、コマンドラインで関数を実行し、その出力をリダイレクトします。その際、余計なメッセージや位置揃え、行以外の出力を抑止するため、`-qAt` オプションを指定します。

```shell-session
$ psql -qAt -c "SELECT anon.dump()" anondb
(省略)
 COPY departments
                              FROM STDIN
                              CSV QUOTE AS '"'
                              DELIMITER ',';
1,営業部
2,技術部
\.

 COPY employees
                              FROM STDIN
                              CSV QUOTE AS '"'
                              DELIMITER ',';
1,佐藤,太郎,sa******@ex******.com,300000,1,1987-01-02
2,鈴木,次郎,su******@ex******.com,300000,2,1988-03-04
3,高橋,三郎,ta******@ex******.com,350000,1,1989-05-06
4,田中,花子,ta******@ex******.com,350000,2,1990-07-08
5,伊藤,雪子,it******@ex******.com,400000,1,1991-09-10
6,渡辺,月子,wa******@ex******.com,400000,2,1992-11-12
\.
```

# 動的マスキング

動的マスキング (dynamic masking) はデータを動的にマスキングし、ユーザにアクセスさせる機能です。元のテーブルと同じ名前のビューを別のスキーマにマスキング関数をかまして定義し、スキーマを切り換えてユーザにアクセスさせることで実現しています。

まず、動的マスキングを有効にするため、`anon.start_dynamic_masking` 関数を実行します。

```shell-session
=# SELECT anon.start_dynamic_masking();
 start_dynamic_masking
-----------------------
 t
(1 行)
```

次に、マスキングしたデータにアクセスさせるユーザ `masked_user` を作成します。

```shell-session
=# CREATE ROLE masked_user LOGIN;
CREATE ROLE
=# SECURITY LABEL FOR anon ON ROLE masked_user IS 'MASKED';
SECURITY LABEL
=# GRANT SELECT ON ALL TABLES IN SCHEMA mask TO masked_user;
GRANT
=# \q
```

`CREATE ROLE` コマンドで `LOGIN` 権限をもったユーザを作成し、`SECURITY LABEL` コマンドで `MASKED` ラベルを与えます。`MASKED` ラベルつきユーザのアクセスはマスキングしたテーブルが格納されたスキーマ `mask` に自動的に切り換えられます。アクセス権限は手動で設定する必要があります。`GRANT` コマンドで `mask` スキーマ内のすべてのテーブルに `SELECT` 権限を与えます。

最後に、`masked_user` ユーザとして `anondb` データベースに接続し、データがマスキングされていることを確認します。

```
$ psql -U masked_user anondb
=> SELECT * FROM employees;
 id | last_name | first_name |         email         | salary | department_id |   birth
----+-----------+------------+-----------------------+--------+---------------+------------
  1 | 佐藤      | 太郎       | sa******@ex******.com | 300000 |             1 | 1987-01-02
  2 | 鈴木      | 次郎       | su******@ex******.com | 300000 |             2 | 1988-03-04
  3 | 高橋      | 三郎       | ta******@ex******.com | 350000 |             1 | 1989-05-06
  4 | 田中      | 花子       | ta******@ex******.com | 350000 |             2 | 1990-07-08
  5 | 伊藤      | 雪子       | it******@ex******.com | 400000 |             1 | 1991-09-10
  6 | 渡辺      | 月子       | wa******@ex******.com | 400000 |             2 | 1992-11-12
(6 行)

=> \q
```

なお、動的マスキングはスキーマの切り換えで実現しているため、マスキング対象のスキーマは 1 つのみに限られます。デフォルトは `public` スキーマで、`anon.start_dynamic_masking` 関数の第 1 引数で指定できます。また、第 2 引数でマスキングしたテーブルを格納するスキーマも指定できます。

---

PostgreSQL Anonymizer の紹介はこれで完了です。今回は紹介しませんでしたが、マスキング関数はほかにも、ノイズ追加やシャッフル化、ランダム化、偽装、部分スクランブル化、一般化、k-匿名化など、[いろいろな関数](https://postgresql-anonymizer.readthedocs.io/en/latest/masking_functions/) があります。なお、人名や都市名などの偽装データは英語しか用意されていませんが、日本語の偽装データを CSV 形式で作成してロードすることもできます。