【Next.js+S3+CloudFront】S3をパブリックかプライベートにアクセスする時の課題
はじめに
ポートのSREを担当している @tomoyuki.hori です。
あるプロジェクトにて、フロントにNext.jsをS3とCloudFrontを使ってデプロイする要件に携わりました。
その際にS3を「静的ウェブサイトホスティング」にしてパブリックアクセスにするケースと、プライベートにして「OAI」「OAC」を使ってアクセス制限するケースで試行錯誤したので、その作業をまとめました。
記事の目的
本記事ではS3に静的コンテンツ(Next.js)をデプロイする上で、S3をパブリックにアクセス可能とするか、プライベートアクセスにするか、それぞれの方法と問題点をまとめています。
パブリックにアクセスの場合

プライベートアクセスの場合

説明しないこと
- Next.jsの詳細や解説
- 証明書の作成やCloudFrontの代替ドメインの設定やビヘイビア、その他の詳細設定
- AmplifyやVercelなどのホスティングサービスとの比較説明
1.S3をパブリックアクセス可能にした場合
S3には静的ウェブサイトホスティング機能というものがあります。
S3で静的ウェブサイトホスティングを使用する場合、バケットのコンテンツを「パブリックにアクセス可能」の状態にする必要があります。
その際、HTTPのWebサイトが公開されることになり、セキュリティの観点からS3には必ずCloudFront経由でアクセスさせるために、S3をCloudFrontのカスタムオリジンとして設定する必要があります。
オリジンが、ウェブサイトエンドポイントとして設定されている Amazon S3 バケットである場合、CloudFront でカスタムオリジンとして設定する必要があります。つまり、OAC (または OAI) を使用することはできません。ただし、カスタムヘッダーを設定し、それらを要求するようにオリジンを設定することで、カスタムオリジンへのアクセスを制限できます。
ですので、以下のポイントで実装します。
- S3の「静的ウェブサイトホスティング」を有効化
- S3のブロックパブリックアクセスを無効にする
- バケットポリシーでアクセス制限、バケットをパブリックに読み取り可能にする
- 「静的ウェブサイトホスティング」を有効化したS3をCloudFrontのカスタムオリジンに設定
1-1. Next.jsプロジェクト生成
Next.jsのプロジェクトを生成
npx create-next-app --typescript
npm run dev
http://localhost:3000/ にアクセス

1-2. S3の設定
バケット作成
バケットを以下のように作成
| 項目 | 内容 |
|---|---|
| バケット名 | test-next-bucket |
| AWSリージョン | ap-northeast-1 |
| ACL | 無効 |
| オブジェクト所有者 | オブジェクトライター |
| ブロックパブリックアクセス設定 | チェック全て外す |


静的ウェブサイトホスティング有効化
作成したバケットの「プロパティ」タブから「静的ウェブサイトホスティング」を編集
| 項目 | 内容 |
|---|---|
| 静的ウェブサイトホスティング | 有効にする |
| ホスティングタイプ | 静的ウェブサイトをホストする |
| インデックスドキュメント | index.html |
| エラードキュメント | 404/index.html |

Next.js側の設定
Next.jsをS3にデプロイするためにアプリ側で設定変更する
package.jsonを以下のように編集
{
"name": "アプリ名",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build && next export",
"start": "next start",
"lint": "next lint"
},
}
next.config.jsを以下のように編集
module.exports = {
nextConfig,
trailingSlash: true,
experimental: {
images: {
unoptimized: true
}
}
}
上記はNext.jsのTrailing Slash機能を利用している
1-3. CloudFrontの設定
ディストリビューションを作成
「ディストリビューションを作成」から以下のように設定
| 項目 | 内容 |
|---|---|
| オリジンドメイン | S3の静的ウェブサイトホスティングのバケットウェブサイトエンドポイント※S3バケットのエンドポイントではないので注意
|
| オリジンパス | 空白 |
| 名前 | (自動入力される) |
| S3バケットアクセス | Public |
| オリジンシールドを有効にする | いいえ |
| パスパターン | デフォルト (*) |
| オブジェクトを自動的に圧縮 | Yes |
| ビューワープロトコルポリシー | Redirect HTTP to HTTPS |
| 許可された HTTP メソッド | GET, HEAD |
| ビューワーのアクセスを制限する | No |
| キャッシュキーとオリジンリクエスト | Cache policy and origin request policy(recommended) CachingOptimized |
| 料金クラス | すべてのエッジロケーションを使用する (最高のパフォーマンス) |
| セキュリティポリシー | TLSv1.2_2021 (推奨) |
| サポートされている HTTP バージョン | HTTP/2 |
| デフォルトルートオブジェクト | index.html |
| 標準ログ記録 | オフ |
| IPv6 | オン |
※注意※
選択肢に出てきたS3バケットのエンドポイントは静的ウェブサイトのエンドポイントではない
そのまま選ぶと注意画面が出てくる

設定完了後、CloudFrontのディストリビューションのオリジンのタイプがカスタムオリジンとなっていることを確認する。

