Amazon S3 Presigned URLを試してみた

# 背景・目的
S3の署名済みURL（Presigned URL）について整理し、試してみます。

# まとめ
下記に特徴をまとめます。

|特徴|説明|
|:--|:--|
|署名済みURLとは|S3オブジェクトへの時間制限付きアクセス権を付与する事が可能<BR><BR>署名済みURL認証情報は、URLを生成したAWSユーザのもの<BR><BR>ダウンロードとアップロードが可能|
| ダウンロード|署名済みURLをブラウザに入力か、アプリからダウンロードできる|
|アップロード|S3バケットに対する特定のオブジェクトのアップロードを他のユーザに許可することもできる<BR><BR>他のユーザは、セキュリティ認証情報やアクセス許可がなくてもアップロード可能<BR><BR>署名済みURLで指定したのと同一キーを持つオブジェクトであれば置き換え可能|
|署名済みURLの有効期限|期限付きで何度も使用できる|
|署名付き URL の作成|・自身のセキュリティ認証情報<BR>・S3バケット<BR>・オブジェクトキー<BR>・HTTPメソッド（ダウンロードはGET、アップロードはPUT）<BR>・有効期限の時間間隔|
|制限事項|署名済みURLは、アップロードの際には、下記の整合性チェックサムは使用できない<BR>・CRC32<BR>・CRC32C<BR>・SHA-1<BR>・SHA-256|
|整合性の確認|アップロード後にオブジェクトの整合性を確認するには、署名済みURLでアップロードするときにオブジェクトのMD5ダイジェストを指定できる|
|署名付き URL の作成に使用できる認証情報のタイプ|・IAMインスタンスプロファイル/最大6時間<BR>・STS/最長36時間<BR>・IAMユーザ/最長7日間|
|署名付き URL の有効期限|・署名付き URLは、URL生成時に指定した期間にわたり有効<BR>・コンソールで作成した場合、有効期間は1分から12Hまで設定できる<BR>・CLI,SDKでは、有効期間は最大7日間に設定可能|
|期限切れや認証情報が無効になった場合の挙動|・トークンが期限切れになると、URLは期限切れになる<BR>・作成に使用した認証情報が無効になった場合、URLも使えなくなる|
|ダウンロード中の挙動|ダウンロード開始から終了までの間に、有効期限が切れてもダウンロードは継続される<BR><BR>ただし、接続が中断した場合など再開した場合は、失敗する|


# 概要
下記の記事を基に整理します。

https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/using-presigned-url.html

> バケットポリシーを更新せずに、Amazon S3 内のオブジェクトへの時間制限付きのアクセス権を付与するには、署名付き URL を使用できます。署名付き URL をブラウザに入力するか、プログラムで使用してオブジェクトをダウンロードできます。署名付き URL で使用される認証情報は、URL を生成した AWS ユーザーのものです。

- 署名済みURLにより、S3オブジェクトへの時間制限付きアクセス権を付与する事が可能
- 署名済みURLをブラウザに入力か、アプリからダウンロードできる
- 署名済みURL認証情報は、URLを生成したAWSユーザのもの

> また、署名付き URL を使用して、Amazon S3 バケットに対する特定のオブジェクトのアップロードを他のユーザーに許可することもできます。これにより、他のユーザーは AWS のセキュリティ認証情報やアクセス許可を持たなくてもアップロードできます。署名付き URL で指定したのと同じキーを持つオブジェクトがバケット内に既に存在する場合、Amazon S3 は既存のオブジェクトをアップロードしたオブジェクトで置き換えます。

- 署名済みURLを使用して、S3バケットに対する特定のオブジェクトのアップロードを他のユーザに許可することもできる
    - 他のユーザは、セキュリティ認証情報やアクセス許可がなくてもアップロード可能
    - 署名済みURLで指定したのと同一キーを持つオブジェクトであれば置き換え可能

> 署名付き URL は、有効期限日時まで複数回使用できます。

- 期限付きで何度も使用できる

> 署名付き URL を作成する場合には、ご自身のセキュリティ認証情報を設定し、さらに次の情報を指定する必要があります。
> - Amazon S3 バケット
> - オブジェクトキー (オブジェクトのダウンロード先は Amazon S3 バケット、アップロード先はアップロード先のファイル名)
> - HTTP メソッド (オブジェクトをダウンロードする場合は GET、アップロードする場合は PUT)
> - 有効期限の時間間隔

- 署名済みURLを作成するには、下記を指定する
    - 自身のセキュリティ認証情報
    - S3バケット
    - オブジェクトキー
    - HTTPメソッド（ダウンロードはGET、アップロードはPUTS）
    - 有効期限の時間間隔


