ネットワークルールを利用したSnowflake内部ステージへのIPアクセス制限

本記事は、Snowflake の内部ステージへの IP アクセス制限（許可した IP アドレスからの接続のみ許可する）を行うための、

- ネットワークルール
- ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES パラメーター

に関する動作確認の記録です。~~ちなみに、これらの機能は 2024/2/11 時点ではパブリックプレビューです。~~

# 1. 前提知識

Snowflake には接続元のクライアントを制限するための **ネットワークポリシー** という機能があります。企業内で外部 SaaS を用いる場合、社内ネットワークからのアクセスのみに制限したいというニーズ（もしくはセキュリティーチェックシート上のルール）があり、それを実現することができる機能です。

![01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/b58de4b5-60a7-668e-745d-0ca1c9b984ab.png)

通常はこれで困らないのですが、Snowflake の内部の動作を考慮すると一部気になる箇所があります。それは **内部ステージ** に関する動作です。

Snowflake ステージはテーブルにデータをロードする、もしくはテーブルからデータをアンロードするためのファイル置き場のことであり、外部ステージと内部ステージの2種類があります。

- 外部ステージ：ユーザーが管理するオブジェクトストレージ上の領域
- 内部ステージ：Snowflake 社が管理するウオブジェクトストレージ上の領域

（AWS 上の Snowflake であれば、実体はどちらも Amazon S3 です。）

テーブルデータのロード/アンロードがよく知られた利用用途ですが、内部ステージに限っては SEELCT 文の結果をクライアントへ返すために暗黙的に利用されることがあります（必ずではないです）。

![02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/f02458c5-a0b3-ade1-a8a0-5cb3343d3636.png)

1. クライアントから SELECT 文を Snowflake サービス（～.snowflakecomputing.com）へ投げる。（上図①）
1. Snowflake サービスは受け取った SELECT 文を実行し、結果を内部ステージに保存する。（上図②）
1. クライアントは SELECT 文の結果を内部ステージからダウンロードする。（上図③）

（上の図はデータの流れの矢印になっています。③の TCP としての実際の接続の向きは逆です。）

問題になるのは、この内部ステージへのアクセスに関してはネットワークポリシーによるアクセス制限がデフォルトでは適用されないことです。

![03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/ef72cd9a-2ca4-74d7-92c7-71c59e86101a.png)

通常は Snowflake サービスへの接続の時点でネットワークポリシーによる制限が掛かっているので問題にはなりません。ただし、クライアントから Snowflake サービスと内部ステージへのアクセスの経路が異なっているなどネットワーク構成が複雑な場合、意図しない経路でデータ通信が許可されるという問題が発生します。

**ネットワークルール** と **ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES** パラメータを利用して、内部ステージへのアクセスを制限してこの問題を回避できるか確認してみたいと思います。

# 2. 動作確認

本来は　PrivateLink 利用時に意味がある設定内容ですが、コスト面の都合で Standard Edition & パブリックネットワーク経由での検証です。

まず、通常のネットワークポリシーにより Snowflake サービスへのアクセス制限ができることを確認します。次に、内部ステージにはその制限が掛かっていないことを確認します。最後ににネットワークルールと ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES パラメータを使用して、内部ステージへのアクセス制限が有効になったことを確認します。

動作確認のために、AWS 上に以下のような環境を準備しました。

![04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/388675/1b9f04a0-f2ea-0fb6-3567-ab29185d1bff.png)

ちなみに、

- クライアントでは HTTP_PROXY / HTTPS_PROXY 環境変数を設定して、SnowSQL がプロキシサーバーを利用するように設定しています。
- $HOME/.snowsql/config で SnowSQL による接続時のデフォルトアカウント名などは設定済みです。

## 2-1. ネットワークポリシーの適用と確認

まず、プロキシサーバーからの接続を識別するネットワークルールを作成します（ネットワークポリシーに直接 IP アドレスを指定できるのですが、後々のためにここでネットワークルールを作成します。

```sql
use role accountadmin;
use schema mydb01.public;
create or replace network rule nr_ec2_proxy
    type = IPV4
    value_list = ('54.150.142.182/32')
;
```

作成したネットワークルールの設定内容は以下のようになります。（ネットワークルールがスキーマオブジェクトというのが若干腑に落ちませんが）

