CloudFront×S3で403 Access Deniedが出るときに確認すべきこと

# 前提
- S3をオリジンとしたCloudFrontディストリビューションを作成している
- HTMLや画像ファイルをCloudFront経由で配信したい
- 独自ドメインのCNAMEを登録している

CloudFrontのドメイン、またはCNAMEで登録している独自ドメインにアクセスすると以下のような画面が表示される

<img width="840" alt=" 2020-03-20 22.20.01.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/7449732e-081b-9cb9-4df7-16270ba2d05a.png">

これをアクセスできるようにすることがゴールです。

# 確認すべきこと

## バケット内のオブジェクトが公開されているかどうか
まずCloudFront抜きのS3単体で考えます。

特に制限をかけておらず、バケットのパブリックアクセスが可能になっている状態であれば、
`パブリック` という黄色のラベルが表示されているはずです。

<img width="1260" alt="2020-03-21_1_21_03.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/f3c5164d-6b27-ca4a-dec8-64c0789066d2.png">

もしついていなければとりあえず以下のバケットポリシーを設定しましょう。

```json
{
    "Version": "2008-10-17",
    "Id": "PublicRead",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": "*",
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{your bucket name}/*"
        }
    ]
}
```

これは、制限なしで誰でもバケット内のオブジェクトにアクセスできる状態を作ります。

※ブロックパブリックアクセスをオンにしている場合、設定がうまくいかないことがあります。
下記画像の4つの設定のどれがオンになっているのか、バケットポリシーが新規なのか既存なのか等々、
ユースケースに応じて原因が異なるので、以下の公式ドキュメントを参考にしてください。
[Amazon S3 ブロックパブリックアクセスの使用](https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/dev/access-control-block-public-access.html)

<img width="1263" alt=" 2020-03-21 1.25.09.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/ea7f971b-6a14-8a9f-df7f-2cb3a524bdb7.png">

これでひとまずS3のREST API エンドポイントを叩いてアクセスができる確認してみましょう。
https://your_bucket_name.s3.amazonaws.com/hoge.html

## バケット内のオブジェクトがCloudFrontに対して公開されているかどうか

アクセスができたら、今度はアクセス元を制限します。
直接バケットへのアクセスが可能になっているのはセキュリティ上好ましくないですし、
CloudFrontの恩恵も受けられないので、CloudFrontからのアクセスのみ可能な形にします。

まずは OAI（Origin access identity）を作成します。
ディストリビューションの作成時に一緒に作成する方法と、
既存のディストリビューションのOrigin編集時に作成してアタッチする方法と、
CloudFrontのマネジメントコンソールから作成して、既存のディストリビューションにアタッチする方法があります。

これはマネジメントコンソールからの作成。
コメントはヒューマンリーダブルなものを適当につけましょう。
<img width="1326" alt=" 2020-03-21 1.38.08.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/f981dd99-31f5-6eef-147e-938bfd2fb297.png">

作成したOAIを [Your Identities] に指定します。
後述しますが、[Grant Read Permission on Bucket]のYesにチェックを入れると後々楽です。
<img width="1372" alt="2020-03-21_1_40_55.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/bccae218-1de1-31e5-65ef-ab82387b578f.png">

バケットポリシーの編集に戻ります。
アタッチしたOAIのIDのみ許可するようにします。

```json
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity {your oai id}"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::{your bucket name}/*"
        }
    ]
}
```

先ほどの [Grant Read Permission on Bucket] にチェックを入れていると、
作成したOAIでバケットポリシーを自動で更新してくれるのでこの作業は不要です。

これで先ほどの S3 REST API エンドポイントで今度は Access Denied が出るようになります。
バケット内のオブジェクトにアクセスできるのは、該当のOAIを付与したディストリビューションだけになったからです。

試しにディストリビューションの Domain Name でアクセスしてみましょう。
http://hohge.cloudfront.net/hoge.html

## バケットポリシーで Denied を設定してたりしないかどうか
バケットポリシーには明示的にアクセスを拒否するステートメントも記述することができます。
そのような設定がなされている場合は削除したり、削除した後もキャッシュが残っていることがあるので5分待つ必要があります（Invalidations で無効にするも可）。

>注: CloudFront は、Access Denied エラーの結果を最大 5 分間キャッシュします。バケットポリシーから拒否ステートメントを削除した後、ディストリビューションに対して無効化を実行してオブジェクトをキャッシュから削除できます。

