署名付きURLの制限方法を検証してみた(S3バケットポリシー利用)

# はじめに
S3の署名付きURL機能はAWS認証情報を持たないユーザーに対してファイルの配布やアップロードを許可することができる非常に便利な機能です。
反面、使い方を誤ると想定外のユーザーからのアクセスを許可してしまったり、S3上のファイルをダウンロードされてしまったりします。

本記事では署名付きURLを制限するための設定について検証してみました。

# 前提
この記事にたどり着いた方であれば既に見ているかもしれませんが、IAMポリシーを利用した署名付きURLの制限方法や

https://dev.classmethod.jp/articles/presigned-url-iam-policy-control/

IPアドレスを使用したURLへのアクセスユーザーの制限方法は既にまとめられてる方がいらっしゃるので、こちらを見ていただくと早いかと思います。

https://dev.classmethod.jp/articles/s3-pre-signed-url-ip-restriction/

ただ、バケットポリシーで署名付きURLを制限した記事は見当たらなかったので、以下のドキュメントを参考に設定してみました。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/bucket-policy-s3-sigv4-conditions.html#bucket-policy-sig-v4-condition-key-example

# 前提説明
上記ドキュメントにも記載されていますが、バケットポリシーで署名付きURLを指定したリクエストを拒否するには以下のようなバケットポリシーを設定する必要があります。

```
{
   "Version": "2012-10-17",
   "Statement": [
         {
               "Sid": "Allow only requests that use Authorization header for request authentication. Deny POST or presigned URL requests.",
               "Effect": "Deny",
               "Principal": "*",
               "Action": "s3:*",
               "Resource": "arn:aws:s3:::examplebucket3/*",
               "Condition": {
                     "StringNotEquals": {
                           "s3:authType": "REST-HEADER"
                     }
               }
         }
   ]
}
```

↑のバケットポリシーの中で特に重要なのは以下の箇所です。

- 「"s3:authType": "REST-HEADER"」

下記ドキュメントに詳細が記載されていますが、要はS3での認証方式を条件として細かく設定することができます。
認証方式の値にはREST-HEADER、REST-QUERY-STRING、POSTの3種類が指定できます。

> Amazon S3は様々な認証方法をサポートしている（リクエストの認証（AWS Signature Version 4）参照）。オプションで、この条件キーを使用して、特定の認証方法を使用するように受信リクエストを制限することができます。例えば、HTTPAuthorizationヘッダーのみをリクエスト認証に使用することを許可することができます。
> Valid values:
> REST-HEADER
> REST-QUERY-STRING
> POST

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/API/bucket-policy-s3-sigv4-conditions.html#bucket-policy-sig-v4-condition-key-example

S3の認証方式については、下記ドキュメントに詳しく書かれています。

> HTTP Authorizationヘッダー- HTTPAuthorizationヘッダーを使用することは、Amazon S3リクエストを認証する最も一般的な方法です。すべてのAmazon S3 REST操作（POSTリクエストを使用するブラウザベースのアップロードを除く）は、このヘッダーを必要とします。Authorizationヘッダーの値、署名の計算方法、関連オプションの詳細については、Authenticating Requestsを参照してください：Using the Authorization Header (AWS Signature Version 4)を参照。
> 
> クエリ文字列パラメータ- クエリ文字列を使用して、リクエスト全体をURLで表現できます。この場合、認証情報を含むリクエスト情報を提供するためにクエリパラメー タを使用する。リクエスト署名はURLの一部であるため、このタイプのURLはしばしば事前署名付きURLと呼ばれます。署名済みURLを使用して、HTMLに最大7日間有効なクリック可能リンクを埋め込むことができます。詳細については、リクエストの認証を参照してください：Using Query Parameters (AWS Signature Version 4)を参照してください。
> 
> Amazon S3は、HTTP POSTリクエストを使用するブラウザベースのアップロードもサポートしています。HTTP POSTリクエストでは、ブラウザから直接Amazon S3にコンテンツをアップロードできます。POSTリクエストの認証については、Browser-Based Uploads Using POST (AWS Signature Version 4)を参照してください。

https://docs.aws.amazon.com/AmazonS3/latest/API/sig-v4-authenticating-requests.html#auth-methods-intro

もう少しわかりやすく書くと以下のようになります。
AWSCLIなどでのS3アップロードでは、通常REST-HEADERが利用されるようになっており、署名付きURLではREST-QUERY-STRINGが利用されます。

- REST-HEADER　　　　→　POSTリクエストを除くすべてのAmazon S3 REST操作で使用
- REST-QUERY-STRING　→　事前署名付きURLで使用
- POST　　　　　　　　→　HTTP POSTリクエストを使用するブラウザベースのアップロードで使用

以上のことから、以下のバケットポリシーを設定することで、署名付きURLの利用を制限することができます。

```
{
   "Version": "2012-10-17",
   "Statement": [
         {
               "Sid": "Allow only requests that use Authorization header for request authentication. Deny POST or presigned URL requests.",
               "Effect": "Deny",
               "Principal": "*",
               "Action": "s3:*",
               "Resource": "arn:aws:s3:::examplebucket3/*",
               "Condition": {
                     "StringNotEquals": {
                           "s3:authType": "REST-HEADER"
                     }
               }
         }
   ]
}
```