> 現在、Amazon S3 の署名付き URL では、オブジェクトをアップロードする際に次のデータ整合性チェックサムアルゴリズム (CRC32、CRC32C、SHA-1、SHA-256) を使用することはできません。アップロード後にオブジェクトの整合性を確認するには、署名付き URL でアップロードするときに、オブジェクトの MD5 ダイジェストを指定できます。オブジェクトの整合性の詳細については、「オブジェクトの整合性をチェックする」を参照してください。

- 署名済みURLは、アップロードの際には、下記の整合性チェックサムは使用できない
    - CRC32
    - CRC32C
    - SHA-1
    - SHA-256
- アップロード後にオブジェクトの整合性を確認するには、署名済みURLでアップロードするときにオブジェクトのMD5ダイジェストを指定できる

## 署名付き URL を作成できるユーザー

> 有効なセキュリティ認証情報を持つすべてのユーザーが、署名付き URL を作成できます。しかし、何らかの理由でオブジェクトに正常にアクセスするには、署名付き URL を使用して行うオペレーションの実行許可を持っているユーザーが、署名付き URL を作成する必要があります。
署名付き URL の作成に使用できる認証情報のタイプは以下のとおりです。

> - IAM インスタンスプロファイル – 最大 6 時間有効。
> - AWS Security Token Service — 長期のセキュリティ認証情報を使用して署名した場合、または一時的な認証情報の有効期間 (どちらか早い方) で最長 36 時間有効です。
> - IAM ユーザー – AWS Signature Version 4 を使用している場合は、最大 7 日間まで有効。
最大 7 日間有効な署名付き URL を作成するには、まず、署名付き URL の作成に使用するメソッドへの IAM ユーザー認証情報 (アクセスキーとシークレットキー) の委任を行います。


署名付き URL の作成に使用できる認証情報のタイプは、下記の通り
- IAMインスタンスプロファイル
    - 最大6時間
- STS
    - 最長36時間
- IAMユーザ
    - AWS Signature v4を使用しているなら最長7日間

## 署名付き URL の有効期限
> 署名付き URL は、URL の生成時に指定した期間にわたって有効です。Amazon S3 コンソールで署名付き URL を作成した場合、有効期限は 1 分から 12 時間の間で設定できます。AWS CLI または AWS SDK を使用する場合、有効期限は最大 7 日間に設定できます。

- 署名付き URLは、URL生成時に指定した期間にわたり有効
- コンソールで作成した場合、有効期間は1分から12Hまで設定できる
- CLI,SDKでは、有効期間は最大7日間に設定可能


> 一時トークンを使用して署名付き URL を作成した場合、そのトークンが有効期限切れになると、URL は期限切れになります。一般に、署名付き URL は、作成に使用した認証情報が取り消された場合、削除された場合、または非アクティブ化された場合は、期限切れになります。URL の有効期限がより長い場合でも失効します。認証情報の使用が有効期限にどのように影響するかについては、「署名付き URL を作成できるユーザー」を参照してください。

- トークンが期限切れになると、URLは期限切れになる
- 作成に使用した認証情報が無効になった場合、URLも使えなくなる

> Amazon S3 は、HTTP リクエスト時に署名付き URL の有効期限日時を確認します。例えば、有効期限が切れる時刻の直前にクライアントが大きなファイルのダウンロードを開始した場合は、ダウンロード中に有効期限時刻が経過しても、そのダウンロードは継続されます。しかし、接続が中断し、クライアントがダウンロードを再開しようとした時点で有効期限切れの時刻が経過している場合には、そのダウンロードは失敗します。


- ダウンロード開始から終了までの間に、有効期限が切れてもダウンロードは継続される
- ただし、接続が中断した場合など再開した場合は、失敗する

## 署名付き URL 機能の制限
> 署名付き URL の機能は、それを作成したユーザーの許可によって制限されます。本質的に署名付き URL は、それらを保有しているユーザーに対しアクセスを許可するためのベアラートークンです。そのため、適切に保護することをお勧めします。署名付き URL の使用を制限するために使用できるいくつかの方法を以下に示します。

- 署名済みURLは、作成者の許可に基づいてアクセスが制限されるベアラートークン
- 適切に保護することが重要
- 署名付きURLの使用を制限する

### AWS Signature Version 4 (SigV4)
> 署名済み URL リクエストが AWS Signature Version 4 (SigV4) により認証される際に実行する特定の動作を適用するには、バケットポリシーとアクセスポイントポリシーで条件キーを使用します。例えば、次のバケットポリシーでは、s3:signatureAge 条件を使用することで、署名が作成後 10 分を超えている場合、amzn-s3-demo-bucket1 バケット内のオブジェクトに対する Amazon S3 の署名付き URL リクエストをすべて拒否します。この例を実行するには、user input placeholders をユーザー自身の情報に置き換えます。


