列レベルセキュリティのドキュメントを読んでみる

アーキテクチャの理解がまぁまぁ進んでホワイトペーパーを読んで「へー」と思える感じになったが、
ガバナンス系の機能について完全に知ったかレベルで「言葉だけ聞いたことがあるな」ぐらい。
知識の幅と深さを広げてみようと思う。妄想と憶測の個人のメモなのはご容赦ください。

公式は以下。
列レベルのセキュリティについて

列レベルセキュリティの下にダイナミックデータマスキング、外部トークン化という単元がある。
今回はこれらを読んでいく。

中央集権的な権限管理

Enterpriseな分野において”職務分掌”という考え方がある。SoD, Segregation of Duties。

職務分掌とは、社内においてそれぞれの役職や部署、個人が取り組む業務を明確にし、責任の所在や業務の権限を整理・配分することです。1人の担当者が2つ以上の職務を重複して持つことで不正を行う機会が発生してしまうことを未然に防ぎ、内部統制やコンプライアンスを実現します。
職務分掌/ポリシー管理

ユーザ・ロールに付与するアクセス権を中央管理できるか否かを表す言葉。

セキュアビューを使うことによってもユーザ・ロールに対して見せる/見せないを制御できるが、
セキュアビューはRBACに基づく権限設定するわけで、所有者に全能の権限変更を許してしまう。
つまり、せっかく閲覧制限しても所有者であれば自力で閲覧可能に出来てしまう。

こういうことがないように、中央集権的な権限管理を行いたいという欲求がある。
“SoD対応”という感じで使われる。ダイナミックデータマスキングと外部トークン化はSoD対応。

見えてはいけないデータをどうやって隠すか?

ざっくり、DBに生データを格納して読み取り時に必要に応じてマスクする方法と、
DBにマスクしたデータを保存して読み取り時に復元する方法がある。
前者はダイナミックデータマスキング、後者は外部トークン化。

どちらを利用するか選択するか、という話ではなく、
ダイナミックデータマスキングを行う際に追加で外部トークン化するか否か、決めるイメージ。

DB内のデータを外部トークン化して物理的に読めない状態としないでも、
生データの読み取り時に動的にマスクすることで要件を達成することはできる。
ニーズに合わせてどちらがより適切か選択する必要があるが、これが結構細かい。
「どちらがより強力か」みたいな簡単な比較にはならない。

外部トークン化は、同一のデータを同一のマスク値に変換する特徴がある。
中身は見えないがカウントを取るとその数は一致する。
対して、ダイナミックデータマスキングは、見えない人には無いものとして扱われるから、
マスク値を使ってグループ化したり、カウントを取ったりすることはできない。

その他、それぞれの比較が以下に書かれている。
ダイナミックデータマスキングまたは外部トークン化の選択

マスキングポリシー

マスキングポリシーはスキーマレベルのオブジェクト。テーブルと独立して定義できる。
要は、保護対象と保護方法が独立していて、保護対象に依存しない形で保護方法を定義する。
クエリの実行時にマスキングポリシーが評価されて、クエリ実行者は適用済みデータを取得する。

ユーザIDとかロールとかによって保護するか否かを制御する内容を書いておく。
ロールAには見えるけど、ロールBには見えない、という結果になる。

公式に実行イメージがあるので貼ってみる。
マスキングポリシーを作ってテーブルのカラムにあてているんだな、とか、
文字列型のカラム値を加工して文字列型の値を返す何かを書くんだな、とか分かる。


-- Dynamic Data Masking
create masking policy employee_ssn_mask as (val string) returns string ->
  case
    when current_role() in ('PAYROLL') then val
    else '******'
  end;

-- Apply Masking Policy to Table column
ALTER TABLE {テーブル名} ALTER COLUMN {カラム名} SET MASKING POLICY employee_ssn_mask

通常、所有者はオブジェクトに対して全権を持つが、マスキングポリシーはアクセス制御と
独立していて、所有者に対してデータを保護することもできる。それぐらい独立している。

「機密を守る」専門の部署を作って、そこで統一的にデータ保護を設計する、
みたいな運用をすることもできる。

UDFsを使ってマスキングポリシーの管理をしやすくする

上のような感じでマスキングポリシーを作ってルールをハードコードし続けると、
いわゆるコードクローンが発生したり、共通化が難しくなるなど、結構面倒なことになる。
こういったケースには、ユーザ定義関数 (User Defined Functions, UDFs)が有効。

