Back to blog
AWSBest PracticesCloud SecurityNetworkingStorage

CloudFront S3 Origin Is Publicly Accessible: Why It Matters and How to Fix It

A public S3 origin lets attackers bypass CloudFront entirely. Learn how to lock down the bucket with Block Public Access and Origin Access Control (OAC).

TL;DR

This check flags CloudFront distributions whose S3 origin bucket is publicly accessible, which lets users bypass CloudFront entirely and hit the bucket directly. Lock the bucket down with Block Public Access and serve content only through CloudFront using Origin Access Control (OAC).

CloudFront sits in front of your S3 buckets to cache content close to users, terminate TLS, and give you a single place to enforce things like WAF rules, signed URLs, and geo restrictions. None of that helps if someone can skip the CDN and read the bucket directly. That is exactly the gap this check catches: a CloudFront distribution backed by an S3 origin that is still open to the public internet.

When the origin is public, the CDN becomes optional from an attacker's point of view. Anyone who can guess or discover the bucket URL gets the raw objects with none of the controls you carefully configured in CloudFront.


What this check detects

The cloudfront_publics3origin check inspects each CloudFront distribution, identifies its S3 bucket origins, and then evaluates whether those buckets allow public access. A bucket is considered publicly accessible when one or more of the following is true:

  • The bucket's Block Public Access settings are disabled or partially disabled.
  • The bucket policy grants read access to "Principal": "*" or AWS: "*".
  • An ACL grants permissions to the AllUsers or AuthenticatedUsers groups.

If the origin is reachable without going through CloudFront, the check fails.

Note: A public S3 bucket is sometimes intentional, for example a static marketing asset bucket. But once that bucket is wired up as a CloudFront origin, leaving it public almost always points to a misconfiguration rather than a deliberate design choice.


Why it matters

CloudFront is where most teams enforce their edge security posture. A public origin quietly undermines all of it.

You lose your access controls

Signed URLs, signed cookies, and WAF rules are attached to the distribution, not the bucket. If the bucket answers requests directly, an attacker fetches objects from the S3 endpoint and never encounters those controls. Paid content, private downloads, or pre-release assets that you thought were gated behind signed URLs become freely downloadable.

Direct bucket enumeration

S3 bucket names live in a global namespace, and predictable names get found. Once someone has the bucket name, a public bucket lets them list and pull objects. Misconfigured buckets remain one of the most common sources of data leaks in cloud breaches, and a CloudFront origin is a particularly tempting target because it tends to hold production content.

Cost and abuse

Traffic that bypasses CloudFront also bypasses your caching, which means you pay full S3 request and data transfer rates instead of cached CloudFront delivery. A scraper hammering the origin directly can run up an unexpected bill while none of your CDN-level rate limiting applies.

Warning: If you have spent effort configuring WAF, signed URLs, or geo blocking on the distribution and the origin is still public, your security model has a hole regardless of how well the CloudFront side is configured. Treat a public origin as a high priority finding.


How to fix it

The goal is simple: block public access on the bucket and allow only your CloudFront distribution to read from it. The modern way to do that is Origin Access Control (OAC), which replaces the older Origin Access Identity (OAI).

Step 1: Enable Block Public Access on the bucket

Danger: If your bucket genuinely needs to serve some objects publicly outside of CloudFront, blocking public access will break those paths. Confirm that all legitimate traffic goes through the distribution before running this.

aws s3api put-public-access-block \
  --bucket my-cloudfront-origin-bucket \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Step 2: Create an Origin Access Control

aws cloudfront create-origin-access-control \
  --origin-access-control-config \
  '{
    "Name": "oac-my-origin-bucket",
    "OriginAccessControlOriginType": "s3",
    "SigningBehavior": "always",
    "SigningProtocol": "sigv4"
  }'

Note the returned Id. You will attach it to the distribution's origin in the next step.

Step 3: Attach the OAC to your distribution origin

In the console: open the distribution, go to Origins, edit the S3 origin, set Origin access to Origin access control settings (recommended), and select the OAC you created. Save and let the distribution redeploy.

Step 4: Restrict the bucket policy to CloudFront only

Replace any public statements with a policy that allows only your distribution to read objects, scoped by the AWS:SourceArn condition:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AllowCloudFrontServicePrincipalReadOnly",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudfront.amazonaws.com"
      },
      "Action": "s3:GetObject",
      "Resource": "arn:aws:s3:::my-cloudfront-origin-bucket/*",
      "Condition": {
        "StringEquals": {
          "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E1ABCDEF2GHIJK"
        }
      }
    }
  ]
}
aws s3api put-bucket-policy \
  --bucket my-cloudfront-origin-bucket \
  --policy file://bucket-policy.json

Step 5: Verify

Confirm the bucket no longer responds directly while CloudFront still serves content:

# Should return AccessDenied
curl -I https://my-cloudfront-origin-bucket.s3.amazonaws.com/index.html

# Should return 200 OK
curl -I https://d1234abcd.cloudfront.net/index.html

Tip: Use OAC rather than the legacy OAI. OAC supports SSE-KMS encrypted objects, all AWS Regions, and SigV4 signing, and AWS now recommends it for all new distributions. If you are still on OAI, migrating is a good opportunity to clean this up.


How to prevent it from happening again

Fixing one bucket is easy. Stopping the next public origin from shipping takes guardrails.

Enforce Block Public Access at the account level

Turn on account-wide Block Public Access so new buckets are private by default and individual buckets cannot quietly opt back in:

aws s3control put-public-access-block \
  --account-id 111122223333 \
  --public-access-block-configuration \
  "BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true"

Define origins as private in your IaC

If you use Terraform, build the OAC pattern directly into the module so a public origin is impossible to create by accident:

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" "this" {
  name                              = "oac-${aws_s3_bucket.origin.id}"
  origin_access_control_origin_type = "s3"
  signing_behavior                  = "always"
  signing_protocol                  = "sigv4"
}

Add a policy-as-code gate in CI/CD

Run a tool like Checkov, tfsec, or OPA against plans before they merge so a public origin fails the pipeline rather than reaching production. A Checkov run is a one-liner:

checkov -d ./infra --check CKV_AWS_20,CKV2_AWS_6

Tip: Pair the CI/CD gate with a continuous scan in Lensix. Pre-merge checks catch new code, but continuous monitoring catches drift, manual console changes, and buckets created outside your pipeline.


Best practices

  • One bucket, one purpose. Keep CloudFront origin buckets separate from buckets that intentionally serve public content, so a policy change never affects both.
  • Always scope bucket policies with conditions. The AWS:SourceArn condition prevents the "confused deputy" problem where another account's distribution could be pointed at your bucket.
  • Use SSE-KMS for sensitive objects. OAC supports KMS-encrypted origins, giving you an extra layer of access control on top of the bucket policy.
  • Layer WAF on the distribution. Once the origin is locked to CloudFront, attaching AWS WAF actually protects every request rather than being trivially bypassed.
  • Audit regularly. Distributions and origins change over time. A scheduled check catches the bucket someone made public during a debugging session and forgot to revert.

The fix here is not complicated, but the impact of getting it wrong is large. Lock the origin to CloudFront, enforce Block Public Access by default, and gate it in your pipeline so the question never comes up again.