OAIからOACに変更してオリジンのS3バケットをKMSで暗号化してみた

2023.03.02

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部Delivery部のアベシです。

CloudFrontの Origin Access Control(OAC)ではOrigin Access Identity(OAI)で使用できなかったKMSを使ったオリジンのホスト先バケットの暗号化が可能になっています。
OAIではSSE-S3での暗号化は使えましたが、こちらではCloudTrailに証跡を残すことができませんでした。
今回OAIからOACに変更しオリジンのS3バケットをKMSで暗号化してみました。その際にCloud Trailでどのような証跡が取れるようになるのか確認しました。OAIからOACへの移行方法も含めて紹介します。

OAIでオリジンへのアクセスをCloudFrontからに限定する構成を構築

まずはOAIで構築します。
検証のためSSE-S3で暗号化します
オリジンのS3バケットにホストしているのはHello World!!と表示するだけのHTMLファイルです。
AWS環境の構築にはCDKを用いました。

lib/s3-hosting-practice-stack.ts

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

import { Construct } from 'constructs';
export class S3HostingPracticeStack extends Stack {
  constructor(
    scope: Construct,
    id: string,
    props: StackProps,
  ) {
    super(scope, id, props);

    //オリジンに指定するS3バケット作成
    const hostingBucket = new aws_s3.Bucket(
      this,
      `s3-hosting-bucket`,
      {
        bucketName: `s3-hosting-bucket-${Stack.of(this).account}`,
        removalPolicy: RemovalPolicy.DESTROY,
        encryption: aws_s3.BucketEncryption.S3_MANAGED,
        blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL, // オリジンへのアクセスをCloudFrontに限定するためにBLOCK_ALL設定
      },
    );

    //OAI作成
    const oai = new aws_cloudfront.OriginAccessIdentity(this, 'OAI', {
      comment: 'OAI',
    });

    //OAIにGetObjectを許可するバケットポリシーを作成しバケットに付与
    const policyGetObjectForOAI = new aws_iam.PolicyStatement({
      sid:"AllowOAIGetObject",
      effect: aws_iam.Effect.ALLOW,
      actions: ['s3:GetObject'],
      principals: [
        new aws_iam.CanonicalUserPrincipal(
          oai.cloudFrontOriginAccessIdentityS3CanonicalUserId,
        ),
      ],
      resources: [`${hostingBucket.bucketArn}/*`],//s3:GetObjectはオブジェクトへの操作なので`/*`を付ける
    });
    hostingBucket.addToResourcePolicy(policyGetObjectForOAI);

    //Distributionの作成
    const websiteDistribution = new aws_cloudfront.Distribution(
      this,
      'CloudFrontDistribution',
      {
        comment: 'website-distribution',
        defaultRootObject: 'index.html',
        defaultBehavior: {
          allowedMethods: aws_cloudfront.AllowedMethods.ALLOW_GET_HEAD,
          cachedMethods: aws_cloudfront.CachedMethods.CACHE_GET_HEAD,
          cachePolicy: aws_cloudfront.CachePolicy.CACHING_OPTIMIZED,
          viewerProtocolPolicy:
            aws_cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
          origin: new aws_cloudfront_origins.S3Origin(hostingBucket, {
            originAccessIdentity: oai,
          }),
        },
        errorResponses: [
          {
            ttl: Duration.seconds(300),
            httpStatus: 403, //forbidden
            responseHttpStatus: 403,
            responsePagePath: '/index.html',
          },
          {
            ttl: Duration.seconds(300),
            httpStatus: 404, //not found
            responseHttpStatus: 404,
            responsePagePath: '/index.html',
          },
        ],
      },
    );
    //S3バケットにデプロイするコンテンツの設定
    new aws_s3_deployment.BucketDeployment(this, 'webSiteDeploy', {
      destinationBucket: hostingBucket,
      sources: [
        aws_s3_deployment.Source.asset('./web-app'),
      ],
      distribution: websiteDistribution,
      distributionPaths: ['/*'],
    });
  }
}

コードの解説