バケットポリシーの作成
S3に移動し、「アクセス許可」から「バケットポリシー」を以下のように編集
"aws:Referer”部分はCloudFrontのドメイン名(https://ディストリビューションドメイン名/*)
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicReadGetObject",
"Effect": "Allow",
"Principal": "*",
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::art-tunes-prd-nextjs-bucket/*",
"Condition": {
"StringLike": {
"aws:Referer": "ディストリビューションドメイン名/*"
}
}
}
]
}
上記設定後、「パブリックにアクセス可能」となる

オリジンの作成
作成されたディストリビューションの「オリジン」タブを開く
オリジン名を選択→「ヘッダーを追加」で以下のように編集
| ヘッダー名 | 値 |
|---|---|
| Referer | CloudFrontのディストリビューションドメイン名/* 例:https://ddddd12345xxxxx.cloudfront.net/* |

オリジンにカスタムヘッダーを設定することにより、特定のディストリビューションからのアクセスをS3バケットポリシーと照らし合わせて、許可をしています。
今回はヘッダー名にRefererを入れ、値はディストリビューションドメインを指定しています。
1-4.S3にデプロイ
ここまで最低限の準備はできたので、Next.jsをS3にデプロイします。
package.jsonが存在するディレクトリで以下を実行
npm run build
ディレクトリにoutフォルダーができる
outディレクトリ以下のモジュールをS3にあげる
aws s3 sync --region ap-northeast-1 ./out s3://test-next-bucket --delete
CloudFrontのキャッシュを削除
aws cloudfront create-invalidation --region ap-northeast-1 --distribution-id ディストリビューションID --paths "/*"
デプロイ後はCloudFrontの代替ドメインを設定など行い、完了

問題点
カスタムヘッダーを使って、特定のディストリビューションからのアクセスを許可するようにしているが、
万が一、Refereの値が漏洩してしまった場合、S3エンドポイントにアクセスできてしまう点があります。
S3をパブリックにアクセス可能にしている点もあり、そういったセキュリティリスクを抱えていることになります。
2.S3をプライベートモードにしてアクセスする場合
今度は「静的ウェブサイトホスティング」を無効にし、つまりパブリックにアクセスではなくプライベートにアクセスする方法をとります。
このケースの場合、CloudFrontにはオリジンの設定に「Origin Access Identity(OAI)」、「Origin Access Control(OAC)」があり、
この設定により、S3バケットをパブリックにアクセス可能にすることなく、CloudFrontからのみアクセス許可をする設定ができます。
「Origin Access Control(OAC)」は2022年8月に新たに発表されている
こちらの方を実際に実装していきます。
2-1.静的ウェブサイトホスティングを無効にする
静的ウェブサイトホスティング機能を無効化にします。

2-2.ブロックパブリックアクセスを可能にする
チェックを入れて、全てブロックを有効にします。

2-3.オリジンのエンドポイント変更とOACの適用
オリジンにS3バケットを指定して、Origin Access Controlを有効にします。
この際、カスタムヘッダーのRefereも削除します。
-
通常のS3バケットを選択

-
OACを作成し、適用,カスタムヘッダーも削除して、設定なしにする

2-4.バケットポリシーでOACのアクセスを許可する
下記のようにバケットポリシーを変更する
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::test-next-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::アカウントID:distribution/ディストリビューションID"
}
}
}
]
}
以上で設定完了です。
再度デプロイ
再びアクセスを試みると...

デフォルトのページ(index.html)は表示されます。
一見これでOKのように見えます。
ですが、ここで問題なのはページ遷移すると正しく表示されない問題が発生します。
しかし他のページに遷移、例えばindex.html/example のようなページに遷移したい場合すると、このようにアクセス不可となります。
ページ遷移を実装して再度検証してみると...
Next.js側で以下のように設定します。
pages/example.tsx
import styles from '../styles/Home.module.css';
const Example = () => {
return <div className={styles.container}>Example page</div>
}
export default Example;
そして再度デプロイしてみると、
- パブリックアクセスの場合
問題なく、exampleページが表示されています。

- プライベートアクセスの場合
以下のようにアクセスができません。

これはCloudFrontはデフォルトページのオブジェクトのみを表示させる仕様なので、今回はデフォルトページに index.html を設定しており、それ以外のページ(example.tsx)が存在しないと判断されたと考えられます。
パブリックアクセスだとS3バケットの中身を全て公開させているので、その要因もあるかと思います。
この問題を解決するためにLambda@Edgeと呼ばれる方法を使って、解決することも可能です。
ですが、Lambdaを使用する場合は、Lambdaのリソース管理が発生し、アプリケーションの規模が大きくなると都度メンテナンスしなけらばならないといったことも考えられます。
問題点とまとめ
-
S3を静的ウェブサイトホスティングにし、バケットポリシーにRefererを設定してカスタムオリジンにする
- Refereの値が漏れるとバケットに直接アクセスできてしまうセキュリティリスクを抱える
-
OAIやOACを使用して、S3をプライベートにアクセス、Lambda@Edgeを利用する
- Lambdaのコスト面で考慮したりやメンテナンスをする必要がある
S3ないし、CloudFrontのアップデートは今後もあるので、それによって設定や状況が変わるかもしれません。
Discussion