💽

本番環境のAlloyDBをPostgreSQL Anonymizerでデータマスキングしました

2024/07/26に公開

はじめに

株式会社CastingONEでバックエンドエンジニアをしている村上です。

弊社では、セキュリティの観点から本番DBにアクセスできる人を一部の開発者に限定してきたのですが、システムの規模が大きくなるにつれて、不具合調査や問い合わせ対応が追いつかなくなってきました。

そこで、ユーザー様の個人情報を適切にデータマスキングすることを前提に、他の開発者やCSもアクセスできる仕組みを作りました。

今回は、そのときのことを下記の観点でまとめます。
何かの参考になれば嬉しいです。

  • 権限の設計をどんな感じにしたか
  • どんな項目をマスキングしたか
  • 静的データマスキングと動的データマスキング、どちらにしたか
  • AlloyDB、PostgreSQLでデータマスキングする方法

権限の設計をどんな感じにしたか

個人情報をデータマスキングすると言っても、マスキング前の元データを見れる人が誰もいない体制にすると支障が出てしまいます(個人情報を含むカラムに関係する不具合や問い合わせもあるため)

そのため、権限を2つに分けることにしました。

  1. 強い権限
  2. 弱い権限

強い権限

強い権限を持つ人は、マスキング前の元データを見ることができます。基本的にはチームリーダーがこの権限を持ちます。

弱い権限

弱い権限を持つ人は、マスキングされたデータを見ることができます。チームリーダー以外の開発者やCSがこの権限を持ち、個人情報がマスキングされた安全な環境でデータの調査や分析を行えます。

これにより、不具合調査や問い合わせ対応をチームリーダー以外に分散させることができ、冒頭で触れた「対応が追いつかない」問題をかなり解消することができました。

ただし、個人情報を含むカラムに関係する調査はできないため、強い権限を持つ人に代理でSQLを実行してもらう体制にしました。

どんな項目をマスキングしたか

マスキングする項目の取捨選択が思いのほか難しく、社内で意見を戦わせた結果、個人情報を含むテーブルにおける文字列型カラムをすべてマスキングする 方針に落ち着きました。

例えば、下記の学生テーブルがあった場合、nameが個人情報にあたるのでmemoとevaluationを含めて文字列型カラムをすべてマスキングします。

nameは間違いなく個人情報ですが、memoやevaluationについては人によって解釈が異なり、カラムを一つずつ精査するのは運用的に負担がかかるという判断です。

データの調査や分析で文字列型のカラムを使うことは比較的少ないので、「深く考えずに全部マスキングしてしまえ」という方針になりました。

逆に、id、class_id、created_at、updated_atは文字列型ではないためマスキングしません。テーブル間の関連を表すid系のカラムや日付型のカラムは調査で使うことが多いため、そのまま残すようにしました。

静的データマスキングと動的データマスキング、どちらにしたか

※静的と動的の違いについては、下記ページを参照してください。
https://aws.amazon.com/jp/what-is/data-masking/

下記の理由で、動的データマスキングを選びました。

  • リアルタイムのデータが見れる
  • 実装コストが小さい

静的データマスキングにした場合、1.本番DBのデータをマスキング用DBにコピーする -> 2.マスキング用DBのデータを静的データマスキングする のような工程を踏むため、2の後で更新された本番DBのデータはマスキング用DBに反映されません(再び同じ工程を踏むまでは)

また、動的データマスキングの場合は本番DBの設定を変更するだけで終わりますが、静的データマスキングの場合はマスキング用DBを立ち上げて、上記1~2の工程を自動化するためのコストがかかります。

以上のことを考慮して、今回の用途では動的データマスキングがマッチしていると判断しました。

AlloyDB、PostgreSQLでデータマスキングする方法

PostgreSQL Anonymizer

動的データマスキングをするために、PostgreSQL Anonymizer を利用しています。
https://postgresql-anonymizer.readthedocs.io/en/stable/

弊社のシステムではGoogle CloudのAlloyDBを使っているのですが、そちらでもサポートされています。

google_cloud_docs

データマスキングの設定

データマスキングの設定はDDLで行います。

下記は弊社でテンプレートにしているマスキングルールで、該当カラムにデータが入っている場合のみ CONFIDENTIAL という固定値でマスキングし、NULLや空文字の場合はそのままにします。