```
mabe#(no warehouse)@MYDB01.PUBLIC>show network rules;
+-------------------------------+--------------+---------------+-------------+--------------+---------+------+---------+----------------------+-----------------+
| created_on                    | name         | database_name | schema_name | owner        | comment | type | mode    | entries_in_valuelist | owner_role_type |
|-------------------------------+--------------+---------------+-------------+--------------+---------+------+---------+----------------------+-----------------|
| 2024-02-11 20:33:11.981 +0900 | NR_EC2_PROXY | MYDB01        | PUBLIC      | ACCOUNTADMIN |         | IPV4 | INGRESS |                    1 | ROLE            |
+-------------------------------+--------------+---------------+-------------+--------------+---------+------+---------+----------------------+-----------------+
1 Row(s) produced. Time Elapsed: 0.072s
```

その後、ネットワークポリシーを作成し、アカウントレベルでアクティブ化します。

```sql
create network policy np_ec2_proxy
    allowed_network_rule_list = ('nr_ec2_proxy')
;
alter account set network_policy = NP_EC2_PROXY;
```

ネットワークポリシーが有効になったことを確認するために、プロキシサーバーを経由せずにアクセスしてみます。

```
[ec2-user@client ~]$ unset HTTP_PROXY
[ec2-user@client ~]$ unset HTTPS_PROXY
[ec2-user@client ~]$ snowsql
Password: 
250001 (08001): Failed to connect to DB: oedvigf-ac01.snowflakecomputing.com:443. Incoming request with IP/Token 35.77.20.196 is not allowed to access Snowflake. Contact your account administrator. For more information about this error, go to https://community.snowflake.com/s/ip-xxxxxxxxxxxx-is-not-allowed-to-access.
If the error message is unclear, enable logging using -o log_level=DEBUG and see the log to find out the cause. Contact support for further help.
Goodbye!                                                                        
[ec2-user@client ~]$ 
```

想定通り、アクセスは拒否されました。

## 2-2. ネットワークポリシーが内部ステージに効いていないことの確認

次に、内部ステージのみプロキシサーバーを経由しないように設定してみます。NO_PROXY 環境変数で内部ステージの S3 バケットの FQDN を指定します（この FQDN は SYSTEM$ALLOWLIST システム関数から取得可能です）。

```
[ec2-user@client ~]$ export HTTP_PROXY=http://proxy:3128
[ec2-user@client ~]$ export HTTPS_PROXY=${HTTP_PROXY}
[ec2-user@client ~]$ export NO_PROXY=sfc-jp-ds1-14-customer-stage.s3.ap-northeast-1.amazonaws.com
[ec2-user@client ~]$ snowsql
Password: 
* SnowSQL * v1.2.31
Type SQL statements or !help
mabe#COMPUTE_WH@MYDB01.PUBLIC>select current_timestamp();
+-------------------------------+                                               
| CURRENT_TIMESTAMP()           |
|-------------------------------|
| 2024-02-11 23:49:38.231 +0900 |
+-------------------------------+
1 Row(s) produced. Time Elapsed: 0.071s
mabe#COMPUTE_WH@MYDB01.PUBLIC>select * from test_table;  
（略）
| 9994 | efnIPR7kGLp7d0w29BwPkl1eFZKWlU |
| 9995 | icFFEjxOC6PuJFTZVHroIgQfrg9rdu |
| 9996 | XzB63nIElNLBZFWcPq9qB7JnhXZQpX |
| 9997 | bf8VStU4isf7KTFbVf0789NPvamCsX |
| 9998 | GB6OZNHXTaFKC77MAFvdfIORPeOgSm |
| 9999 | cIgOhYhFagRDSDP4IwwFkOCvd0fKXT |
+------+--------------------------------+
10000 Row(s) produced. Time Elapsed: 1.459s
mabe#COMPUTE_WH@MYDB01.PUBLIC>
```

問題なく、SEELCT 文の結果は返ってきており、内部ステージへのアクセス制限は効いていないことを確認できます。

## 2-3. 内部ステージへのアクセス制限の有効化

内部ステージへのアクセス制限を有効にするには ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES パラメーターを設定します。

```sql
use role accountadmin;
alter account set enforce_network_rules_for_internal_stages = true;
```

その後、SnowSQL から Snowflake に接続し、SELECT 文を実行してみます。