- 署名付きURLリクエストに特定の動作を適用するためには、バケットポリシーやアクセスポイントポリシーで条件キーを使う
- 例：`s3:signatureAge`条件を使って、署名が作成されてから10分以上経過した場合に、バケット内のオブジェクトへのアクセスを拒否する設定できる

```json
{
   "Version": "2012-10-17",
   "Statement": [
      {
         "Sid": "Deny a presigned URL request if the signature is more than 10 min old",
         "Effect": "Deny",
         "Principal": {"AWS":"*"},
         "Action": "s3:*",
         "Resource": "arn:aws:s3:::amzn-s3-demo-bucket1/*",
         "Condition": {
            "NumericGreaterThan": {
               "s3:signatureAge": 600000
            }
         }
      }
   ]
}
```

### ネットワークパスでの制限
> 署名付き URL の使用と、特定のネットワークパスへのすべての Amazon S3 アクセスを制限する場合は、AWS Identity and Access Management (IAM) ポリシーを記述できます。これらのポリシーは、呼び出しを作成する IAM プリンシパル、Simple Storage Service (Amazon S3) バケット、またはその両方に設定できます。

- 署名付きURLやAmazon S3へのアクセスを特定のネットワークに制限するには、IAMポリシーを使う

> IAM プリンシパルでのネットワークパスの制限では、これらの認証情報のユーザーは、指定したネットワークからリクエストを送信する必要があります。バケットまたはアクセスポイントの制限により、そのリソースに対するすべてのリクエストは、指定したネットワークから発信される必要があります。これらの制限は、署名付き URL のシナリオ以外でも適用されます。

- IAMプリンシパルでのネットワーク制限では、ユーザーは指定したネットワークからのみリクエストを送信できる
- バケットやアクセスポイントの制限も同様。すべてのリクエストが指定ネットワークから発信される必要がある
- これらの制限は、署名付きURL以外のアクセスにも適用される

> どの IAM グローバル条件キーを使用するかは、エンドポイントのタイプによって異なります。Amazon S3 のパブリックエンドポイントを使用している場合は、aws:SourceIp を使用します。Amazon S3 への仮想プライベートクラウド (VPC) エンドポイントを使用している場合は、aws:SourceVpc または aws:SourceVpce を使用します。

- エンドポイントの種類に応じて、使用するIAMグローバル条件キーが異なる
    - パブリックエンドポイントの場合は`aws:SourceIp`
    - VPCエンドポイントの場合は`aws:SourceVpc`や`aws:SourceVpce`

> 次の IAM ポリシーステートメントでは、プリンシパルは、指定されたネットワーク範囲からのみ AWS にアクセスする必要があります。このポリシーステートメントでは、すべてのアクセスがその範囲から発信される必要があります。これは、Amazon S3 の署名付き URL を使用しているユーザーにも当てはまります。この例を実行するには、user input placeholders をユーザー自身の情報に置き換えます。


- 下記のIAMポリシーステートメントでは、指定されたネットワーク範囲からのみAWSにアクセスできるように制限している
    - この制限は、署名付きURLを使用しているユーザーにも適用される
    - ポリシー内の情報をユーザー自身の設定に置き換えて使用する

```json
{
    "Sid": "NetworkRestrictionForIAMPrincipal",
    "Effect": "Deny",
    "Action": "*",
    "Resource": "*",
    "Condition": {
        "NotIpAddressIfExists": {"aws:SourceIp": "IP-address-range"},
        "BoolIfExists": {"aws:ViaAWSService": "false"}
    }
}
```

# 実践
## 前提
1. AWSにサインインし、S3に移動します
1. 専用のバケットを作成します
1. 下記のファイルを作成します
    ```bash
    % cat test.txt 
    test
    % 
    ```
1. アップロードします
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/206276/78842304-f07c-c1fc-2208-ba3337927130.png)

## 署名済みURLの検証 (GETリクエスト)
1. AWS CLIで署名付きURLを生成します。URLが返されます
    ```bash
    % aws s3 presign s3://${MyBucket}/download/test.txt --expires-in 300
    https://XXXXXXXXX.s3.ap-northeast-1.amazonaws.com/download/test.txt?X-Amz-Algorithm=XXXXX&X-Amz-Credential=XXXXXX&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Security-Token=XXXXXXX&X-Amz-Signature=XXXXXX
    % 
    ```
1. CurlでURLを指定して取得します
    ```bash
    curl "https://XXXXXXXXX.s3.ap-northeast-1.amazonaws.com/download/test.txt?X-Amz-Algorithm=XXXXX&X-Amz-Credential=XXXXXX&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Security-Token=XXXXXXX&X-Amz-Signature=XXXXXX"
    test
    ```

