🌚

【Next.js+S3+CloudFront】S3をパブリックかプライベートにアクセスする時の課題

2023/04/20に公開

はじめに

ポートのSREを担当している @tomoyuki.hori です。
あるプロジェクトにて、フロントにNext.jsをS3とCloudFrontを使ってデプロイする要件に携わりました。
その際にS3を「静的ウェブサイトホスティング」にしてパブリックアクセスにするケースと、プライベートにして「OAI」「OAC」を使ってアクセス制限するケースで試行錯誤したので、その作業をまとめました。

記事の目的

本記事ではS3に静的コンテンツ(Next.js)をデプロイする上で、S3をパブリックにアクセス可能とするか、プライベートアクセスにするか、それぞれの方法と問題点をまとめています。

パブリックにアクセスの場合

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

説明しないこと

  • Next.jsの詳細や解説
  • 証明書の作成やCloudFrontの代替ドメインの設定やビヘイビア、その他の詳細設定
  • AmplifyやVercelなどのホスティングサービスとの比較説明

1.S3をパブリックアクセス可能にした場合

S3には静的ウェブサイトホスティング機能というものがあります。
https://docs.aws.amazon.com/ja_jp/AmazonS3/latest/userguide/WebsiteHosting.html

S3で静的ウェブサイトホスティングを使用する場合、バケットのコンテンツを「パブリックにアクセス可能」の状態にする必要があります。
その際、HTTPのWebサイトが公開されることになり、セキュリティの観点からS3には必ずCloudFront経由でアクセスさせるために、S3をCloudFrontのカスタムオリジンとして設定する必要があります。

オリジンが、ウェブサイトエンドポイントとして設定されている Amazon S3 バケットである場合、CloudFront でカスタムオリジンとして設定する必要があります。つまり、OAC (または OAI) を使用することはできません。ただし、カスタムヘッダーを設定し、それらを要求するようにオリジンを設定することで、カスタムオリジンへのアクセスを制限できます。

https://docs.aws.amazon.com/ja_jp/AmazonCloudFront/latest/DeveloperGuide/private-content-restricting-access-to-s3.html

ですので、以下のポイントで実装します。

  1. S3の「静的ウェブサイトホスティング」を有効化
  2. S3のブロックパブリックアクセスを無効にする
  3. バケットポリシーでアクセス制限、バケットをパブリックに読み取り可能にする
  4. 「静的ウェブサイトホスティング」を有効化した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機能を利用している
https://nextjs.org/docs/api-reference/next.config.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月に新たに発表されている
https://aws.amazon.com/jp/blogs/news/amazon-cloudfront-introduces-origin-access-control-oac/

こちらの方を実際に実装していきます。

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)が存在しないと判断されたと考えられます。

https://repost.aws/ja/knowledge-center/cloudfront-default-root-object-subdirectory

パブリックアクセスだとS3バケットの中身を全て公開させているので、その要因もあるかと思います。

この問題を解決するためにLambda@Edgeと呼ばれる方法を使って、解決することも可能です。

https://aws.amazon.com/jp/lambda/edge/

ですが、Lambdaを使用する場合は、Lambdaのリソース管理が発生し、アプリケーションの規模が大きくなると都度メンテナンスしなけらばならないといったことも考えられます。

問題点とまとめ

  • S3を静的ウェブサイトホスティングにし、バケットポリシーにRefererを設定してカスタムオリジンにする

    • Refereの値が漏れるとバケットに直接アクセスできてしまうセキュリティリスクを抱える
  • OAIやOACを使用して、S3をプライベートにアクセス、Lambda@Edgeを利用する

    • Lambdaのコスト面で考慮したりやメンテナンスをする必要がある

S3ないし、CloudFrontのアップデートは今後もあるので、それによって設定や状況が変わるかもしれません。

Discussion