```
[ec2-user@client ~]$ snowsql
Password: 
* SnowSQL * v1.2.31
Type SQL statements or !help
mabe#COMPUTE_WH@MYDB01.PUBLIC>select current_timestamp();
+-------------------------------+                                               
| CURRENT_TIMESTAMP()           |
|-------------------------------|
| 2024-02-11 23:52:02.097 +0900 |
+-------------------------------+
1 Row(s) produced. Time Elapsed: 0.087s
mabe#COMPUTE_WH@MYDB01.PUBLIC>select * from test_table;
000403 (n/a): HTTP 403: Forbidden   
```

- Snowflake への接続自体は許可されたプロキシサーバー経由であるため可能であること
- SELECT 文の結果を受け取る時点で、内部ステージへのアクセスが拒否されて結果を取得できないこと

が確認できます。

ちなみに、QUERY_HISTORY ビューを確認すると、実行した SELECT 文の ```EXECUTION_STATUS``` は```SUCCESS```になっており、実行自体は成功していることが分かります。内部ステージからの結果ダウンロードの成否は Snowflake サービスとしては関知しないということのように思えます。

```
mabe#COMPUTE_WH@MYDB01.PUBLIC>select query_text, start_time, end_time, execution_status, error_code from snowflake.account_usage.query_history order by start_time desc limit 10;
+-----------------------------------------------------------+-------------------------------+-------------------------------+------------------+------------+
| QUERY_TEXT                                                | START_TIME                    | END_TIME                      | EXECUTION_STATUS | ERROR_CODE |
|-----------------------------------------------------------+-------------------------------+-------------------------------+------------------+------------|
| ROLLBACK                                                  | 2024-02-11 23:52:57.854 +0900 | 2024-02-11 23:52:57.889 +0900 | SUCCESS          | NULL       |
| select * from test_table;                                 | 2024-02-11 23:52:06.999 +0900 | 2024-02-11 23:52:07.040 +0900 | SUCCESS          | NULL       |
| select current_timestamp();                               | 2024-02-11 23:52:02.097 +0900 | 2024-02-11 23:52:02.146 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ COLUMNS IN SCHEMA MYDB01.PUBLIC        | 2024-02-11 23:51:53.078 +0900 | 2024-02-11 23:51:53.148 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ STAGES IN SCHEMA MYDB01.PUBLIC         | 2024-02-11 23:51:53.008 +0900 | 2024-02-11 23:51:53.061 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ USER FUNCTIONS IN SCHEMA MYDB01.PUBLIC | 2024-02-11 23:51:52.942 +0900 | 2024-02-11 23:51:52.992 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ VIEWS IN SCHEMA MYDB01.PUBLIC          | 2024-02-11 23:51:52.859 +0900 | 2024-02-11 23:51:52.925 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ TABLES IN SCHEMA MYDB01.PUBLIC         | 2024-02-11 23:51:52.768 +0900 | 2024-02-11 23:51:52.828 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ WAREHOUSES                             | 2024-02-11 23:51:52.708 +0900 | 2024-02-11 23:51:52.804 +0900 | SUCCESS          | NULL       |
| SHOW /* snowsql */ ROLES                                  | 2024-02-11 23:51:52.707 +0900 | 2024-02-11 23:51:52.786 +0900 | SUCCESS          | NULL       |
+-----------------------------------------------------------+-------------------------------+-------------------------------+------------------+------------+
10 Row(s) produced. Time Elapsed: 1.296s
```

# 3. まとめ

本記事では、ネットワークルールと ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES パラメーターによる内部ステージへの IP アクセス制限が効いていることを確認しました。

元々のネットワークポリシーの機能でも、

- Snowflake サービスへの接続は制限できる
- 内部ステージへの通信は TLS で保護されている

という点を考慮すると、この機能は **セキュリティー向上のためというより社内のセキュリティールールを通すため** にある機能のような気もしなくはないのですが、GA になったら商用環境には設定することになるのかなと思っています。

最後に、注意点 / 制約をリストしておきます。

- ~~2024/2/11 時点ではネットワークルールと ENFORCE_NETWORK_RULES_FOR_INTERNAL_STAGES パラメーターはパブリックプレビュー中~~
- ネットワークルールは PrivateLink 経由のアクセスを指定したいのであれば、IP アドレスではなく VPC ID を指定することが推奨
-  GET_PRESIGNED_URL 関数で生成した URL を利用した内部ステージへのアクセスはこの機能でも制限できない（実はこの機能が欲しかったりするのですが）

追記：ネットワークルールはプレビューと記載していましたが、2024年1月下旬の8.3で GA になっていました。ただし、Snowsight の GUI でネットワークルールを設定する機能はパブリックプレビュー中です。