Snowflake で個人情報の列を部署ごとに伏せる進め方|見せ方を列ごとに表にし、伏せたあとの件数と絞り込みまで確かめる
Snowflake のマスキングポリシーで、メールアドレスや電話番号の列をロールごとに伏せる進め方です。伏せる前に列ごとの見せ方を表にし、セカンダリロールを外してからロールを1つずつ切り替えて結果を見比べます。今回のマスキングポリシーでは ALL のままだとマーケのロールでも元の値が返ったこと、伏せた列で絞り込むと0件になること、ハッシュなら重複を除いた件数は数えられること、タグで伏せた列は DESCRIBE TABLE の policy name が null になることを、試用の Snowflake で確かめた結果で書きます。
目次 クリックで開く
「マーケの担当にも顧客テーブルを使ってほしい。でも、メールアドレスと電話番号は見せたくない」。そう考えて Snowflake で列の値を伏せるとき、確かめたいことは2つあります。伏せたつもりの値が、本当に見えていないか。伏せたあとも、マーケの担当が顧客の数を数えたり、条件で絞り込んだりできるか。
この記事は、Snowflake を使い始めた会社のデータ担当・情シスの方に向けて書きました。列ごとに誰にどこまで見せるかを先に表にし、ロールを1つずつ切り替えて、同じ SELECT の結果を見比べる進め方です。ACCOUNTADMIN で件数を数えただけでは、伏せたはずの値が見えていることに気づけない場面がありました。
試したのは、Snowflake の導入を支援している当社です。試用の環境に、氏名も連絡先も架空の顧客テーブルを作って確かめました。
伏せる前に、列ごとに誰にどこまで見せるかを表にする
列の値をロールごとに伏せるには、マスキングポリシーを使います。マスキングポリシーは列に適用する決まりで、SELECT を実行した人のロールによって、元の値を返すか、伏せた値を返すかを決めます。テーブルに入っているデータそのものは書き換わりません。使えるのは Enterprise Edition 以上です。
作り始める前に、列ごとの見せ方を表にしておきます。見せ方は、そのまま見せる、使い道を残して形を変える、何も残さずに伏せる、の3つに分けました。今回の顧客テーブルは200行で、顧客ID・氏名・メールアドレス・電話番号・都道府県・累計金額の6列です。ロールは、営業・マーケ・個人情報担当の3つを作りました。個人情報担当のロールは、すべての列を元の値で見るロールです。
形を変えるときは、マーケがその列を何に使うかで形を選びます。メールアドレスをハッシュにしたのは、マーケに顧客の数を数えてもらうためです。同じメールアドレスからはいつも同じハッシュができるので、重複を除いた件数は数えられます。固定の *** にすると、どの行も同じ値になるので、数えられません。
マスキングポリシーを作り、列に適用する
マスキングポリシーの中身は CASE 式です。列の値を引数で受け取り、ロールによって返す値を変えます。メールアドレスのマスキングポリシーは、営業か個人情報担当のロールには元の値を返します。マーケのロールには SHA2 のハッシュを、それ以外のロールには *** を返します。
ロールの判定には、IS_ROLE_IN_SESSION という関数を使いました。Snowflake では、1つのセッションで複数のロールが同時に効きます。USE ROLE で選んだロールがプライマリロールで、それと一緒に効くほかのロールがセカンダリロールです。ロールは、別のロールに付与することもできます。ロール A をロール B に付与すると、B を使う人は A の権限も使えます。こうしてできるロールのつながりを、ロールの階層と呼び、A は B の下のロールにあたります。IS_ROLE_IN_SESSION は、指定したロールがプライマリロールかセカンダリロールそのものか、その下のロールなら真を返します。指定したロールが上にあるだけでは、真になりません。ロールの階層まで含めて判定するときは、この関数を使うよう Snowflake は勧めています。ロールの名前は、Snowflake に保存されているとおりの大文字・小文字で書きます。
電話番号のマスキングポリシーは、マーケのロールには、***-****- と下4桁をつないだ値を返します。この式は Snowflake のドキュメントの例には無く、今回作ったものです。氏名のマスキングポリシーは、営業と個人情報担当のロールにだけ元の値を返し、ほかのロールには 顧客**** を返します。氏名の列には、タグを通して適用しました。
返す値の型は、伏せる列の型と同じにします。文字列の列なら *** を返せますが、数値や日付の列には文字列を返せません。型が合わないと、マスキングポリシーを作る時点でエラーになります。数値の列を伏せるなら、-1 や NULL を返すのがドキュメントの例です。
列に適用するには、ALTER TABLE … MODIFY COLUMN … SET MASKING POLICY を実行します。1つの列に適用できるマスキングポリシーは1つだけです。別のマスキングポリシーに替えるときは、SET MASKING POLICY に FORCE を付けて、1つの文で替えます。UNSET で外してから SET で適用し直すと、2つの文のあいだは列が伏せられていません。その間に値が見えてしまうおそれがあると、Snowflake は注意しています。FORCE で替える場合も、替えたあとの条件によっては、それまで見えていた人が見えなくなることがあります。中身だけを直すなら、ALTER MASKING POLICY の SET BODY で、列に適用したまま書き換えられます。
マスキングポリシーを作るには、スキーマの CREATE MASKING POLICY 権限が要ります。列に適用するには、アカウントの APPLY MASKING POLICY 権限が要り、既定では ACCOUNTADMIN が持っています。マスキングポリシーごとに APPLY 権限を付与して、テーブルの持ち主のロールに適用を任せることもできます。今回は、作るのも適用するのも ACCOUNTADMIN で行いました。
セカンダリロールを外してから、ロールを1つずつ切り替えて結果を見比べる
マスキングポリシーを適用したら、ロールを1つずつ切り替えて、同じ SELECT を実行します。今回は、確かめに使ったユーザーに3つのロールを付与し、USE ROLE で切り替えました。切り替えるたびに、続けて USE SECONDARY ROLES NONE; を実行し、セカンダリロールを外します。こうすると、効くのは USE ROLE で選んだロールだけになります。
セカンダリロールが外れたかは、CURRENT_SECONDARY_ROLES() という関数で見られます。外したあとの結果は {"roles":"","value":""} で、roles と value がどちらも空でした。営業とマーケのロールで同じ SELECT を実行した結果が、次の画面です。
営業のロールでは、氏名もメールアドレスも電話番号も、元の値が返りました。マーケのロールでは、氏名が 顧客****、メールアドレスがハッシュでした。電話番号は、***-****-0001 のように下4桁だけです。個人情報担当のロールでも、メールアドレスと電話番号は、営業と同じく元の値でした。行数は、どのロールでも200行です。
ACCOUNTADMIN で顧客ID 1 のメールアドレスと電話番号を確かめると、どちらも *** でした。ACCOUNTADMIN は、このテーブルを作ったので、テーブルの持ち主です。テーブルの持ち主でも、元の値を返す条件にそのロールを書いていなければ、伏せた値を見ることになります。
新しく作ったロールは、作った直後はどのユーザーにもどのロールにも付与されていません。また、ロールを作った側はそのロールの持ち主になりますが、持ち主になっても権限までは受け継ぎません。今回の3つのロールは ACCOUNTADMIN で作ったので、ロールの持ち主は ACCOUNTADMIN になります。3つのロールは、確かめに使ったユーザーにだけ付与しました。そのため、セカンダリロールを外した ACCOUNTADMIN は、どの条件にも当てはまらなかったと見られます。
Snowflake は、自分で作ったロールのうち、いちばん上のロールを SYSADMIN に付与するよう勧めています。この勧めを今回に当てはめると、3つのロールをそれぞれ SYSADMIN に付与することになる、と当社は読んでいます。ACCOUNTADMIN は SYSADMIN を含むので、営業・マーケ・個人情報担当の3つとも、ACCOUNTADMIN の下に入ります。IS_ROLE_IN_SESSION は階層も見るため、ACCOUNTADMIN にも元の値が返るはずです。この組み方は試していません。ACCOUNTADMIN に元の値を見せるかどうかは、ロールをどこに付与するかとあわせて決めます。
セカンダリロールが ALL のままだと、マーケのロールでも元の値が返った
次に、同じマーケのロールで USE SECONDARY ROLES ALL; を実行してから、SELECT を実行し直しました。メールアドレスも電話番号も、元の値が返りました。ACCOUNTADMIN でも同じです。
ALL にすると、ユーザーに付与されているロールが、すべてセカンダリロールとして効きます。ユーザーの設定 DEFAULT_SECONDARY_ROLES の既定(何も指定しないときの値)も ALL です。何も変えていなければ、ログインした時点でセカンダリロールは ALL になっています。確かめに使ったユーザーには、営業と個人情報担当のロールも付与してあります。そのため、マーケのロールに切り替えていても、元の値を返す条件に当てはまったと見られます。
CASE 式は、上から順に条件を調べ、最初に当てはまった値を返します。今回のマスキングポリシーは、元の値を返す条件を先に書いているので、この結果になりました。この書き順だと、営業とマーケの両方のロールを持つ人は、ALL のままならマーケのロールに切り替えても元の値を見ます。マーケの条件を先に書けば、ALL のままでもハッシュが返るはずです。ただしその書き順では、ALL のままなら、同じ人が営業のロールに切り替えてもハッシュを見るはずです。どちらの書き順でも、ALL のままでは、ロールごとの見え方は確かめられません。
振り返ると、マスキングポリシーを適用した直後にも、ACCOUNTADMIN で行数と重複を除いたメールアドレスの件数を数えていました。このときの件数は200件で、*** に伏せられていれば1件のはずでした。セカンダリロールは既定のままでした。ALL の ACCOUNTADMIN で元の値が返った結果と合うので、営業と個人情報担当のロールが効いていたと見られます。
手順ごとの結果は、記録用のテーブルに書き込んで並べました。次の画面はその一部です。STEP は、手順の番号と、ロール・セカンダリロールの組み合わせです。SEC は、CURRENT_SECONDARY_ROLES() の結果です。ALL の2行は、ロールの名前が入るので灰色で隠しています。
EMAIL_1 と PHONE_1 は、顧客ID 1 のメールアドレスと電話番号です。EMAIL_DISTINCT と PHONE_DISTINCT は、重複を除いた件数です。WHERE_EMAIL_ROWS は、元のメールアドレスで絞り込んだ件数です。ROWS_N は行数です。STEP の文字は、画面の列の幅で途中までしか出ていません。4 と 6 は ACCOUNTADMIN の NONE と ALL です。
利用者ごとに、付与されたロールの組み合わせで確かめる方法は、行アクセスポリシーで行の見え方を確かめた記事に書きました。行アクセスポリシーは、ロールによって見える行を分ける仕組みです。そちらでは、条件をプライマリロールの名前だけを返す CURRENT_ROLE で書いたときの違いも扱っています。
伏せた列で絞り込むと、エラーにならずに0件が返る
マーケのロールで、元のメールアドレスを条件にして WHERE EMAIL = 'user0001@example.com' で絞り込みました。結果は0件です。営業と個人情報担当のロールでは1件でした。エラーは出ませんでした。
マスキングポリシーは、SELECT に並べた列だけでなく、WHERE や JOIN の条件、GROUP BY に書いた列にもかかります。マーケのロールで上の WHERE を実行すると、列の値はハッシュに置き換わります。Snowflake は、そのハッシュを、WHERE に書いた「user0001@example.com」という文字列と比べます。一致する行は無いので、0件になります。伏せた列を条件に使って、元の値を探り当てられないようにするための動きです。
この例では、列の側だけが伏せた値になり、WHERE に書いた値は伏せられません。Snowflake のドキュメントは、この比べ方を避けるべき書き方として挙げています。マーケのロールで0件が返っても、該当する顧客がいないとは限りません。まず、伏せた列を条件にしていないかを見ます。
重複を除いた件数は、伏せ方によって変わりました。ハッシュにしたメールアドレスは、マーケのロールでも200件のままでした。*** で伏せた値を見る ACCOUNTADMIN では、1件です。マーケに顧客の数を数えてもらう列は、*** ではなくハッシュで伏せます。
電話番号の重複を除いた件数も、200件でした。今回のデータは、下4桁がすべて違う番号で作ってあるためです。下4桁が同じ番号は、伏せると同じ値になるので、件数が減ります。
マーケに絞り込みやほかのテーブルとの結合をさせたいなら、伏せていない列を使います。今回なら、顧客ID や都道府県です。顧客ID を伏せずに残しておけば、ほかのテーブルとも顧客ID で結合できます。メールアドレスのハッシュで結合することも考えられます。ただ Snowflake は、JOIN に使う値をハッシュで伏せないよう勧めています。別の値が同じハッシュになるおそれがある、とも注意しています。
タグで伏せた列は、DESCRIBE TABLE ではマスキングポリシーが無いように見える
個人情報の列が多いと、1列ずつマスキングポリシーを適用するのは手間で、適用し忘れる列が出るおそれもあります。そこで使えるのがタグです。タグにマスキングポリシーを割り当てておき、伏せたい列にはそのタグを設定します。この形を、タグベースのマスキングポリシーと呼びます。これも Enterprise Edition 以上で使えます。
タグに割り当てたマスキングポリシーは、型の合う列に効きます。テーブルにタグを設定すれば、その中の型の合う列がまとめて伏せられます。スキーマやデータベースに設定すれば、あとから作るテーブルの列にも効きます。1つのタグに割り当てられるマスキングポリシーは、型ごとに1つです。同じ列に、直接適用したものとタグを通したものの両方があれば、直接適用したほうが優先されます。
今回は、PII_NAME というタグを作り、氏名のマスキングポリシーを割り当てました。そのタグを氏名の列に設定してから、DESCRIBE TABLE を実行した結果が次の画面です。
policy name の列には、メールアドレスと電話番号のマスキングポリシーの名前が出ました。氏名は null で、伏せていない列と見分けがつきません。タグを通して伏せた列は、DESCRIBE TABLE では policy name が null になる仕様です。
どの列にどのマスキングポリシーがかかっているかは、POLICY_REFERENCES というテーブル関数で一覧にできます。Information Schema にある関数で、テーブルの名前を渡すと、列ごとにマスキングポリシーの名前が出ます。タグを通している列には、タグの名前も出ます。
氏名の行には、TAG_NAME に PII_NAME が入りました。POLICY_STATUS は、3つの列とも ACTIVE です。このテーブル関数の結果は、実行したロールの権限によって変わります。アカウント全体を見るなら、SNOWFLAKE.ACCOUNT_USAGE の POLICY_REFERENCES ビューもあります。ただし、反映が最大2時間遅れます。適用した直後に確かめるなら、テーブル関数のほうを使います。
今回の SQL と、試していないこと
環境は、試用の Snowflake(Enterprise Edition、AWS の東京リージョン)です。2026年10月に試し、ウェアハウスは X-Small を使いました。顧客テーブルの200行は、行を作る関数 GENERATOR で作りました。金額も、顧客ID から計算した値です。
下の SQL は、マスキングポリシーとタグの部分と、確かめる部分を抜き出したものです。名前は置き換えています。データベースは mydb、スキーマは crm です。ロールは、営業が CRM_SALES、マーケが CRM_MKT、個人情報担当が CRM_PII です。3つのロールには、ウェアハウス・データベース・スキーマの USAGE と、テーブルの SELECT を付与してあります。本番とは別のデータベースで試してください。
USE SCHEMA mydb.crm;
-- メールアドレス:営業・個人情報担当は元の値、マーケはハッシュ、それ以外は ***
CREATE OR REPLACE MASKING POLICY MASK_EMAIL AS (VAL STRING) RETURNS STRING ->
CASE WHEN IS_ROLE_IN_SESSION('CRM_SALES') OR IS_ROLE_IN_SESSION('CRM_PII') THEN VAL
WHEN IS_ROLE_IN_SESSION('CRM_MKT') THEN SHA2(VAL)
ELSE '***' END;
-- 電話番号:マーケは下4桁だけ(この式はドキュメントの例ではない)
CREATE OR REPLACE MASKING POLICY MASK_PHONE AS (VAL STRING) RETURNS STRING ->
CASE WHEN IS_ROLE_IN_SESSION('CRM_SALES') OR IS_ROLE_IN_SESSION('CRM_PII') THEN VAL
WHEN IS_ROLE_IN_SESSION('CRM_MKT') THEN '***-****-' || RIGHT(VAL, 4)
ELSE '***' END;
ALTER TABLE CUSTOMERS MODIFY COLUMN EMAIL SET MASKING POLICY MASK_EMAIL;
ALTER TABLE CUSTOMERS MODIFY COLUMN PHONE SET MASKING POLICY MASK_PHONE;
-- 氏名:タグにマスキングポリシーを割り当て、列にタグを設定する
CREATE OR REPLACE TAG PII_NAME;
CREATE OR REPLACE MASKING POLICY MASK_NAME AS (VAL STRING) RETURNS STRING ->
CASE WHEN IS_ROLE_IN_SESSION('CRM_SALES') OR IS_ROLE_IN_SESSION('CRM_PII') THEN VAL
ELSE '顧客****' END;
ALTER TAG PII_NAME SET MASKING POLICY MASK_NAME;
ALTER TABLE CUSTOMERS MODIFY COLUMN NAME SET TAG PII_NAME = 'name';
-- ロールごとに確かめる(セカンダリロールを外してから)
USE ROLE CRM_MKT;
USE SECONDARY ROLES NONE;
SELECT CUSTOMER_ID, NAME, EMAIL, PHONE, PREFECTURE, TOTAL_AMOUNT FROM mydb.crm.CUSTOMERS ORDER BY CUSTOMER_ID LIMIT 5;
-- どの列にかかっているか(タグを通した列も出る)
USE ROLE ACCOUNTADMIN;
SELECT REF_COLUMN_NAME, POLICY_NAME, POLICY_KIND, TAG_NAME, POLICY_STATUS
FROM TABLE(mydb.INFORMATION_SCHEMA.POLICY_REFERENCES(REF_ENTITY_NAME => 'mydb.crm.CUSTOMERS', REF_ENTITY_DOMAIN => 'table'))
ORDER BY REF_COLUMN_NAME;
試し終えたあと、データベースと3つのロールは削除しました。
今回の結果は、1回の検証で見たものです。絞り込みは WHERE で試しただけで、JOIN と GROUP BY は試していません。ほかにも、条件付きマスキング(ほかの列の値で伏せ方を決める方法)、ビューやクローンでの扱い、FORCE での差し替えは試していません。ロールを SYSADMIN に付与したときの結果や、Snowsight の画面での確かめ方も見ていません。
料金への影響も見ていません。読んだドキュメントのページに、マスキングポリシー専用の料金は書かれていませんでした。Snowflake の料金の仕組みは、料金とコストの記事で説明しています。
マーケに渡す前に、マーケのロールで SELECT を3つ実行する
マーケに渡す前に、まず列ごとの見せ方の表を作ります。顧客や会員のテーブルから個人情報の列を書き出し、ロールごとに、そのまま見せるか、使い道を残して形を変えるか、何も残さずに伏せるかを決めておきます。
表ができたら、冒頭の2つの問いを確かめます。マーケのロールで SELECT を3つ実行し、比べるための件数を個人情報担当のロールで1回数えると、答えが出ます。どれも、先に USE SECONDARY ROLES NONE; を実行します。CURRENT_SECONDARY_ROLES() の roles が空になったのを見てから、SELECT を実行します。
1つ目は、伏せた列を数行だけ表示し、元の値が出ていないかを見ます。2つ目は、COUNT(DISTINCT …) で重複を除いた件数を数え、個人情報担当のロールでの件数と比べます。ハッシュにした列なら、同じ件数になっているかを見ます。3つ目は、マーケがふだん使う絞り込みの条件で件数を数え、0件になっていないかを見ます。
どのロールに何を見せるかは、ロール階層の設計と一緒に決めることになります。ロール階層の設計は、当社の Snowflake 導入支援では「現状整理とアーキテクチャ設計」の工程に入っています。この工程だけを頼むこともできます。
データ分析・データ基盤支援
Snowflake・BigQuery・Treasure Dataといったデータ基盤の選定から、基幹・SaaS・広告・POSに散らばったデータの集約、dbtでの指標統一、Lookerなどでのダッシュボード化までを支援します。
