How to Use Amazon CloudFront Origin Access Control (OAC) to Securely Serve S3 Content and Migrate from Legacy OAI

Intermediate

Introduction: Why OAC Matters and Who Should Read This

If you’re serving static content from Amazon S3 through Amazon CloudFront, there’s a critical security question you need to answer: how do you prevent users from bypassing CloudFront and accessing your S3 bucket directly?

For years, the answer was Origin Access Identity (OAI) — a special CloudFront identity that you’d grant read permissions on your S3 bucket. It worked, but it came with significant limitations: no support for SSE-KMS encrypted objects, no support for S3 buckets in non-default regions using the legacy path-style URL format, no PUT/DELETE support for uploads through CloudFront, and no support for Amazon S3 bucket policies that use conditions like aws:SourceVpc.

In 2022, AWS introduced Origin Access Control (OAC) as the recommended replacement. OAC uses AWS Signature Version 4 (SigV4) to sign requests to your S3 origin, supports SSE-KMS, works with all S3 regions, supports all HTTP methods, and integrates cleanly with IAM-based access patterns.

This article is for you if:

  • You’re setting up a new CloudFront + S3 distribution and want to follow best practices
  • You have existing distributions using OAI and need to migrate to OAC
  • You need to serve SSE-KMS encrypted content through CloudFront
  • You want to understand the security model behind OAC at a practical level

Prerequisites

Before you begin, make sure you have the following in place:

  • AWS CLI v2 installed and configured with credentials that have permissions to manage CloudFront, S3, and KMS resources. Verify with aws --version.
  • An existing S3 bucket (or willingness to create one) that will serve as your CloudFront origin.
  • Basic familiarity with CloudFront distributions, S3 bucket policies, and IAM policy syntax.
  • An AWS account with permissions for cloudfront:*, s3:PutBucketPolicy, s3:GetBucketPolicy, and optionally kms:* if you’re using SSE-KMS.

Understanding the Difference: OAI vs. OAC

Before diving into implementation, let’s understand what changed and why it matters.

Origin Access Identity (OAI) — The Legacy Approach

OAI creates a special CloudFront user identity. You reference this identity in your S3 bucket policy to grant read access. Internally, CloudFront sends requests to S3 as this identity. The core problems:

  • No SSE-KMS support: OAI cannot include the required KMS headers for decryption. You were stuck with SSE-S3 or no encryption.
  • Read-only in practice: While technically supporting GET/HEAD, OAI was not designed for PUT/DELETE operations through CloudFront to S3.
  • Limited S3 bucket policy conditions: OAI uses a non-standard principal format (CanonicalUser), which doesn’t work with many IAM policy condition keys.
  • Legacy signing: OAI does not use SigV4 consistently across all regions.

Origin Access Control (OAC) — The Modern Approach

OAC is a standalone CloudFront resource that tells CloudFront how to sign requests sent to your origin. For S3 origins, CloudFront uses SigV4 to sign every request with credentials that your S3 bucket policy can authorize using the standard service principal pattern.

Feature OAI OAC
SSE-S3 Encryption ✅ Yes ✅ Yes
SSE-KMS Encryption ❌ No ✅ Yes
All HTTP Methods (PUT, DELETE) ❌ Limited ✅ Yes
Signing Protocol Mixed (legacy) SigV4
S3 Bucket Policy Principal CanonicalUser CloudFront Service Principal
Dynamic Regions Support ⚠️ Partial ✅ All regions
AWS Recommended ❌ Legacy ✅ Yes

Setting Up OAC from Scratch: Step-by-Step

Let’s walk through creating a new CloudFront distribution with OAC protecting an S3 origin. We’ll do this with the AWS CLI so you can automate it.

Step 1: Create the S3 Bucket and Upload Content

# Create a bucket (choose your region)
aws s3api create-bucket \
    --bucket my-secure-oac-bucket \
    --region us-east-1

# Block all public access (critical for security)
aws s3api put-public-access-block \
    --bucket my-secure-oac-bucket \
    --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

# Upload a test file
echo "<h1>Hello from CloudFront OAC</h1>" > index.html
aws s3 cp index.html s3://my-secure-oac-bucket/index.html

Step 2: Create the Origin Access Control

aws cloudfront create-origin-access-control \
    --origin-access-control-config \
    Name=my-s3-oac,\
Description="OAC for my-secure-oac-bucket",\
SigningProtocol=sigv4,\
SigningBehavior=always,\
OriginAccessControlOriginType=s3

This returns JSON with the OAC’s Id. Save it — you’ll need it when creating or updating the distribution.

Key parameters explained:

  • SigningProtocol: Must be sigv4. This is the only supported protocol for S3 origins.
  • SigningBehavior: always means CloudFront signs every request to the origin. Other options are never (don’t sign) and no-override (sign only if the viewer request doesn’t include an Authorization header).
  • OriginAccessControlOriginType: Set to s3 for S3 origins. OAC also supports mediastore, mediapackagev2, and lambda origin types.