1. 5分（300秒）経過後、アクセスします。403エラーになりました。想定通りです
    ```xml
    % curl "https://XXXXXXXXX.s3.ap-northeast-1.amazonaws.com/download/test.txt?X-Amz-Algorithm=XXXXX&X-Amz-Credential=XXXXXX&X-Amz-Expires=300&X-Amz-SignedHeaders=host&X-Amz-Security-Token=XXXXXXX&X-Amz-Signature=XXXXXX"
    
    <?xml version="1.0" encoding="UTF-8"?>
    <Error><Code>AccessDenied</Code><Message>Request has expired</Message><X-Amz-Expires>300</X-Amz-Expires><Expires>2024-10-17T12:26:14Z</Expires><ServerTime>2024-10-17T12:27:13Z</ServerTime><RequestId>XXXXX</RequestId><HostId>XXXXX</HostId></Error>
    %
    ```

## 署名付きURLの検証 (PUTリクエスト)
:::note info
Putリクエスト用の署名付きURLは、AWS CLIではサポートされていません
boto3を使用し生成しています。
また、下記にあるように、バケットを作成直後は、307 Temporary Redirectが返される場合がありますのでreplaceでドメインを書き換えています。

> Amazon S3 バケットを作成した後、バケット名がすべての AWS リージョンに伝達するまでに最長で 24 時間かかる場合があります。その間に、お使いのバケットと同じリージョンにないリージョンのエンドポイントにリクエストすると、307 Temporary Redirect レスポンスが返される場合があります。


https://repost.aws/ja/knowledge-center/s3-http-307-response
:::

1. boto3をインストールします
    ```bash
    % pip install boto3
    %  pip list | grep boto3
    boto3              1.35.42
    % 
    ```

1. 下記のプログラムを作成します
    ```py
    import boto3
    
    s3_client = boto3.client('s3')
    response = s3_client.generate_presigned_url('put_object',
                                                Params={'Bucket': 'my-bucket',
                                                        'Key': 'download/test.txt'},
                                            ExpiresIn=300,
                                            HttpMethod = 'PUT')

    # See https://repost.aws/ja/knowledge-center/s3-http-307-response
    replaced_response = response.replace('s3.amazonaws.com', 's3.ap-northeast-1.amazonaws.com')
    
    print(replaced_response)
    ```
1. 実行します。URLが返されます
    ```bash
    % python presigned-put.py
    https://XXXXXX.s3.amazonaws.com/download/test.txt?AWSAccessKeyId=XXXXXXX&Signature=XXXXXXXX%3D&x-amz-security-token=XXXXXX&Expires=1729169215
    % 
    ```
1. 実行前のタイムスタンプを確認します
    ```bash
    % aws s3 ls s3://XXXXXX/download/test.txt
    2024-10-17 21:10:37          5 test.txt
    % 
    ```

1. 変数に格納し、そのURLを使用してPutします
    ```bash
    % URL=$(python presigned-put.py)
     % curl -D - -X PUT --upload-file test.txt $URL           
    HTTP/1.1 200 OK
    x-amz-id-2: XXXXX
    x-amz-request-id: XXXXX
    Date: Thu, 17 Oct 2024 13:18:23 GMT
    x-amz-server-side-encryption: AES256
    ETag: "XXXXX"
    Server: AmazonS3
    Content-Length: 0

    % 
    ```

1. 実行後のタイムスタンプを確認します。更新されました
    ```bash
    % aws s3 ls s3://XXXXXX/download/test.txt
    2024-10-17 22:18:23          5 test.txt
    %
    ```

1. 5分（300秒）経過後、アクセスします。403エラーになりました。想定通りです
    ```xml
    % curl -D - -X PUT --upload-file test.txt $URL           
    HTTP/1.1 403 Forbidden
    x-amz-request-id: XXXXX
    x-amz-id-2: XXXXX
    Content-Type: application/xml
    Transfer-Encoding: chunked
    Date: Thu, 17 Oct 2024 13:27:29 GMT
    Server: AmazonS3
    Connection: close
    
    <?xml version="1.0" encoding="UTF-8"?>
    <Error><Code>AccessDenied</Code><Message>Request has expired</Message><Expires>2024-10-17T13:21:26Z</Expires><ServerTime>2024-10-17T13:27:31Z</ServerTime><RequestId>XXXXX</RequestId><HostId>XXXX</HostId></Error>%
    % 
    ```

# 考察
今回、S3の署名済みURL（Presigned URL）について基本を整理しました。使用することで、オブジェクトに対する一時的なアクセスを安全に提供できることがわかりました。
また、本書では、GETとPUTリクエストの両方において署名付きURLを利用し、その基本的な使い方を整理しました。
今後も、積極的に利用していきたいと思います。

# 参考
https://docs.aws.amazon.com/AmazonS3/latest/userguide/using-presigned-url.html

https://ja.wikipedia.org/wiki/Bearer%E3%83%88%E3%83%BC%E3%82%AF%E3%83%B3

https://repost.aws/ja/knowledge-center/s3-http-307-response