## オブジェクトが存在するかどうか
根本的な確認になりますが、意外とこういうケアレスミスがあります。ファイル名のタイポとか。
ややこしいのは、存在しないオブジェクトにアクセスした場合でも403 Access Denied が返ってくる仕様になっていることです。
404 Not Found が隠蔽されることがしれっとドキュメントに書いてあります。
>オブジェクトがバケット内にない場合は、アクセス拒否エラーは「404 Not Found」エラーを隠します。
注: セキュリティ上のベストプラクティスではないため、s3:ListBucket の公開を有効にすることはお勧めしません。

s3:ListBucket を付与すれば404になるんでしょうね。
バケット内の状態が不可視になっているために、
オブジェクトが存在しないのか、権限がなくて不可視になっているだけなのか、判別できないとうことなんでしょう。

## デフォルトのルートオブジェクトが定義されているかどうか
先述のオブジェクトの存在チェックとも絡んできますが、
ディストリビューションのルートオブジェクトを設定しましょう。

>ディストリビューションにデフォルトのルートオブジェクトが定義されておらず、リクエスタが s3:ListBucket アクセス許可を持っていない場合、リクエスタはアクセス拒否エラーを受け取ります。ディストリビューションのルートをリクエストするとき、リクエスタは 404 Not Found エラーの代わりにこのエラーを受け取ります。

動作が異なるので、S3の静的ウェブサイトホスティングに慣れている人は特に注意してください。
[CloudFrontのデフォルトルートオブジェクトとS3の静的ウェブサイトホスティングのインデックスドキュメントの動作の違い](https://dev.classmethod.jp/cloud/aws/cloudfront_s3_difference/)

## バケットを作成してから24時間以上経っているかどうか
ブラウザ上で確認していると気付きにくいが、403ではなく、307でリダイレクトされ結果403になっていることがあります。
リダイレクト先はS3のREST API エンドポイントです。
適切にアクセス元が絞られていれば、S3への直アクセスは Denied になるので、結果的に403という状態になっています。

これは、リージョンの伝播に時間がかかるためです。

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

どういうことかというと、

バケットのエンドポイントにはリージョンが含まれます。
ap-northeast-1 で作成したバケット内のオブジェクトのエンドポイントは以下のようになります。
https://your-bucket-name.s3-ap-northeast-1.amazonaws.com/hoge.html

しかし、ディストリビューションを作成する際に Origin として候補に出てくるのは、
S3のデフォルトのエンドポイント（`your_bucket_name.s3.amazonaws.com`）です。
リージョンがついてませんが、こいつの実態は `us-east-1` です。

つまり、 `us-east-1` 以外のバケットにおいては、
このエンドポイントが使用できるまでに遅延が発生する可能性があるということです。

>Amazon S3 オリジンで Amazon CloudFront ディストリビューションを使用する場合、CloudFront はリクエストをデフォルトの S3 エンドポイント (s3.amazonaws.com) に転送します。このエンドポイントは、us-east-1 リージョンにあります。バケットを作成後 24 時間以内に Amazon S3 にアクセスする必要がある場合は、バケットのリージョンのエンドポイントが含まれるように、このディストリビューションのオリジンドメイン名を変更します。たとえば、バケットが us-west-2 にある場合は、オリジンドメイン名を bucketname.s3.amazonaws.com から bucketname.s3-us-west-2.amazonaws.com に変更することができます。
>
>[Amazon S3 から HTTP 307 Temporary Redirect レスポンスが返るのはなぜですか?](https://aws.amazon.com/jp/premiumsupport/knowledge-center/s3-http-307-response/)

解消方法は24時間待つか、 Originにリージョン込みのエンドポイントを設定することです。
選択式のため入力できないかと思いがちですが、別にそんなことはありません。

<img width="1388" alt="2020-03-21_2_35_33.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83183/e92c0e92-fa0d-13f0-741c-dd128f57e8ca.png">

リージョン込みのエンドポイントを指定すればすぐに使えるようになります。



だいたい原因はこんなところかなーと思います。
僕は最後のでちょっとハマりました。
バケット名がなんか気にくわないから別のバケットを作って切り替えるみたいなことをしていたら踏みました。

他にもこんなのでハマったとかあれば教えてください👨‍💻