[再検証] Amazon CloudFront のカスタムオリジンとして Amazon S3 のウェブサイトエンドポイントを指定した場合のアクセス制御について検証してみた

結論:バケットポリシー側での「サービスやアカウント」によるアクセス制御は難しそう
2024.01.17

こんにちは、CX 事業本部製造ビジネステクノロジー部の若槻です。

数日前に次のブログを執筆しました。CloudFront から S3 バケットへのカスタムオリジンによるアクセスを、バケットポリシーの Principal や Condition によりどの程度制限できるのか?という検証を行ったブログです。

しかし上記での検証の問題点として、キャッシュの考慮ができていませんでした。キャッシュ機能によりオリジンから取得されたコンテンツがエッジロケーションに保持された場合に、バケットポリシーの設定が TTL の期間は反映されない可能性があります。

そこで今回は、CloudFront のカスタムオリジンとして Amazon S3 のウェブサイトエンドポイントを指定した場合のアクセス制御について、キャッシュを考慮した再検証をしてみました。

試してみた

前回同様に次のパターンを AWS CDK で環境を構築しつつ検証してみます。

  • Principal で匿名アクセスを許可
  • Principal で特定サービスからのアクセスを許可
  • Principal で特定アカウントからのアクセスを許可
  • Condition で特定アカウントからのアクセスを許可

Principal で匿名アクセスを許可した場合

まずは匿名アクセスを許可した場合はアクセス制限がかかっていないことを確認してみます。

実装全体の CDK コードは以下になります。AnyPrincipal クラスにより匿名アクセスの許可を設定しています。なお検証パターンごとに変更する設定はバケットポリシーのみですが、最初のみ全体の CDK コードを掲載します。前回との相違点として、ディストリビューションの defaultBehavior.cachePolicy を設定してキャッシュを無効化しています。

lib/cdk-sample-stack.ts

import {
  aws_iam,
  aws_s3,
  aws_cloudfront,
  aws_cloudfront_origins,
  aws_s3_deployment,
  Stack,
  RemovalPolicy,
  Duration,
  CfnOutput,
} from 'aws-cdk-lib';
import { Construct } from 'constructs';

export class CdkSampleStack extends Stack {
  constructor(scope: Construct, id: string) {
    super(scope, id);

    // S3 バケット
    const websiteBucket = new aws_s3.Bucket(this, 'WebsiteBucket', {
      websiteIndexDocument: 'index.html',
      removalPolicy: RemovalPolicy.DESTROY,
      // autoDeleteObjects: true, // ポリシー単純化のため一旦コメントアウト
    });

    // S3 バケット名を出力
    new CfnOutput(this, 'BucketName', {
      value: websiteBucket.bucketName,
    });

    // CloudFront ディストリビューション
    const distribution = new aws_cloudfront.Distribution(this, 'Distribution', {
      defaultRootObject: 'index.html',
      errorResponses: [
        {
          ttl: Duration.minutes(5),
          httpStatus: 403,
          responseHttpStatus: 403,
          responsePagePath: '/error.html',
        },
        {
          ttl: Duration.minutes(5),
          httpStatus: 404,
          responseHttpStatus: 404,
          responsePagePath: '/error.html',
        },
      ],
      defaultBehavior: {
        cachePolicy: aws_cloudfront.CachePolicy.CACHING_DISABLED, // キャッシュ無効化
        viewerProtocolPolicy:
          aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        // HttpOrigin により、S3 バケットのウェブサイトエンドポイントを指定
        origin: new aws_cloudfront_origins.HttpOrigin(
          websiteBucket.bucketWebsiteDomainName,
          {
            protocolPolicy: aws_cloudfront.OriginProtocolPolicy.HTTP_ONLY,
          }
        ),
      },
    });

    // CloudFront ディストリビューションのドメイン名を出力
    new CfnOutput(this, 'DistributionDomainName', {
      value: distribution.distributionDomainName,
    });

    // バケットポリシーの明示的な作成
    const websiteBucketPolicy = new aws_s3.BucketPolicy(
      this,
      'WebsiteBucketPolicy',
      {
        bucket: websiteBucket,
      }
    );

    // バケットポリシー
    websiteBucketPolicy.document.addStatements(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        principals: [new aws_iam.AnyPrincipal()], // "Principal":{"AWS":"*"}
        actions: ['s3:GetObject'],
        resources: [websiteBucket.arnForObjects('*')],
      })
    );

    // S3 バケットへのコンテンツのデプロイ
    new aws_s3_deployment.BucketDeployment(this, 'WebsiteDeploy', {
      distribution,
      destinationBucket: websiteBucket,
      distributionPaths: ['/*'],
      sources: [
        aws_s3_deployment.Source.data(
          '/index.html',
          '<html><body><h1>Hello, World!</h1></body></html>'
        ),
        aws_s3_deployment.Source.data(
          '/error.html',
          '<html><body><h1>Error!!!</h1></body></html>'
        ),
        aws_s3_deployment.Source.data('/favicon.ico', ''),
      ],
    });
  }
}