Step 3: Create the CloudFront Distribution

Create a distribution configuration file (distribution-config.json):

{
    "CallerReference": "my-oac-dist-2024",
    "Comment": "Distribution with OAC for S3",
    "Enabled": true,
    "DefaultRootObject": "index.html",
    "Origins": {
        "Quantity": 1,
        "Items": [
            {
                "Id": "myS3Origin",
                "DomainName": "my-secure-oac-bucket.s3.us-east-1.amazonaws.com",
                "OriginAccessControlId": "YOUR_OAC_ID_HERE",
                "S3OriginConfig": {
                    "OriginAccessIdentity": ""
                }
            }
        ]
    },
    "DefaultCacheBehavior": {
        "TargetOriginId": "myS3Origin",
        "ViewerProtocolPolicy": "redirect-to-https",
        "AllowedMethods": {
            "Quantity": 2,
            "Items": ["GET", "HEAD"]
        },
        "CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
        "Compress": true
    },
    "PriceClass": "PriceClass_100",
    "ViewerCertificate": {
        "CloudFrontDefaultCertificate": true
    }
}

Important notes:

  • The DomainName must use the regional S3 endpoint format: BUCKET.s3.REGION.amazonaws.com. Do not use the legacy s3.amazonaws.com global endpoint.
  • Set OriginAccessControlId to the OAC ID from Step 2.
  • S3OriginConfig.OriginAccessIdentity must be an empty string (not omitted) — this tells CloudFront not to use an OAI.
  • The CachePolicyId value 658327ea-f89d-4fab-a63d-7e88639e58f6 is the AWS managed CachingOptimized policy.
aws cloudfront create-distribution \
    --distribution-config file://distribution-config.json

The response includes the distribution’s Id and DomainName (e.g., d1234abcdef8.cloudfront.net). Save both.

Step 4: Update the S3 Bucket Policy

This is the step people forget — and then wonder why they get 403 errors. Your S3 bucket needs a policy that allows the CloudFront service principal to access objects, scoped to your specific distribution.

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipalReadOnly",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::my-secure-oac-bucket/*",
            "Condition": {
                "StringEquals": {
                    "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
                }
            }
        }
    ]
}

Replace 111122223333 with your AWS account ID and EDFDVBD6EXAMPLE with your distribution ID.

aws s3api put-bucket-policy \
    --bucket my-secure-oac-bucket \
    --policy file://bucket-policy.json

The AWS:SourceArn condition is critical. Without it, any CloudFront distribution in any AWS account could potentially access your bucket using the cloudfront.amazonaws.com service principal. Always scope to your specific distribution ARN.

Step 5: Test It

# Wait for distribution to deploy (Status: Deployed)
aws cloudfront get-distribution --id EDFDVBD6EXAMPLE \
    --query 'Distribution.Status'

# Test via CloudFront
curl -I https://d1234abcdef8.cloudfront.net/index.html

# Verify direct S3 access is blocked
curl -I https://my-secure-oac-bucket.s3.us-east-1.amazonaws.com/index.html
# Should return 403 Forbidden

Serving SSE-KMS Encrypted Content with OAC

This is one of the biggest reasons to migrate to OAC. If you need server-side encryption with AWS KMS (SSE-KMS) on your S3 objects, OAI simply cannot decrypt them. OAC handles this natively.

Step 1: Create or Identify Your KMS Key

# Create a KMS key (or use an existing one)
aws kms create-key \
    --description "Key for CloudFront OAC S3 content" \
    --query 'KeyMetadata.KeyId' \
    --output text

Step 2: Upload an Object with SSE-KMS

aws s3 cp index.html s3://my-secure-oac-bucket/index.html \
    --sse aws:kms \
    --sse-kms-key-id arn:aws:kms:us-east-1:111122223333:key/YOUR-KEY-ID

Step 3: Add KMS Permissions to the Key Policy

You need to allow the CloudFront service principal to use the KMS key for decryption. Add this statement to your KMS key policy:

{
    "Sid": "AllowCloudFrontServicePrincipalSSE-KMS",
    "Effect": "Allow",
    "Principal": {
        "Service": "cloudfront.amazonaws.com"
    },
    "Action": [
        "kms:Decrypt",
        "kms:GenerateDataKey*"
    ],
    "Resource": "*",
    "Condition": {
        "StringEquals": {
            "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/EDFDVBD6EXAMPLE"
        }
    }
}

Note: The Resource is * because this statement is inside the key policy itself — it refers to the key the policy belongs to. The AWS:SourceArn condition ensures only your CloudFront distribution can invoke these

Leave a Comment

Your email address will not be published. Required fields are marked *