CloudFront Distribution Uses Origin Access Control (OAC) for All S3 Origins
Overview
This check verifies that your CloudFront distributions use Origin Access Control (OAC) when serving content from S3 buckets. OAC ensures that users can only access your S3 content through CloudFront, not by going directly to the S3 bucket URL.
Think of OAC as a secure handshake between CloudFront and S3. Without it, anyone who knows your S3 bucket URL could bypass CloudFront entirely.
Risk
Without OAC, your S3 objects remain publicly accessible outside of CloudFront. This creates several security issues:
- Data exposure: Attackers can access files directly from S3, bypassing your CloudFront security controls
- Lost protections: You lose the benefits of AWS WAF, rate limiting, and detailed access logging
- Cost abuse: Direct S3 access can lead to unexpected data transfer charges
- Tampering risk: Limited support for signed uploads and SSE-KMS encryption when not using OAC
Remediation Steps
Prerequisites
You need:
- Access to the AWS Console with permissions to modify CloudFront and S3
- The name of the affected CloudFront distribution and S3 bucket
Required IAM permissions
Your IAM user or role needs these permissions:
cloudfront:CreateOriginAccessControlcloudfront:GetDistributioncloudfront:UpdateDistributions3:GetBucketPolicys3:PutBucketPolicy
AWS Console Method
Step 1: Create an Origin Access Control
- Open the CloudFront console
- In the left menu, click Security then Origin access
- Click Create control setting
- Enter a name (e.g.,
my-bucket-oac) - For Signing behavior, select Sign requests (recommended)
- For Origin type, select S3
- Click Create
Step 2: Attach OAC to your distribution
- Go back to Distributions in the CloudFront console
- Click your distribution ID to open it
- Click the Origins tab
- Select your S3 origin and click Edit
- Under Origin access, select Origin access control settings (recommended)
- Choose the OAC you created in Step 1
- Click Save changes
- CloudFront will show a banner with a bucket policy. Click Copy policy
Step 3: Update your S3 bucket policy
- Open the S3 console
- Click on your bucket name
- Go to the Permissions tab
- Under Bucket policy, click Edit
- Paste the policy you copied from CloudFront
- Click Save changes
Step 4: Remove public access (if present)
- Still on the bucket's Permissions tab
- Under Block public access, click Edit
- Check Block all public access
- Click Save changes and confirm
AWS CLI (optional)
Step 1: Create an Origin Access Control
aws cloudfront create-origin-access-control \
--region us-east-1 \
--origin-access-control-config \
Name=my-bucket-oac,\
Description="OAC for S3 origin",\
SigningProtocol=sigv4,\
SigningBehavior=always,\
OriginAccessControlOriginType=s3
Save the Id from the output. You will need it in the next step.
Step 2: Get your current distribution configuration
aws cloudfront get-distribution-config \
--region us-east-1 \
--id <distribution-id> > dist-config.json
Step 3: Modify the configuration
Edit dist-config.json:
- Find the S3 origin in the
Origins.Itemsarray - Add or update
OriginAccessControlIdwith the OAC ID from Step 1 - Remove any existing
S3OriginConfig.OriginAccessIdentityvalue (set to empty string) - Copy the
ETagvalue and remove theETagfield from the file - Remove the outer
Distributionwrapper, keeping onlyDistributionConfig
Step 4: Update the distribution
aws cloudfront update-distribution \
--region us-east-1 \
--id <distribution-id> \
--if-match <etag-value> \
--distribution-config file://dist-config.json
Step 5: Update the S3 bucket policy
Create a file named bucket-policy.json:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::<your-bucket-name>/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::<account-id>:distribution/<distribution-id>"
}
}
}
]
}
Apply the policy:
aws s3api put-bucket-policy \
--region us-east-1 \
--bucket <your-bucket-name> \
--policy file://bucket-policy.json
Step 6: Block public access on the bucket
aws s3api put-public-access-block \
--region us-east-1 \
--bucket <your-bucket-name> \
--public-access-block-configuration \
BlockPublicAcls=true,\
IgnorePublicAcls=true,\
BlockPublicPolicy=true,\
RestrictPublicBuckets=true
CloudFormation (optional)
This template creates a CloudFront distribution with OAC configured for an S3 origin:
AWSTemplateFormatVersion: '2010-09-09'
Description: CloudFront distribution with Origin Access Control for S3
Parameters:
BucketName:
Type: String
Description: Name of the S3 bucket to use as origin
Resources:
S3Bucket:
Type: AWS::S3::Bucket
Properties:
BucketName: !Ref BucketName
PublicAccessBlockConfiguration:
BlockPublicAcls: true
BlockPublicPolicy: true
IgnorePublicAcls: true
RestrictPublicBuckets: true
OriginAccessControl:
Type: AWS::CloudFront::OriginAccessControl
Properties:
OriginAccessControlConfig:
Name: !Sub '${BucketName}-oac'
Description: OAC for S3 bucket access
OriginAccessControlOriginType: s3
SigningBehavior: always
SigningProtocol: sigv4
CloudFrontDistribution:
Type: AWS::CloudFront::Distribution
Properties:
DistributionConfig:
Enabled: true
DefaultRootObject: index.html
Origins:
- Id: S3Origin
DomainName: !GetAtt S3Bucket.RegionalDomainName
S3OriginConfig:
OriginAccessIdentity: ''
OriginAccessControlId: !GetAtt OriginAccessControl.Id
DefaultCacheBehavior:
TargetOriginId: S3Origin
ViewerProtocolPolicy: redirect-to-https
CachePolicyId: 658327ea-f89d-4fab-a63d-7e88639e58f6
BucketPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref S3Bucket
PolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowCloudFrontServicePrincipal
Effect: Allow
Principal:
Service: cloudfront.amazonaws.com
Action: s3:GetObject
Resource: !Sub '${S3Bucket.Arn}/*'
Condition:
StringEquals:
AWS:SourceArn: !Sub 'arn:aws:cloudfront::${AWS::AccountId}:distribution/${CloudFrontDistribution}'
Outputs:
DistributionId:
Value: !Ref CloudFrontDistribution
DistributionDomainName:
Value: !GetAtt CloudFrontDistribution.DomainName
OACId:
Value: !GetAtt OriginAccessControl.Id
Deploy the stack:
aws cloudformation deploy \
--region us-east-1 \
--stack-name cloudfront-oac-stack \
--template-file template.yaml \
--parameter-overrides BucketName=my-secure-bucket
Terraform (optional)
terraform {
required_providers {
aws = {
source = "hashicorp/aws"
version = ">= 5.0"
}
}
}
provider "aws" {
region = "us-east-1"
}
variable "bucket_name" {
description = "Name of the S3 bucket"
type = string
}
resource "aws_s3_bucket" "origin" {
bucket = var.bucket_name
}
resource "aws_s3_bucket_public_access_block" "origin" {
bucket = aws_s3_bucket.origin.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_cloudfront_origin_access_control" "oac" {
name = "${var.bucket_name}-oac"
description = "OAC for S3 bucket access"
origin_access_control_origin_type = "s3"
signing_behavior = "always"
signing_protocol = "sigv4"
}
resource "aws_cloudfront_distribution" "cdn" {
enabled = true
default_root_object = "index.html"
origin {
domain_name = aws_s3_bucket.origin.bucket_regional_domain_name
origin_id = "S3Origin"
origin_access_control_id = aws_cloudfront_origin_access_control.oac.id
}
default_cache_behavior {
target_origin_id = "S3Origin"
viewer_protocol_policy = "redirect-to-https"
allowed_methods = ["GET", "HEAD"]
cached_methods = ["GET", "HEAD"]
forwarded_values {
query_string = false
cookies {
forward = "none"
}
}
}
restrictions {
geo_restriction {
restriction_type = "none"
}
}
viewer_certificate {
cloudfront_default_certificate = true
}
}
data "aws_caller_identity" "current" {}
resource "aws_s3_bucket_policy" "origin" {
bucket = aws_s3_bucket.origin.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Sid = "AllowCloudFrontServicePrincipal"
Effect = "Allow"
Principal = {
Service = "cloudfront.amazonaws.com"
}
Action = "s3:GetObject"
Resource = "${aws_s3_bucket.origin.arn}/*"
Condition = {
StringEquals = {
"AWS:SourceArn" = aws_cloudfront_distribution.cdn.arn
}
}
}
]
})
}
output "distribution_id" {
value = aws_cloudfront_distribution.cdn.id
}
output "distribution_domain_name" {
value = aws_cloudfront_distribution.cdn.domain_name
}
output "oac_id" {
value = aws_cloudfront_origin_access_control.oac.id
}
Deploy:
terraform init
terraform apply -var="bucket_name=my-secure-bucket"
Verification
After completing the remediation:
- Open the CloudFront console and click on your distribution
- Go to the Origins tab
- Verify that your S3 origin shows Origin access control with your OAC name
- Test that content loads correctly through your CloudFront URL
- Confirm that direct S3 bucket URLs return "Access Denied"
CLI verification commands
Check that OAC is attached to the distribution:
aws cloudfront get-distribution \
--region us-east-1 \
--id <distribution-id> \
--query 'Distribution.DistributionConfig.Origins.Items[*].{Origin:Id,OAC:OriginAccessControlId}'
Verify the bucket blocks public access:
aws s3api get-public-access-block \
--region us-east-1 \
--bucket <your-bucket-name>
All four settings should be true.
Additional Resources
- AWS Documentation: Restricting access to an Amazon S3 origin
- AWS Documentation: Creating an origin access control
- AWS Documentation: Migrating from OAI to OAC
Notes
-
Migrating from OAI: If you are currently using the older Origin Access Identity (OAI), AWS recommends migrating to OAC. OAC provides better security and supports additional features like SSE-KMS encryption and PUT/DELETE operations.
-
Multiple origins: If your distribution has multiple S3 origins, you need to create and attach an OAC to each one. You can reuse the same OAC across origins if they have similar access requirements.
-
Distribution update time: After updating the distribution, changes typically propagate within a few minutes, but can take up to 15 minutes to fully deploy across all edge locations.
-
SSE-KMS buckets: If your S3 bucket uses SSE-KMS encryption, you must also grant CloudFront permission to use the KMS key. Add the CloudFront service principal to your KMS key policy.