## 検証
事前にS3バケットの作成とファイルのアップロードは実施している前提で、署名付きURLの発行とアクセスを試してみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/01bade0f-cd34-498b-a612-d98fe62c06df.png)

開くボタンでも署名付きURLは発行できますが、今回は敢えてアクションボタンから発行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/11d3f560-df58-29df-4433-4449cb0038e7.png)

有効期限を設定して発行します。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/fef266d7-f2f1-c8f1-d7da-cdbe976765e0.png)

クリップボードに署名付きURLがコピーされました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/d8463497-3838-89c2-6280-52c50678c625.png)

いくつかマスク入れてますが、以下のようなURLが発行されます。
この時URL内に「X-Amz-Security-Token」があると思いますが、署名付きURLの場合はこのセキュリティトークンがクエリ文字列に含まれます。

```
https://s3-signature-test-bucket.s3.ap-northeast-1.amazonaws.com/test.txt?response-content-disposition=inline&X-Amz-Security-Token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240319T100253Z&X-Amz-SignedHeaders=host&X-Amz-Expires=600&X-Amz-Credential=xxxxxxxxxxxxxxx%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Signature=0ba8c9a14xxxxxxxxxxxxxx47ecfebdf2f6db4b7f94174c901f365
```

そのURLをブラウザバーに入れると、想定通りAccessDeniedでエラーとなりました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/2e35905d-7670-0e8f-afc0-ffca43021fa0.png)

ローカルPC上のAWSCLIでファイルのアップロードとダウンロードを試してみると、こちらは署名付きURLを利用していないので問題なく成功しました。

```
$ aws s3api get-object --bucket s3-signature-test-bucket --key test.txt test1.txt
{
    "AcceptRanges": "bytes",
    "LastModified": "2024-03-19T04:09:42+00:00",
    "ContentLength": 4,
    "ETag": "\"098f6bcd4621d373cade4e832627b4f6\"",
    "VersionId": "ZyxEWpOH2hp_u1Bk0yJzODSXu3KL0RdC",
    "ContentType": "text/plain",
    "ServerSideEncryption": "AES256",
    "Metadata": {}
}
$ aws s3 cp test.txt s3://s3-signature-test-bucket/test1.txt
upload: ./test.txt to s3://s3-signature-test-bucket/test1.txt
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/0586fab1-30d7-674a-ce07-80681f5906e6.png)

これでバケットポリシーで署名付きURLでのファイル操作を制限できることが確認できました。

と、上手いこといけばよかったのですが、この制限方法には1つ大きなデメリットがあります。

## 問題点
それはS3のマネジメントコンソールからのファイルアップロードとダウンロードがエラーとなってしまうことです。

- ダウンロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/f6669b42-f4b0-987a-a4b5-59d9929d5238.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/79f8f4a9-2cb3-205d-8171-eb42539c77d7.png)

- アップロード
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/a72d241d-163d-61a1-5da6-550ecb05c378.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/83a9f9a5-57b8-a81d-2f0c-b0c13a5b96d2.png)

<br>
原因はダウンロードエラー時のブラウザバーのURLを見ていただくとすぐにわかります。

> https://s3-signature-test-bucket.s3.ap-northeast-1.amazonaws.com/test.txt?response-content-disposition=attachment&X-Amz-Security-Token=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxwWZrb2djU%3D&X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20240319T101607Z&X-Amz-SignedHeaders=host&X-Amz-Expires=300&X-Amz-Credential=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx40319%2Fap-northeast-1%2Fs3%2Faws4_request&X-Amz-Signature=57e75ac4440de8xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx8405f4c62d714c18220907

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/440403/598c3438-314f-c3bb-0c5a-c56cf6a5d630.png)


クエリ文字列に「X-Amz-Security-Token」が含まれていることで察した方もいるかもしれませんが、どうやらS3のマネジメントコンソールからのファイルアップロードとダウンロードでは、裏側で署名付きURLが使用されているみたいでした。

恐らく以下のような流れで処理が行われているのではないかなと思います。
1. ダウンロードボタン/アップロードボタン押下
2. 署名付きURLを発行
3. 署名付きURLを使用してファイルのダウンロード/アップロードを実施

そのため、署名付きURLを制限すると、同時にコンソール経由のダウンロード/アップロードも制限することになってしまうため、毎回AWSCLI等を利用して実行する等の対応が必要になり、かなり手間が増えてしまうことがわかりました。

私の方で色々と調べてみましたが、署名付きURLだけを制限する方法はまだ見つかっていないため、もしご存じの方がいれば是非コメントください。

# おわりに
制限はできるけどめちゃめちゃでかいデメリットもあるという微妙な結果になってしまいましたが、裏側の挙動を知れたのは勉強になりました。

実運用観点で考えると署名付きURLは発行できること前提で、以下のようなIP制限を入れる等の対策を取るのが現実的かなと思いました。

https://dev.classmethod.jp/articles/s3-pre-signed-url-ip-restriction/

この記事がどなたかの参考になれば幸いです。