S3 + CloudFront構成のSPAでWAFによるブロックが効かないときの解決策

はじめに

SREチームの森原(@daichi_morihara)です。今後は積極的に発信していこうという誓いを込めてXのアカウントを作成しました。

今回はEC2+ALBでホストしていたフロントエンドをS3+CloudFrontに移行する際に起きた問題について紹介しようと思います。移行後のフロントエンドのインフラ構成としては下の図の通りで、S3の前段にCloudFrontを配置しており、CloudFrontに紐づいたWAFがアクセス制限を行います。

フロントエンドのインフラ構成図

ここでハマった問題というのが「CloudFrontに紐づいたWAFが、なぜかブロックすべきアクセスまで通してしまう…?」というものでした。 本記事では問題の原因と、それに対する解決策を共有していきます。もしかすると気づかずのうちに同じ状況になっている可能性もあると思うのでご参考にしていただけると幸いです。

問題の原因

問題に気づいた当初はWAFにブロックされるべきアクセスが素通りしているように見えたため、WAFの設定ミスを疑いました。しかし、WAFのログを確認してみると、実際にはWAFは期待通りにアクセスをブロックしていました。 さらに調査を進めると原因は、CloudFrontのカスタムエラーページ設定にありました。CloudFrontのカスタムエラーページ設定で403のエラーに対してレスポンスページパスを/index.htmlに設定していたため、WAFがブロックしたリクエストもS3オリジンの/index.htmlで返していることが判明しました。

CloudFrontとS3でSingle-Page Application (SPA)のフロントエンドをホストする場合、ルーティングを /index.html に統一する必要があります。つまり、存在しないパスへのアクセスなどに対してもカスタムエラーページとして /index.htmlの設定が必要となります。 ここで重要なのは、CloudFrontのエラーページの設定はWAFのブロックによる403レスポンスと、S3オリジンからの「オブジェクトが存在しない」などの理由による403レスポンスを区別できないということです。

この問題に対する解決方法は2つあります。

解決策1:S3オリジンのバケットポリシーを修正する

CloudFrontの作成の際にはS3オリジンへのアクセスをCloudFrontに限定するためにOrigin Access Control (OAC) の使用がAWSの推奨となっています。このOACを選択すると、AWSが自動的にS3オリジンバケットに付与するバケットポリシーを作成してくれます。

しかし、このデフォルトのバケットポリシーでは、CloudFrontが存在しないオブジェクトにアクセスしようとした場合に403エラーが返されます。これは、デフォルトのバケットポリシーには s3:ListBucket 権限が付与されていないためです。 CloudFrontが存在しないS3オブジェクトを取得しようとする際に上記の権限がなければ、S3は該当するオブジェクトが存在しないのか、もしくはオブジェクトへのアクセス権限がないのかを公開することができず、セキュリティ上の理由から一律で403レスポンスを返します。

したがって、デフォルトのバケットポリシーに s3:ListBucket 権限を追加することで、S3は存在しないオブジェクトに対してs3:GetObjectが実行されると404レスポンスを返すようになり、CloudFrontのカスタムエラーページで403エラーに対する設定は不要になります。WAFによるブロックは引き続き403レスポンスが返るため、CloudFrontはWAFのブロックとS3のエラーを区別できるようになります。

{
    "Sid": "AllowCloudFrontServicePrincipal1",
    "Effect": "Allow",
    "Principal": {
        "Service": "cloudfront.amazonaws.com"
    },
    "Action": "s3:ListBucket",
    "Action": [
        "s3:GetObject",
        "s3:ListBucket"
    ],
    "Resource": "arn:aws:s3:::<Bucket name>",
    "Resource": [
        "arn:aws:s3:::<Bucket name>/*",
        "arn:aws:s3:::<Bucket name>"
    ],
    "Condition": {
        "StringEquals": {
            "AWS:SourceArn": "arn:aws:cloudfront::<Account Number>:distribution/<Distribution ID>"
        }
    }
}
S3オリジンのバケットポリシー

解決策2:AWS WAFのカスタムレスポンスを利用する

WAFがリクエストをブロックする際の挙動は、以下の優先順位で決定されます。

  1. AWS WAFのカスタムレスポンス
  2. 紐づけられたリソースに設定されたカスタムレスポンス(例:CloudFrontのカスタムエラーレスポンス、API GatewayのGatewayレスポンス)
  3. AWS WAFのデフォルトブロックレスポンス

docs.aws.amazon.com

したがって、AWS WAF自体にカスタムレスポンスを設定することで、CloudFrontのカスタムエラーレスポンスよりも優先され、意図した通りのブロック時の挙動(例えば、特定のエラーページを表示するなど)を実現できます。

採用した解決策

今回は、より少ない工数で対応可能なS3バケットポリシーの変更を採用しました。具体的には上記で説明した通りデフォルトのバケットポリシーに s3:ListBucket権限を追加し、403に対するカスタムエラーページの設定を削除するという対応です。 これによりWAFがブロックした際の403レスポンスはそのまま返されるため、期待通りの挙動となりました。

最後に

今回直面した問題に関する情報が少なくブログ等も見当たらなかったため、WAFとCloudFrontのAWSサポートチームにご協力いただき解決に至りました。もし同様の課題に遭遇された方や今後CloudFrontを使用する方に本記事を参考にしていただければ幸いです。また、困った場合は、AWSサポートさんに問い合わせることも有効な手段だと改めて感じました。