SECURITY LABEL FOR anon ON COLUMN table.column IS 'MASKED WITH VALUE CASE WHEN column <> '''' THEN ''CONFIDENTIAL'' ELSE column END';

先ほどのstudentsテーブルを例にすると、name、memo、evaluationをマスキングする方針になっていたため、下記のようになります。DDLを実行すると、PostgreSQL Anonymizerが管理しているViewに反映されます。

SECURITY LABEL FOR anon ON COLUMN students.name IS 'MASKED WITH VALUE CASE WHEN name <> '''' THEN ''CONFIDENTIAL'' ELSE name END';
SECURITY LABEL FOR anon ON COLUMN students.memo IS 'MASKED WITH VALUE CASE WHEN memo <> '''' THEN ''CONFIDENTIAL'' ELSE memo END';
SECURITY LABEL FOR anon ON COLUMN students.evaluation IS 'MASKED WITH VALUE CASE WHEN evaluation <> '''' THEN ''CONFIDENTIAL'' ELSE evaluation END';
CREATE OR REPLACE VIEW mask.students
 AS
 SELECT students.id,
    students.class_id,
    CASE
        WHEN students.name <> '' THEN 'CONFIDENTIAL'
        ELSE students.name
    END AS name,
    CASE
        WHEN students.memo <> '' THEN 'CONFIDENTIAL'
        ELSE students.memo
    END AS memo,
    CASE
        WHEN students.evaluation <> '' THEN 'CONFIDENTIAL'
        ELSE students.evaluation
    END AS evaluation,
    students.created_at,
    students.updated_at
   FROM students;

このViewは mask というスキーマにあり、弱い権限の人にはこのスキーマへの参照権限のみを付与することで、下記のようにSQLの結果がデータマスキングされます。

select * from mask.students;
+----+----------+--------------+------+--------------+------------------------+------------------------+
| id | class_id | name         | memo | evaluation   | created_at             | updated_at             |
|----+----------+--------------+------+--------------+------------------------+------------------------|
| 1  | 1        | CONFIDENTIAL |      | CONFIDENTIAL | 2020-07-01 19:15:09+09 | 2022-11-29 15:44:35+09 |
+----+----------+--------------+------+--------------+------------------------+------------------------+

強い権限の人には従来のスキーマへの参照権限を付与することで、マスキングされていない元データを参照できます。

select * from hoge.students;
+----+----------+--------------+------+--------------+------------------------+------------------------+
| id | class_id | name         | memo | evaluation   | created_at             | updated_at             |
|----+----------+--------------+------+--------------+------------------------+------------------------|
| 1  | 1        | Taro Yamada  |      | Great        | 2020-07-01 19:15:09+09 | 2022-11-29 15:44:35+09 |
+----+----------+--------------+------+--------------+------------------------+------------------------+

パフォーマンスへの影響

弱い権限を持つ人はmaskスキーマのViewを経由して検索するため、マスキングルールの内容によってはクエリの実行時間が長くなります。

例えば、studentsテーブルに学籍番号という一意制約のあるカラム(下表の下から3つ目)を追加して、一意制約を維持したままマスキングする必要があるとします。

そのような場合、弊社ではAnonymizerのhash関数を使っています。下記1つ目がマスキングルールのDDLで、2つ目がそれを反映させたViewの定義です。

SECURITY LABEL FOR anon ON COLUMN students.student_no IS 'MASKED WITH VALUE CASE WHEN student_no <> '''' THEN anon.hash(student_no) ELSE student_no END';
CREATE OR REPLACE VIEW mask.students
 AS
 SELECT students.id,
    students.class_id,
    CASE
        WHEN students.name <> '' THEN 'CONFIDENTIAL'
        ELSE students.name
    END AS name,
    CASE
        WHEN students.memo <> '' THEN 'CONFIDENTIAL'
        ELSE students.memo
    END AS memo,
    CASE
        WHEN students.evaluation <> '' THEN 'CONFIDENTIAL'
        ELSE students.evaluation
    END AS evaluation,
    CASE
        WHEN students.student_no <> '' THEN anon.hash(students.student_no)
        ELSE students.student_no
    END AS student_no,
    students.created_at,
    students.updated_at
   FROM students;

student_noを使うクエリを実行すると、1レコードずつhash関数を実行するからだと思いますが、パフォーマンスへの影響が顕著に見られました。

蛇足ですが、アプリケーションはmaskスキーマではなく従来のスキーマを参照するので、パフォーマンスへの影響はないと判断していますし、実際に影響は出ていません。

おわりに

というわけで、今回は本番環境のDBを動的データマスキングした取り組みをまとめてみました。

「こうしたらもっと便利だよ!」などご意見があれば、ぜひ教えて下さい。

弊社でいっしょに働いてくれるエンジニアを募集中です。社員でもフリーランスでも、フルタイムでも短時間の副業でも大歓迎なので、気軽にご連絡ください!

https://www.wantedly.com/projects/1130967

https://www.wantedly.com/projects/768663

Discussion