OAIなS3+CloudFront環境でタグを使って特定のS3オブジェクトだけ非公開にする

CloudFrontのOrigin Access Identityを使用してS3バケットポリシーによるアクセス制限をしている環境で、タグを付与した特定のS3オブジェクトのみをCloudFront経由で非公開にする、ということをやってみます。
2021.08.31

はじめに

清水です。Amazon CloudFrontのオリジンアクセスアイデンティティ(Origin Access Identity: OAI)を使ってS3へのアクセスをCloudFrontからのみに限定することができます。CloudFrontディストリビューション作成時などに設定されるデフォルトのOAIを使用するS3バケットポリシーの場合、対象のS3バケット内のオブジェクトはすべてCloudFront経由で公開される状態となります。この状態から、S3バケット内の特定のオブジェクトのみを非公開(CloudFront経由でアクセスできない)にする方法を考えてみました。いろいろと方法はあるかと思いますが、前提としてS3のアクセスコントロールリスト(Access Control List: ACL)は使用しないこととしました。ACLを使うことでアクセス権限管理が複雑になってしまうので、なるべくシンプルに済ませたい、とういう考えです。

今回試してみた具体的な方法は、S3オブジェクトに特定のタグを付与し、このタグ情報を用いてOAIによるアクセス可否を判断するようS3バケットポリシーを編集する、というものです。目的自体は達成できたかと思いますが、タグを外したあとにまた付ける、といったような場合のCloudFrontのキャッシュの挙動には注意が必要です。以下、設定方法や実際の挙動などのまとめになります。

デフォルト状態のOAI用S3バケットポリシーを確認する

まずはCloudFrontのOAI使用時のデフォルトのS3バケットポリシーを確認してみます。なおここでの「デフォルト」は、マネジメントコンソールよりCloudFrontディストリビューション作成時に自動的に設定されるもの、とします。

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E3VRXXXXXXXXXX"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-s3-bucket/*"
        }
    ]
}

なおS3ユーザガイドやCloudFrontデベロッパーズガイドにも、同様のバケットポリシーの記載があります。

ざっくり内容を確認すれば、CloudFront側はPrincipalで指定されたIDでS3にアクセス、このPrincipalに対して対象バケットのオブジェクトにs3:GetObjectを許可することとなります。(結果、CloudFrontからはS3のオブジェクトにアクセスが可能となります。)

オブジェクトに付与されているタグでアクセスを制限する

上記のデフォルトのOAI用のS3バケットポリシーから、S3オブジェクトに付与されているタグの内容でアクセスを制限することを考えます。今回、特定のオブジェクトのみ非公開とするということで、このオブジェクトにはprivate:trueというタグを付けるとします。

S3バケットポリシー側では、このタグの内容(private:true)以外であればアクセス(s3:GetObject)を許可する、としました。具体的には以下となります。(追加箇所をハイライト表示しています。)

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity E3VRXXXXXXXXXX"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-s3-bucket/*",
            "Condition": {
                "StringNotEquals": {
                    "s3:ExistingObjectTag/private": "true"
                }
            }            
        }
    ]
}

タグの内容に基づくポリシーの記載方法は以下などを参考にしました。

実際に挙動を確認してみます。private-object.htmlというオブジェクトをアップロード、この際にprivate:trueのタグを付与しました。

CloudFront経由でアクセスしてみます。以下のように403が返りました。オブジェクトが非公開になっていますね。

$ curl -i https://www.example.com/private-object.html
HTTP/2 403 
content-type: application/xml
date: Tue, 31 Aug 2021 07:57:09 GMT
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 409082e9caee4a1cdc1a950363f5172d.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT12-C2
x-amz-cf-id: yDYb_RudTwuWxBKTG95lEbS2VdAiIRfDpMLeR17uXnW4rWfHplNC7g==

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>1R9C55Y03CNAHGEP</RequestId><HostId>xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</HostId></Error>

公開するためにはオブジェクトのタグをprivate:true以外(もしくはprivateタグを削除)にします。

