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 optionallykms:*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 besigv4. This is the only supported protocol for S3 origins.SigningBehavior:alwaysmeans CloudFront signs every request to the origin. Other options arenever(don’t sign) andno-override(sign only if the viewer request doesn’t include an Authorization header).OriginAccessControlOriginType: Set tos3for S3 origins. OAC also supportsmediastore,mediapackagev2, andlambdaorigin 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
DomainNamemust use the regional S3 endpoint format:BUCKET.s3.REGION.amazonaws.com. Do not use the legacys3.amazonaws.comglobal endpoint. - Set
OriginAccessControlIdto the OAC ID from Step 2. S3OriginConfig.OriginAccessIdentitymust be an empty string (not omitted) — this tells CloudFront not to use an OAI.- The
CachePolicyIdvalue658327ea-f89d-4fab-a63d-7e88639e58f6is 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