S3 + CloudFront で配信してる画像をRailsアプリの認証がないとみれなくする

# 何がしたいか

S3 にアップロードして CloudFront で配信してるファイルのURLをRailsアプリで許可したユーザにしか見れなくしたくなった。

# どうするか

CloudFront の署名付きURL機能をつかう。 閲覧制限時間を設定できるので、たとえそのURLが漏れたとしても(制限時間が切れてれば)悪意の第三者は閲覧できない。

ユーザが画像を見るときは以下のような手順になる。

1. ユーザが制限付きの画像URL(`https://your-cloud-front-domain/private/image.jpg` とする) を含むリソース(`https://your-app-domain/index.html` とする)をアプリにリクエストする
2. アプリは CloudFront の署名付きURL(`https://your-cloud-front-domain/private/image.jpg?認証用のクエリ` を発行する
3. アプリは 署名付きURLを含んだ `index.html` をユーザに返却する
4. ユーザは署名つきURLで画像にアクセスし、画像を閲覧する

## 設定手順

### 前提

(フォルダ名は任意。)

- 運用中の S3, CloudFront がある
- S3 は `public/` ってフォルダ(オブジェクトキーのprefix)以下に配信用の画像を置いてる
- 制限付きで配信するリソースを配置するS3のフォルダは `private/` とする
- アプリ側はRailsで実装されてることにする(ただしRails固有の記述は本記事中にないと思う。)



### 手順

#### AWS のセキュリティ認証情報で CloudFront 用の秘密鍵のペアを作成する

[署名付き URL と署名付き Cookie (信頼された署名者) の作成が可能な AWS アカウントの指定 - Amazon CloudFront 開発者ガイド](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-trusted-signers.html)

ここで作った鍵のペアは安全なとこに保存する。

> IAM ユーザーは CloudFront キーペアを作成できません。キーペアを作成するには、ルート認証情報を使用してログインする必要があります。

同一アカウントで作った開発・ステージング・商用などのCloudFrontがあるとき、環境ごとに鍵を分けられないらしい。まじ注意。

#### CloudFront の設定

- Origins に以下を設定する。これで設定するとS3のバケットポリシーが更新される。
    - `Restrict Bucket Access`: `Yes`
    - `Origin Access Identity`: `Yes`
    - `Grant Read Permissions on Bucket`: `Yes, Update Bucket Policy`
- Behaviors に以下を新規作成する。
    - `path pattern`: `/private/*`
    - `Restrict Viewer Access`: `Yes`

こんな感じ
![f949c9177fa8eca3d1030917743e7779.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/26239/661bf05a-6a3b-ab35-eb52-5d624326cd40.png)


#### S3 の設定

CloudFront の設定手順でバケットポリシーが更新されているので、 `Resource` に今回適用したいフォルダ(prefix)を追記する。

```js:追加された設定
[
  {
    // 既存の設定
  },
  {
    "Sid": "適当な番号",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity 認証用の文字列"
    },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::バケットの名前/*"
  }
]
```

```js:変更した設定
[
  {
    // 既存の設定
  },
  {
    "Sid": "適当な番号",
    "Effect": "Allow",
    "Principal": {
      "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity 認証用の文字列"
    },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::バケットの名前/private/*"
  }
]
```


### 設定のテスト

AWS の設定はこれで終わり。以下でちゃんとアクセス制限されてるか確認する。

S3の `private/` にファイルをアップロードし、S3は`AccessDenied` 、CloudFrontは `MissingKey` エラーになって見れないことを確認する。

見れちゃう場合は設定が間違ってるのでS3バケットポリシーが適切に設定されてるか確認する。また CloudFront は設定変更が整うまで結構時間かかるのでステータスが delpoying になってないか確認する。

次の手順通りに署名付きのURLを発行し、見れることを確認する。
[Perl を使用して URL 署名を作成する - Amazon CloudFront 開発者ガイド](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/CreateURLPerl.html) 


## 実装

https://qiita.com/chorori/items/162fead6f9b5d4a933f2
こちらの記事の通り。
リクエストしたユーザの権限をチェックしてチェックが通った場合だけ署名付きURLを発行するようにすればよい


# 参考ページ

- [署名付き URL と署名付き Cookie を使用してプライベートコンテンツを供給する - Amazon CloudFront 開発者ガイド](https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/PrivateContent.html)
- [CloudFront+S3で署名付きURLでプライベートコンテンツを配信する - DevelopersIO](https://dev.classmethod.jp/cloud/aws/cf-s3-deliveries-use-signurl/)
- [CloudFrontの署名付きURLをRubyで発行する - Qiita](https://qiita.com/chorori/items/162fead6f9b5d4a933f2)