$ aws s3api put-object-tagging --bucket my-s3-bucket --key private-object.html --tagging '{"TagSet": [{ "Key": "private", "Value": "false" }]}' 
{
    "VersionId": "y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn"
}
$ aws s3api get-object-tagging --bucket my-s3-bucket --key private-object.html{
    "VersionId": "y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn",
    "TagSet": [
        {
            "Key": "private",
            "Value": "false"
        }
    ]
}

今度はCloudFront経由でアクセスできました。

$ curl -i https://www.example.com/private-object.html
HTTP/2 200 
content-type: text/html
content-length: 84
date: Tue, 31 Aug 2021 08:03:10 GMT
last-modified: Tue, 31 Aug 2021 07:56:34 GMT
etag: "e6f4e2329a2c6062bacd8202a383aa5d"
x-amz-version-id: y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn
accept-ranges: bytes
server: AmazonS3
x-cache: Miss from cloudfront
via: 1.1 082329696d49819d97bc7da98006304c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: r8zwlwXHBzWk94T3QtIpDp52G_VG43cdRf5YbXruh8hthOsGFaziwg==

<html>
  <head>
    <title>private</title>
  </head>
  <body>private</body>
</html>

再度、タグをprivate:trueにすることで非公開になりますが、CloudFrontのキャッシュ有効期限内であればオリジン側の公開・非公開にかかわらず、CloudFrontからアクセスが可能な点に注意しましょう。

$ aws s3api put-object-tagging --bucket my-s3-bucket --key private-object.html --tagging '{"TagSet": [{ "Key": "private", "Value": "true" }]}' 
{
    "VersionId": "y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn"
}
$ aws s3api get-object-tagging --bucket my-s3-bucket --key private-object.html{
    "VersionId": "y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn",
    "TagSet": [
        {
            "Key": "private",
            "Value": "true"
        }
    ]
}

以下はCloudFrontエッジにキャッシュがあるため、CloudFront経由のアクセスが可能だったパターンです。

$ curl -i https://www.example.com/private-object.html
HTTP/2 200 
content-type: text/html
content-length: 84
date: Tue, 31 Aug 2021 08:03:10 GMT
last-modified: Tue, 31 Aug 2021 07:56:34 GMT
etag: "e6f4e2329a2c6062bacd8202a383aa5d"
x-amz-version-id: y9I7wTfWyDokhZ9gbearRYMoM1CVt.Yn
accept-ranges: bytes
server: AmazonS3
x-cache: Hit from cloudfront
via: 1.1 082329696d49819d97bc7da98006304c.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT57-C1
x-amz-cf-id: EIs8LqWY9dPFbtsf-wAO--_tiuvb9mXXv6i4AsVCn8vIFzooTLzUaw==
age: 21

<html>
  <head>
    <title>private</title>
  </head>
  <body>private</body>
</html>

以下は、キャッシュがない異なるエッジのため、CloudFront経由のアクセスが不可だったパターンです。

$ curl -i https://www.example.com/private-object.html
HTTP/2 403 
content-type: application/xml
date: Tue, 31 Aug 2021 08:04:36 GMT
server: AmazonS3
x-cache: Error from cloudfront
via: 1.1 786110c43ee4ea47c9ade0944c256de0.cloudfront.net (CloudFront)
x-amz-cf-pop: NRT51-C2
x-amz-cf-id: gK5-13m7hcFqD6HIq2WH8jhTSxAD0A8mHHjgQoVhn1wgcE6pB5L7aA==

<?xml version="1.0" encoding="UTF-8"?>
<Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>CQYW5S95JRH72NFB</RequestId><HostId>yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy</HostId></Error>

まとめ

CloudFrontのOrigin Access Identityを使用してS3バケットポリシーによるアクセス制限をしている環境で、タグを付与した特定のS3オブジェクトのみをCloudFront経由で非公開にする、ということをやってみました。CloudFrontのキャッシュが残っている場合は、S3側でタグを編集して非公開の状態にしても即時に反映されない点は要注意で考慮すべき箇所かと思いますが、S3にファイルアップロードした段階では非公開にしておきたい場合などに利用できるかなと考えています。