Amazon CloudFront の署名付きURL(カスタムポリシー)を使って、安全にコンテンツを配信してみた

# はじめに

[前回の記事](https://qiita.com/sugimount-a/items/382e10fbbc85bbca1f3d)では、CloudFront の署名付きURL(規定ポリシー)を使って、Private の S3 Bucket に格納している画像データを、安全性を高めたアクセス方法を確認しました。

今回の記事では、より柔軟なアクセスポリシーを定義できるカスタムポリシーを利用した方法を確認していきましょう。



# 2種類の署名付き URL

前回の記事でも記載しましたが、今回も改めて記載します。

署名付き URL は、セキュリティに関するポリシーの違いによって、「規定ポリシー」と「カスタムポリシー」の 2 種類あります。2 つのポリシーの違いを AWS Document に書かれている表から抜粋します。規定ポリシーは、デフォルトの設定になっており、手を加えない分比較的楽に生成できます。一方、カスタムポリシーは自分たちのワークロードに合わせたカスタマイズが可能で、柔軟にコントロールができます。カスタムポリシーの方は、複数のオブジェクトのために再利用出来たり、アクセス元の IP アドレスの制限が出来るため、便利に利用できそうですね。

![image-20220225195842898.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/a9fb6949-eee3-f4dc-57f4-bf77f4a0dd3c.png)

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html#private-content-choosing-canned-custom-policy



今回の記事では、カスタムポリシーを使った署名付き URL を生成する方法を確認していきます。



# 秘密鍵・公開鍵の作成

署名付き URL を生成するために、秘密鍵・公開鍵の生成が必要です。手元の Linux マシンなどで作成していきましょう。

```
mkdir ~/temp/cloudfront-presign/
cd ~/temp/cloudfront-presign/
```



秘密鍵を生成します

```
openssl genrsa -out private_key.pem 2048
```



秘密鍵から、CloudFront に登録するための公開鍵を生成します

```
openssl rsa -pubout -in private_key.pem -out public_key.pem
```



こんな感じに生成できました

```
> ls -lha
total 12K
drwxr-xr-x  2 ec2-user docker   51 Feb 25 20:18 .
drwxrwxr-x 36 ec2-user docker 4.0K Feb 25 19:12 ..
-rw-r--r--  1 ec2-user docker 1.7K Feb 25 20:18 private_key.pem
-rw-r--r--  1 ec2-user docker  451 Feb 25 20:18 public_key.pem
```



# CloudFront に公開鍵をアップロード

CloudFront のページに移動して、Create public key を押します

![image-20220225202106137.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b5e3a6a5-f555-9bc1-7d70-6f31376e6b2d.png)



公開鍵の中身を確認して、登録をしていきます

```
cat public_key.pem
```

![image-20220225202319453.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fbc759c4-8c6c-1748-f926-dd46cb86f155.png)



登録されました。

![image-20220225202407849.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/ff5ab494-d9e6-64ab-2187-22fb52f7657d.png)



# CloudFront に Key Group を作成

Key Group を作成して、アップロードした公開鍵を登録します

![image-20220225202500082.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/c2b3df5a-9c43-3ac9-2dd1-058054a37496.png)



名前や公開鍵の名前を選択します

![image-20220225202545446.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/43b11d6a-bcee-e530-d6c8-e99d549b1147.png)



作成されました

![image-20220225202614643.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4dcc41e7-d086-e12b-69fd-429ea7574f65.png)



# S3 Bucket の作成

適当な名前で Private な S3 Bucket を作成して、うちの猫の写真をアップロードします。この写真を、署名付き URL のアクセス確認に使っていきます。

![image-20220225203426231.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/76c230fb-38f1-5eaa-849f-671da4c4dc06.png)



# CloudFront の Distribution の作成

署名付き URL を利用するために、Create distributon を押します。

![image-20220225203558305.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/2d5bbee2-e0dd-8263-594b-e3352fe0557e.png)



- Distribution の Origin として、作成した S3 Bucket を指定
- OAI を利用して、S3 Bucket のアクセスを CloudFront のみに制限

![image-20220225203852078.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/516a6d47-eb3e-3a09-1965-9b3751be6f03.png)



Restrict viewer access を有効にして、署名付き URL が無いとアクセスできないように設定します

![image-20220225204031423.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6bfa64e2-8590-b7e1-b74f-20d8f00cff86.png)



Create Distribution を押します

![image-20220225204128034.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/7d6b75cd-769e-8b02-b0ea-b91ec39c4ed1.png)



Deploy プロセスが走りました

![image-20220225204209601.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/d530270a-b72e-66b1-d220-bafa22793a3b.png)



# 署名付きURL(カスタムポリシー)の生成

カスタムポリシーを利用した署名付き URL は、AWS CLI では生成できず、何らかのプログラムで生成する必要があります。AWS Document にサンプルコードが記載されているため、これを参考にするとよいでしょう。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PrivateCFSignatureCodeAndExamples.html



今回の記事では、Python を使った署名付き URL の生成を行います。ソースコードはこんな感じです。

- Python の実行環境上で秘密鍵 `/home/ec2-user/temp/cloudfront-presign/private_key.pem` が存在している
- 署名付き URL の対象となるパス `https://d31z1elfcc2o1s.cloudfront.net/mycat.jpg` を指定
- 署名付き URL の有効期限を `expire_date` で指定
- アクセス元の IP アドレスを `54.250.23.0/32` と指定 (とある EC2 インスタンスのもの)

```python
import datetime

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives.asymmetric import padding
from botocore.signers import CloudFrontSigner
from dateutil import tz


def rsa_signer(message):
    with open('/home/ec2-user/temp/cloudfront-presign/private_key.pem', 'rb') as key_file:
        private_key = serialization.load_pem_private_key(
            key_file.read(),
            password=None,
            backend=default_backend()
        )
    return private_key.sign(message, padding.PKCS1v15(), hashes.SHA1())


key_id = 'K1DYHGI8XWLXZL'
url = 'https://d31z1elfcc2o1s.cloudfront.net/mycat.jpg'
expire_date = datetime.datetime(
    2022, 2, 25, 22, 10, 0, 0, tz.gettz('Asia/Tokyo'))

cloudfront_signer = CloudFrontSigner(key_id, rsa_signer)

# Create a signed url that will be valid until the specific expiry date
# provided using a canned policy.
policy = cloudfront_signer.build_policy(
    url, expire_date, date_greater_than=None, ip_address='54.250.23.0/32')

signed_url = cloudfront_signer.generate_presigned_url(
    url, date_less_than=None, policy=policy)
print(signed_url)
```



この Python コードを実行すると、署名付き URL を生成できます

```
> python cloudfront-create-presign.py
https://d31z1elfcc2o1s.cloudfront.net/mycat.jpg?Policy=eyJTdGF0ZW1lbnQiOlt7IlJlc291cmNlIjoiaHR0cHM6Ly9kMzF6MWVsZmNjMm8xcy5jbG91ZGZyb250Lm5ldC9teWNhdC5qcGciLCJDb25kaXRpb24iOnsiRGF0ZUxlc3NUaGFuIjp7IkFXUzpFcG9jaFRpbWUiOjE2NDU4ODEwMDB9LCJJcEFkZHJlc3MiOnsiQVdTOlNvdXJjZUlwIjoiMC4wLjAuMC8wIn19fV19&Signature=RmBo1DqAyEMSQWvMgubzHs2zufS03rceMEIvv8JHkQaMyQDtuu3aLTDSyQ9eO4Oyp~lkB2QrC-KM8v21eo9i0IRWFsbY5IreNpOqo5sLc8RWL9LpKWo6~EM1ykD29SCp8fObkLBe9ed85lNLqml2kDztHCV7ebYvU7fBUGHLnsQ4n4NZjRpipDfZdMJbhjx0DWfpp5VmaKIGh0XwZxMeNbR8vMIUEKn1DHLcyLLywkzyFZaI146NvekpDgS-VqMmxBIRVOjxhXc3p8JvPoTdAEYW3Dxp7Cit~Xd1xNoO9cn6GiFQUDn9uWD1fETSzASilj8NbpIzpJXU6S1n0I57XQ__&Key-Pair-Id=K1DYHGI8XWLXZL
```



# アクセス確認

まず、通常通りに CloudFront 経由でアクセスを試みると、想定通りにエラーになります。

![image-20220225224943999.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/7e463096-8f30-bd0f-5e0c-7c5bdd82a9e3.png)





次に、署名付き URL で、許可された IP アドレスからアクセスすると、正常に家猫の画像が出ます。かわいい。

![image-20220225223014443.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/85c047b6-51f9-7a51-15aa-9a43bdd4ae17.png)



署名付き URL の 有効期限が切れたときの結果です。想定通りエラーになります。

![image-20220225223038966.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fd2f8ebe-1868-dfcf-c635-e62ae744dd09.png)



# IP アドレスを制限するための WAF

署名付きURLのカスタムポリシーでは、アクセス元の IP アドレスが指定可能ですが、1個までという制限があります。Web アプリケーションとして認証を加えたうえで、アクセス元のクライアント IP を取得し生成することで、URL が仮に漏れたとしてもアクセスを防ぐ構成も可能です。

上記とは別の方法で、WAF を利用した 複数の IP アドレス制限を加えることも可能です。WAF の構成もしてみましょう。Create IP set を押します。

![image-20220225221613336.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/b258a928-cd15-544b-ce66-aa39aaf2f325.png)



アクセスを許可する IP アドレスを複数入力します。

![image-20220225221745226.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/a367378e-d32f-9892-7859-0530a67c26e8.png)



IP set が作成されました。

![image-20220225221801738.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/6a50f1d4-8fbc-1453-ae8d-7a742c159a09.png)



WAF の Web ACL を作成します。

![image-20220225221819429.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/97827287-9a02-5c81-833f-2090ff8d2667.png)



適当に名前を入れて、CloudFront に紐づけます。

![image-20220225221951132.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/fd02e6f1-0a1c-df56-2d0b-fea406f4524d.png)



Add my own rules and rule groups を選択します。

![image-20220225222133497.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/4a8036af-8c4e-a721-b708-f87fe8793d46.png)



作成した IP set を選択して、Allow を選びます

![image-20220225222223822.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/292c4378-1135-6ac9-d212-89acb1f286b6.png)



ルールに合致しないものは Block とすることで、指定された IP アドレス以外を防ぎます

![image-20220225222256457.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/8301b5b2-dfac-b127-7e0f-4d84aeabbf2d.png)



Next

![image-20220225222323717.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/dc05fdbc-1dc9-ee68-ffc1-c276792532df.png)



Next

![image-20220225222335482.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/5910a189-6a2e-ef4f-6a5d-ceb8c0733688.png)



Create を押します

![image-20220225222348524.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/827149dd-0cdd-0655-9635-9106cbbac505.png)



WAF の Web ACL が作成されました

![image-20220225225617750.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/ecc18680-a775-01e1-49da-7e0670e74ba8.png)



# アクセス確認

WAF でブロックしている IP アドレスから、署名付き URL にアクセスしてみると、想定通りアクセスができません。WAF としてのエラーとなっており、カスタムポリシーとは異なったエラーが表示されます。

![image-20220225222932946.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1002774/cb852f78-9c74-e326-6a87-d7502e910a07.png)



# 検証を通じてわかったこと

- カスタムポリシーでは、アクセス元の IP アドレスを指定可能
  - ただし、カスタムポリシー上では 1 個のみ指定可能
  - IP アドレスを複数利用して制限したい場合は、WAF を利用した IP アドレス制限が検討可能

# 付録 : ワイルドカードで複数のファイルを配信
https://qiita.com/sugimount-a/items/054bfe28bfaeb51dc409

# 参考URL

AWS Document : 署名付き URL の使用
https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-signed-urls.html

Python のサンプルコード
https://github.com/boto/boto3/blob/develop/boto3/examples/cloudfront.rst