例えば、以下のように SHA2 でコードするUDFを定義し、マスキングポリシー内でUDFを
評価することができる。こうしておけば UDFを再利用したりリファクタしたりしやすくなる。


CREATE OR REPLACE FUNCTION .sha2_array(SRC ARRAY)
RETURNS ARRAY
LANGUAGE SQL
AS $$
    ARRAY_CONSTRUCT(SHA2(ARRAY_TO_STRING(SRC, ',')))
$$
;

CREATE OR REPLACE MASKING POLICY .array_mask AS (val ARRAY) RETURNS ARRAY ->
CASE
  WHEN .has_mask_full_access(current_role()) THEN val  -- unmask
  WHEN .has_mask_partial_access(current_role()) THEN .sha2_array(val::ARRAY)::ARRAY  -- partial mask
  ELSE TO_ARRAY('***MASKED***') -- mask
END;

ALTER TABLE IF EXISTS .test MODIFY COLUMN names SET MASKING POLICY .array_mask;

マスキングポリシー条件

上の実行イメージのように、手続き型の処理ではなく、関数評価による宣言的な設定を行う。
上の例ではマスキング対象を判定するcase whenを使用している。
この条件として、条件関数コンテキスト関数、カスタム資格テーブルを利用できると記述がある。

上の例では、current_role コンテキスト関数を使用し、ロールに応じたマスキングを行う。
公式にはコンテキスト関数のうち invoker_role, invoker_share について言及がある。
カスタム資格テーブルはマルチテナントデザインパターンのホワイトペーパーに出てきた。
ユーザID、またはロールIDと、それらが何のリソースにアクセスできるかを1つのテーブルに
したもの。例えば current_role に対して該当列をマスクするか否かを自前のテーブルを
使って表現できる。深読みしないでおく..

セキュアビューが増えてしまう代替案としてマスキングポリシーを使う

セキュアビューを利用することによってユーザ・ロールに対する行レベルセキュリティを実現できる。
(列レベルセキュリティではないが、選択的にデータを見せるという意味で同列)


CREATE TABLE widgets (
    id NUMBER(38,0) DEFAULT widget_id_sequence.nextval,
    name VARCHAR,
    color VARCHAR,
    price NUMBER(38,0),
    created_on TIMESTAMP_LTZ(9));
CREATE TABLE widget_access_rules (
    widget_id NUMBER(38,0),
    role_name VARCHAR);
CREATE OR REPLACE SECURE VIEW widgets_view AS
    SELECT w.*
        FROM widgets AS w
        WHERE w.id IN (SELECT widget_id
                           FROM widget_access_rules AS a
                           WHERE upper(role_name) = CURRENT_ROLE()
                      )
    ;

BI用途が例として上がっているが、ビューからビューを作って、その先のビューを..
といったようにビューは大量に作られることが常なので、セキュアビューだけによって
ユーザ・ロールに対する閲覧制限をかけるというのはかなり大変なことになる。

テーブル・ビューなどのオブジェクトと独立して定義できるマスキングポリシーなら、
どれだけBIが複雑になっても確かに管理が容易になりそう。

マスキング対象列からマスキング非対象列へのコピー

結構ハマったのでメモ。ハマって苦しんだ時何度読んでも理解できなかった…。
考慮事項

ダイナミックデータマスキングが設定された列の値を使うとき、
使う人が非マスク値(生データ)を閲覧できる場合は生データをコピーしてしまう。
生データが閲覧できずマスク値が表示されてしまう場合はマスク値をコピーしてしまう。

外部トークン化

外部関数を使って外部APIに実装されたトークン化処理を実行する、ということなのだが、
信じられないぐらい実装例をWebで見つけられなかった。(探し方が悪いのか..)

外部関数とは、要はクラウド上で利用可能なAPIを指す。
AWSで言うとAPI Gatewayの先にLambdaがあり、API統合により透過的に利用可能にしたもの。
Lambdaにトークン化コードを書いておきAPI Gatewayから呼べるようにしてAPI統合する。

既成の外部トークンプロバイダが存在し、Snowflakeから以下が利用できる様子。
ALTR,Baffle,Comforte,Fortanix,MicroFocus CyberRes Voltage,
Protegrity,Privacera,SecuPI,Skyflow

ちょっと深そうなので省略。