😽

外部CSSを適用させたHTMLをカスタムエラーレスポンスで返す際には、CSSのURIもアクセス許可を与えよう

に公開

はじめに

こんにちわ。しばらくバタバタしておりましてめっちゃ久しぶりのブログです。

先日業務の方でCloudFront+S3の静的ファイル配信の作業を行っていたのですが、その際引っかかったことがあるので備忘としてブログに書き残しておこうと思います。

TL;DR

WAFでアクセス制限をしながらS3に置いてあるカスタムエラーレスポンスのHTMLファイル(外部CSSファイル参照)を返す時には、WAF側にCSSファイルのURIパスの設定を入れてあげないとCSSがちゃんと適用されないで返っちゃうぞ!

どうしても適用したい場合はインラインCSSを使ったほうがいいかも!(たぶん)

どういうことか

皆さんご存じの通り、CloudFrontにはカスタムエラーレスポンス機能があるんですが、その際に、エラーレスポンスをカスタムすることができて、エラーごとにどのファイルを返却する、みたいなのが設定できるんですね。

たぶんカスタムエラーレスポンスとして返すファイルってあんまCSSを使わんだろうし、使うとしてもインラインCSSにすればいいだけなので、あまり記事自体に需要はないとは思うんですが…。

CSSを外部ファイルとして用意して、HTMLファイル側で"href"で参照していると結局のところ、CSSファイルへの通信も発生するんですよね。

またそういう場合であっても、デフォルトアクションをAllowにしておけば特段問題はないんですが、例えばDR用途で作成しているサイトなどの場合は、DR発生時だけデフォルトアクションをAllowにして、通常運用時に関してはBlockに設定している場合もあると思うのです。

そういう条件が重なった場合に今回説明したようなことが起こりえます。

実際に動かしてみよう

今回、WAF、CloudFront、S3が必要になるので、それらはCDKを利用して構築していきます。
それぞれ以下の通り作成しています。(コードはほぼGenAIに出してもらいました、便利だね!!)

import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as s3 from "aws-cdk-lib/aws-s3";
import * as s3deploy from "aws-cdk-lib/aws-s3-deployment";
import * as cloudfront from "aws-cdk-lib/aws-cloudfront";
import * as cloudfrontOrigins from "aws-cdk-lib/aws-cloudfront-origins";
import * as wafv2 from "aws-cdk-lib/aws-wafv2";

export class WafCloudFrontS3Stack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // S3バケット作成(エラーページ格納用)
    const bucket = new s3.Bucket(this, "ErrorPageBucket", {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
    });

    // エラーページをデプロイ
    new s3deploy.BucketDeployment(this, "DeployErrorPages", {
      destinationBucket: bucket,
      sources: [s3deploy.Source.asset("./assets")],
    });

    // CloudFrontディストリビューション
    const distribution = new cloudfront.Distribution(this, "TestDistribution", {
      defaultBehavior: {
        origin: new cloudfrontOrigins.S3Origin(bucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      errorResponses: [
        {
          httpStatus: 403,
          responseHttpStatus: 403,
          responsePagePath: "/sorry/sorry.html",
          ttl: cdk.Duration.seconds(1),
        },
      ],
    });

    // WAFv2 WebACL(最小構成)
    const webAcl = new wafv2.CfnWebACL(this, "MyWebAcl", {
      defaultAction: { block: {} },
      scope: "CLOUDFRONT",
      visibilityConfig: {
        cloudWatchMetricsEnabled: true,
        metricName: "webAcl",
        sampledRequestsEnabled: true,
      },
      name: "MyWebAclForCF",
      rules: [
        {
          name: "AllowSorryPath",
          priority: 0,
          action: { allow: {} },
          statement: {
            byteMatchStatement: {
              searchString: "/sorry/",
              fieldToMatch: { uriPath: {} },
              positionalConstraint: "STARTS_WITH",
              textTransformations: [{ priority: 0, type: "NONE" }],
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AllowSorryPath",
            sampledRequestsEnabled: true,
          },
        },
        {
          name: "AllowSpecificIP",
          priority: 1,
          action: { allow: {} },
          statement: {
            ipSetReferenceStatement: {
              arn: new wafv2.CfnIPSet(this, "AllowedIPSet", {
                addresses: ["xx.xx.xx.xx/32"], // ← 許可したいIPアドレス
                ipAddressVersion: "IPV4",
                scope: "CLOUDFRONT",
                name: "AllowedIPs",
              }).attrArn,
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "AllowSpecificIP",
            sampledRequestsEnabled: true,
          },
        },
        {
          name: "BlockBadBots",
          priority: 2,
          action: { block: {} },
          statement: {
            byteMatchStatement: {
              searchString: "BadBot",
              fieldToMatch: { singleHeader: { Name: "user-agent" } },
              textTransformations: [{ priority: 0, type: "NONE" }],
              positionalConstraint: "CONTAINS",
            },
          },
          visibilityConfig: {
            cloudWatchMetricsEnabled: true,
            metricName: "BlockBadBots",
            sampledRequestsEnabled: true,
          },
        },
      ],
    });

    // WAFとCloudFrontの関連付け
    const cfnDistribution = distribution.node.defaultChild as cloudfront.CfnDistribution;
    cfnDistribution.addPropertyOverride("DistributionConfig.WebACLId", webAcl.attrArn);
  }
}

想定通りだと以下のように表示されるのですが、、

この状態でcloudfront domain/rarara.htmlという存在しないファイルに対してアクセスをしてみても

このように表示されてしまいます。

今回のアクセス時の私のIPアドレスは以下になるのですが

WAFのルールとしてもAllowアクションでIPが存在しています。

なのでIPの問題ではなさそうです。

ということで次はちゃんと想定通りに表示されるように変更していきます。

今回の場合は以下の通り、sorryフォルダを作ってその中にHTMLファイルとCSSファイルを入れているので、このURIをWAFで許可するように設定しましょう。

この通り追加したのでちゃんと表示されるか見てみましょう

ちゃんと表示されてそうです。

まとめ

もしWAFでデフォBlockのアクセス制御を行っているシステムかつカスタムエラーレスポンスで表示するHTMLファイルに外部ファイルでのCSSを適用する場合は、WAFでCSSファイルがあるパスのURIも許可してあげようというお話でした。

でもまぁ安全面とかそういうの考慮するならたぶんインラインでCSS書いた方がいいです。

では。

Discussion