上記のデプロイにより作成されたバケットポリシーは以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdksamplestack-websitebucket75c24d94-dzbqzhrl3hcw/*"
        }
    ]
}

S3 バケットの Web サイトエンドポイントへのアクセスは許可されています。

$ curl http://${BucketName}.s3-website-ap-northeast-1.amazonaws.com/
<html><body><h1>Hello, World!</h1></body></html>

CloudFront ディストリビューションへのアクセスは許可されています。

$ curl https://${DistributionDomainName}/
<html><body><h1>Hello, World!</h1></body></html>

期待通りの動作です。

Principal で特定サービスからのアクセスを許可する場合

Principal として cloudfront.amazonaws.com を指定し、CloudFront サービスからのアクセスのみ許可できるか確認してみます。

ServicePrincipal により指定のサービスの許可を設定しています。

lib/cdk-sample-stack.ts

    // バケットポリシー
    websiteBucketPolicy.document.addStatements(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        principals: [new aws_iam.ServicePrincipal('cloudfront.amazonaws.com')], // { "Service": "cloudfront.amazonaws.com" }
        actions: ['s3:GetObject'],
        resources: [websiteBucket.arnForObjects('*')],
      })
    );

上記のデプロイにより作成されたバケットポリシーは以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdksamplestack-websitebucket75c24d94-dzbqzhrl3hcw/*"
        }
    ]
}

S3 バケットの Web サイトエンドポイントへのアクセスは拒否されます。

$ curl http://${BucketName}.s3-website-ap-northeast-1.amazonaws.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: JMPX72YDQYWQW8KY</li>
<li>HostId: 7yqN4HnD7SBZzLN0ZYv7vaqQy6ooV82Oo2ylWsgGLO/MiO3xkjwY8obkAQlRzAq2Sl5ovHWsAAU=</li>
</ul>
<hr/>
</body>
</html>

CloudFront ディストリビューションへのアクセスは拒否されます。

$ curl https://${DistributionDomainName}/
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: K7DV2D1B1QCRPG26</li>
<li>HostId: YFqCIofABEG43QCA/VFiDWYYflFjFa7SY802XeYD2MZ89C9xaZdesDbBGAFCD3SuilsxkOwwbWU=</li>
</ul>
<hr/>
</body>
</html>

Principal で特定アカウントからのアクセスを許可する場合

プリンシパルとして自身の AWS アカウント ID を指定し、接続元のアカウント ID によるアクセス制御ができるか確認してみます。

AccountPrincipal により指定のアカウントの許可を設定しています。

lib/cdk-sample-stack.ts

    // バケットポリシー
    websiteBucketPolicy.document.addStatements(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        principals: [new aws_iam.AccountPrincipal(this.account)], // { "AWS": "arn:aws:iam::XXXXXXXXXXXX:root" }
        actions: ['s3:GetObject'],
        resources: [websiteBucket.arnForObjects('*')],
      })
    );

上記のデプロイにより作成されたバケットポリシーは以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::XXXXXXXXXXXX:root"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdksamplestack-websitebucket75c24d94-dzbqzhrl3hcw/*"
        }
    ]
}

S3 バケットの Web サイトエンドポイントへのアクセスは拒否されます。

$ curl http://${BucketName}.s3-website-ap-northeast-1.amazonaws.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: 5T6JB0XZR7EJBFXT</li>
<li>HostId: Dk9H25d/E7OQbphlPxCrrvKbzgOE0A9+PbiU3m8xeLCqSQTfcFjmubj4MbjES4IEqG1OsO+JuXU=</li>
</ul>
<hr/>
</body>
</html>

CloudFront ディストリビューションへのアクセスは拒否されます。

$ curl https://${DistributionDomainName}/
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: F40NPW70ZWZNFDD1</li>
<li>HostId: Ig93cX5+TUOLlukg0mCM+0tD6qR3yGRjS2imS5WKSPAkc1xvotHE5EWxCop/U9nSObiVpY+BGW4=</li>
</ul>
<hr/>
</body>
</html>

Condition で特定アカウントからのアクセスを許可する場合

次に Condition により AWS アカウントからのアクセス制御をできるか確認してみます。

PrincipalWithConditions を使うと、Principal と合わせて Condition を指定することができます。

lib/cdk-sample-stack.ts

    // バケットポリシー
    websiteBucketPolicy.document.addStatements(
      new aws_iam.PolicyStatement({
        effect: aws_iam.Effect.ALLOW,
        principals: [
          new aws_iam.AnyPrincipal().withConditions({
            StringEquals: {
              'aws:SourceAccount': this.account,
            },
          }),
        ],
        actions: ['s3:GetObject'],
        resources: [websiteBucket.arnForObjects('*')],
      })
    );

上記のデプロイにより作成されたバケットポリシーは以下になります。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::cdksamplestack-websitebucket75c24d94-dzbqzhrl3hcw/*",
            "Condition": {
                "StringEquals": {
                    "aws:SourceAccount": "XXXXXXXXXXXX"
                }
            }
        }
    ]
}

S3 バケットの Web サイトエンドポイントへのアクセスは拒否されます。

$ curl http://${BucketName}.s3-website-ap-northeast-1.amazonaws.com/
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: C414H3JZKRJVZDQR</li>
<li>HostId: 81PVsChdvPGeMreSSuVz03j3PmHqjOi63YfD7AdusVEsxDFVEQ6kTqf1YtbwhN0pRwnjb8TI2O4=</li>
</ul>
<hr/>
</body>
</html>

CloudFront ディストリビューションへのアクセスは拒否されます。

$ curl https://${DistributionDomainName}/                           
<html>
<head><title>403 Forbidden</title></head>
<body>
<h1>403 Forbidden</h1>
<ul>
<li>Code: AccessDenied</li>
<li>Message: Access Denied</li>
<li>RequestId: QYP5Q3KFYPBJ91T7</li>
<li>HostId: oYc5w0bXNeXBEKp77iw2M2dwmS5gdLxrMJng9xwos3R/foZHrhhEMABIGsCmSsD6UuJvk1RxaxU=</li>
</ul>
<hr/>
</body>
</html>

まとめ

検証の結果をまとめると次のようになりました。

S3 バケットへの直接アクセス CloudFront からのアクセス
Principal で匿名アクセスを許可 許可 許可
Principal で特定サービスからのアクセスを許可 拒否 拒否
Principal で特定アカウントからのアクセスを許可 拒否 拒否
Condition で特定アカウントからのアクセスを許可 拒否 拒否

当初、特定サービスやアカウントからのアクセスを許可した場合であれば CloudFront からのアクセスは許可されると考えていましたが、実際には匿名アクセス許可以外のパターンではアクセスが拒否される結果となりました。この挙動は、カスタムオリジンによる S3 ウェブサイトエンドポイントへのアクセスは AWS サービス間の内部的な API リクエストではなく、公開エンドポイントへの外部的な HTTP リクエストになるためのようです。

結論として、CloudFront のカスタムオリジンとして S3 ウェブサイトエンドポイントを構成した場合は、バケットポリシー側での「サービスやアカウント」によるアクセス制御は難しそうです。下記で紹介されているような「カスタムヘッダー(リファラー)」を利用したアクセス制御を検討する方針をご検討ください。

以上