何点かコードについて解説します。

  • S3バケットのBlockPublicAccess
    S3のオリジンへのアクセスをCloudFrontに限定するためにBlockPublicAccess全てブロックする設定としています。

    blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,

  • OAIの権限を定めるバケットポリシー
    与える権限のs3:GetObjectはオブジェクトへの操作なのでリソースの指定では末尾に/*を付ける事を忘れないようにしてください。
    AWS認定試験でもリソースの記述についての問で何回か出題されているのを見かけました。

    resources: [`${hostingBucket.bucketArn}/*`],

  • DistributionのDefault Root Objectの設定
    こちらを設定しない状態でAWS Security HubのAWS 基礎セキュリティのベストプラクティス v1.0.0を利用している場合、セキュリティチェックがFAILEDとなります。 こちらはセキュリティチェック項目として重要度がCRITICALとなります。 以下の弊社ブログでは設定しない場合の影響について解説していますので、是非参考にしていただければと思います。

  • CloudFrontのデフォルトルートオブジェクトを設定することで意図しないコンテンツの公開が防げるケースを実際に確認してみた

    defaultRootObject: 'index.html',

動作確認

以下の通りバケットの暗号化はSSE-S3となっています。

CloudFrontのドメインにアクセスするとオリジンのバケットにホストしているHTMLが表示されました。

OAIのままバケットの暗号化をKMSにしてみる

OAIでは対応していないKMSに暗号化方法を変更した時にどうなるか確認してみました。
コードは以下の部分を追加修正しています。

//KMSキーの作成を追加
const kmsKey = new aws_kms.Key(
  this,"kms managed key", {
    description:"kms managed key",
    removalPolicy:RemovalPolicy.DESTROY
  }
)

//S3バケットの作成部分の暗号化の指定を修正
const hostingBucket = new aws_s3.Bucket(
  this,
  `s3-hosting-bucket`,
  {
    bucketName: `s3-hosting-bucket-${Stack.of(this).account}`,
    removalPolicy: RemovalPolicy.DESTROY,
    encryptionKey:kmsKey, // 上記作成したKMSキーを指定
    encryption: aws_s3.BucketEncryption.KMS, //バケットの暗号化をKMSに指定
    blockPublicAccess: aws_s3.BlockPublicAccess.BLOCK_ALL,
  },
);

動作確認

CDKでのデプロイはエラーが発生する事無くできてしまいました。 以下の通りバケットの暗号化はKMSに切り替わっています。
この状態でCloudFrontのドメインにアクセスしてみると以下エラーが発生してHTMLファイルは表示されませんでした。

OAIからOACに変更

それではOACに変更してみます。
AWSのブログで変更方法について紹介されていたので参考にしました。
やることは結構簡単で以下3点となります。

  1. OACの作成
  2. ディストリビューションのS3 バケットアクセスをOACに変更
  3. OACがオリジンのオブジェクトをGetObjectできるようにバケットポリシーを変更

OACの作成

CloudFrontのトップページの左側のペインに有るオリジンアクセスを開きます。
コントロール設定からOAC作成できます。隣のアイデンティティのタブはOAIの作成用となります。既にレガシーと記載されていることから非推奨である事が窺えます

コントロール設定を作成をクリックして作成を進めます。 OACの名前を記載します。
署名動作は推奨の署名リクエスト (推奨)を選択しオリジンタイプS3を選択します。
署名動作リクエストに署名しないを選択した場合、オリジンアクセス制御を使用しない事になります。

作成をクリックしますと以下のような画面となりOACが作成されました。

ディストリビューションのS3 バケットアクセスをOACに変更

ディストリビューションのオリジンタブを開いて編集をクリックしオリジンの編集画面に遷移します。

以下の設定を行います。
S3 バケットアクセスOrigin access control settings (recommended)に変更
Origin access controlの下のプルダウンから先程作成したOACを選びます。
ポリシーをコピーを押すとAWSが作ってくれたバケットポリシーがコピーされます。
S3 バケットアクセス許可に移動を押すとバケットポリシーの編集画面が別タブで開きます。
こちらの編集画面は変更を保存を押して完了してからバケットポリシーの変更に進んでください。

バケットポリシーの変更

変更前のOAI用のバケットポリシーが以下となっております。

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowOAIGetObject",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E19UFSHZRALVTS"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::s3-hosting-bucket-************/*"
        }
    ]
}

OACがオリジンにGetObjectできるようにするポリシーを入れ替えます。
OACに必要なポリシーについては以下のAWS公式ドキュメントにも記載があります。

以下が必要なポリシーとなります。

{
    "Version": "2012-10-17",
    "Statement": {
        "Sid": "AllowOACGetObject",
        "Effect": "Allow",
        "Principal": {
            "Service": "cloudfront.amazonaws.com"      // 変更点
        },
        "Action": "s3:GetObject",
        "Resource": "arn:aws:s3:::s3-hosting-bucket-************/*",
        "Condition": {                                 // 変更点
            "StringEquals": {
                "AWS:SourceArn": "arn:aws:cloudfront::<AWS アカウント ID>:distribution/<CloudFront distribution ID>"
            }
        }
    }
}

ポリシーの変化点は以下です。

  • Principal
    • OAIのARNからcloudfront.amazonaws.comに変更
  • Conditionが追加されStringEqualsの条件にディストリビューションのARNが指定されています。

ポリシーは先程オリジンの編集画面でコピーしたものがそのまま使えますので、以下のように入れ替えてください。

KMSのキーポリシーを変更

CloudFrontがKMSキーでオリジンのHTMLファイルを復号化できるようにする必要があるのでキーポリシーを追加します。
オリジンバケットのページのプロパティタブの中にあるデフォルトの暗号化という項目から使用しているKMSキーのページを開けます。
このページでキーポリシーを追加します。
追加するポリシーは以下のとおりです
Conditionの指定にStringEquals条件としてディストリビューションのARNを指定します。

{
    "Effect": "Allow",
    "Principal": {
        "Service": "cloudfront.amazonaws.com"
    },
    "Action": [
        "kms:Encrypt",
        "kms:Decrypt",
        "kms:GenerateDataKey*"
    ],
    "Resource": "*",
    "Condition": {
        "StringEquals": {
            "aws:SourceArn": "arn:aws:cloudfront::<AWS アカウント ID>:distribution/<CloudFront distribution ID>""
        }
    }
}


動作確認

CloudFrontのドメインにアクセスしてHTMLコンテンツを表示

CloudFrontのドメインにアクセスするとHTMLファイルが表示されました!

CloudTrailの証跡ログの確認

オリジンを表示したタイミングで下の画像のような証跡ログが取れておりました。
userIdentityのinvokedByがcloudfrontとなっており、CloudFrontがKMSキーを使って復号化をしていることが確認できます。

さいごに

以下の弊社ブログではcloudFormationでOACを適応した環境の構築方法が紹介されております。

今後CDKでOACを使ったディストリビューションの構築方法がわかりましたら追って紹介したいと思います。