Skip to main content
emnode / learn
Compliance

Protect CloudFront distributions and origins

One capability across the CloudFront edge: lock origins so the distribution is the only door, enforce strong HTTPS to viewers and origins, and turn on the logging, WAF and access controls that make a distribution genuinely hardened rather than merely working.

14 min·10 sections·AWS

Last reviewed

Hardening CloudFront: the basics

Why a distribution that works can still be wide open

CloudFront is the content delivery network that caches and serves your content from edge locations close to your users. A distribution has two trust boundaries that both need hardening: the viewer side (the connection between a browser and CloudFront) and the origin side (the connection between CloudFront and the S3 bucket, load balancer or Lambda URL behind it). A distribution can serve traffic perfectly while leaving either boundary weaker than your standards require, which is why a working distribution is not the same as a hardened one.

AWS Security Hub turns each weakness into its own control, so a single distribution can fail several at once. On the origin side, CloudFront.13 checks for origin access control (OAC) on S3 origins, CloudFront.16 covers Lambda URL origins, CloudFront.9 checks that traffic to custom origins is encrypted, and CloudFront.12 catches a dangling S3 origin pointing at a deleted bucket. On the viewer side, CloudFront.3 requires HTTPS, CloudFront.10 forbids deprecated SSL protocols to the origin, and CloudFront.15 requires a recommended minimum TLS policy. CloudFront.1 (default root object), CloudFront.5 (access logging), CloudFront.6 (WAF association) and CloudFront.17 round out the operational and access-control hardening.

The reason this matters is that most of these failures are silent. The padlock is green, the page loads, nothing breaks, yet the S3 origin may be reachable directly around the CDN, the TLS floor may be set to a decade-old protocol, or the distribution may be one re-created bucket name away from serving an attacker's content on your trusted domain. The job is to make the distribution the only door to its origin, enforce strong encryption on both sides, and turn on the logging and access controls that let you see and govern what flows through the edge.

In this lesson you will learn the two trust boundaries of a CloudFront distribution (viewer and origin), why a working distribution can still fail several hardening controls, and how to lock origins, enforce strong HTTPS, and enable the logging and access controls that make the edge genuinely defended. The Controls this lesson covers section lists every Security Hub control in this capability, each linking to a deep page with the exact check and a copy-and-paste fix.

Fun fact

The bucket that was private for three years

A media company assumed their CloudFront distribution made their S3 origin private, since viewers only ever saw the CloudFront URL. A penetration test proved otherwise: the bucket name appeared in a JavaScript bundle, and the bucket itself had no policy restricting access to the distribution. The tester pulled every object directly from the S3 endpoint, bypassing the WAF, the signed-URL logic and the geo-blocking the company had carefully configured at the edge. The content had been directly reachable for the entire three years the distribution existed. The fix was a single origin access control and a one-line bucket policy condition, the kind of gap that breaks nothing and protects everything.

Finding edge gaps across a distribution fleet

Devon owns the platform team's CloudFront fleet. Security Hub flags a cluster of CloudFront findings across several distributions: missing origin access control, a deprecated TLS floor, and one origin pointing at a bucket a different team deleted last quarter.

Rather than work the findings one by one, he starts by checking whether each S3-origin distribution actually locks its origin, since a missing origin access control means the bucket is reachable around the CDN.

Check the distribution's origins for an origin access control. An empty OriginAccessControlId (and no legacy OAI) means the S3 bucket is open around the CDN.

$ aws cloudfront get-distribution-config --id E2QWRUHAPOMQZL --query 'DistributionConfig.Origins.Items[].{Domain:DomainName,OAC:OriginAccessControlId,OAI:S3OriginConfig.OriginAccessIdentity}' --output table
-------------------------------------------------------------------------
| Domain | OAC | OAI |
+-------------------------------------+----------+----------------------+
| downloads.s3.amazonaws.com | | |
+-------------------------------------+----------+----------------------+
# Both OAC and OAI empty: the S3 bucket is reachable directly, around every edge protection.

An S3 origin with neither OAC nor OAI set is exactly what CloudFront.13 fails on. The bucket answers anyone, not just CloudFront.

How CloudFront hardening worksdeep dive

On the origin side, origin access control (OAC) has CloudFront sign requests to the origin with Signature Version 4, presenting the cloudfront.amazonaws.com service principal. The trust is closed on the S3 side by a bucket policy that grants s3:GetObject to that principal only when the request's AWS:SourceArn matches your specific distribution ARN, so exactly one distribution can read exactly one bucket. OAC supersedes the legacy origin access identity (OAI) because it is a real SigV4 signer: it can read SSE-KMS-encrypted objects (with a matching kms:Decrypt grant on the key policy) and works where OAI could not. The dangling-origin check (CloudFront.12) is separate: because S3 bucket names are globally reusable, a distribution pointing at a deleted bucket is a subdomain-takeover vector that anyone can fill by re-creating the name.

On the viewer side, the security policy CloudFront uses for the TLS handshake encodes both the minimum protocol version and the cipher generation. CloudFront.15 fails anything below the recommended set (TLSv1.2_2021, TLSv1.2_2025, TLSv1.3_2025) and only scopes distributions using a custom certificate served via SNI. MinimumProtocolVersion is nested inside ViewerCertificate, so you cannot patch it in isolation: you get-distribution-config (which returns the full config plus an ETag), edit the field, and update-distribution with the complete config and the matching --if-match ETag. Omitting fields resets them, which is why this is best managed as code.

The same get-edit-put-with-ETag pattern applies to almost every CloudFront change, including attaching a web ACL (CloudFront.6), enabling access logging (CloudFront.5), setting a default root object (CloudFront.1), enforcing an HTTPS viewer protocol policy (CloudFront.3) and requiring encrypted origin connections (CloudFront.9, CloudFront.10). Each change redeploys to all edge locations before it takes effect, so wait for the distribution to reach Deployed status before testing. Security Hub re-evaluates on its own cycle, so a fix shows PASSED on the next evaluation, not instantly.

What is the impact of an unhardened distribution?

The primary impact on the origin side is that the bucket or backend is reachable directly, bypassing every protection applied at CloudFront. WAF rules, signed URLs, signed cookies and geo-restrictions all live at the edge, so a directly-reachable origin ignores them entirely. The distribution can look fully locked down while the origin behind it is wide open, which is precisely why these are silent findings rather than visible outages. A dangling origin is worse still: an outsider who claims the freed bucket name serves their own content on your domain, behind your TLS certificate and brand.

On the viewer side, a weak TLS floor widens the attack surface for downgrade and weak-cipher attacks: a distribution on TLSv1 or TLSv1.1 completes handshakes using protocols the IETF deprecated in 2021. Nothing looks broken, the padlock is green, but the minimum bar on the encryption is set lower than your standards and your auditors expect, leaving room for an attacker on the network path to force a weaker connection.

There is a logging and forensics dimension too. CloudFront access logs record requests that go through the distribution; direct-to-origin requests never appear there, so an exposure of private content can happen with no record of who accessed it unless S3 server-access logging or CloudTrail data events are separately enabled. These controls map to NIST 800-53 configuration and data-protection requirements and PCI DSS, so an open finding on a distribution fronting regulated data becomes an audit exception with evidence-gathering and remediation deadlines attached, all for fixes that mostly cost nothing.

How do you harden the edge safely?

Work the capability as one loop rather than chasing individual findings. The order matters: lock origins first (highest impact), then enforce strong encryption on both sides, then turn on logging and access controls, then enforce the whole pattern in code so it does not recur.

1. Lock every origin to its distribution

For S3 origins, create an OAC (origin type s3, signing protocol sigv4, signing behaviour always), attach it to the origin, and lock the bucket policy to the distribution's ARN with an AWS:SourceArn condition; verify a raw S3 GET now returns 403 while the CloudFront URL still works. For Lambda URL origins use the equivalent OAC pattern. For any dangling origin, confirm with head-bucket, then re-create the bucket name under your own account immediately (or remove the origin) to take it out of the global pool before an attacker can.

2. Enforce strong HTTPS on both trust boundaries

On the viewer side, set the viewer protocol policy to redirect-to-https or https-only and raise MinimumProtocolVersion to a recommended value (TLSv1.2_2021 or newer). On the origin side, require encrypted connections to custom origins and forbid deprecated SSL protocols. These are zero-downtime config edits; the only thing to check before raising the TLS floor is that no flagged distribution legitimately serves genuinely ancient pre-2014 clients, which public web, marketing and API distributions never do.

3. Turn on logging, WAF and a default root object

Enable CloudFront access logging so you have a record of edge requests, associate a WAF web ACL so the distribution inspects inbound traffic, and set a default root object so a request to the distribution root does not expose a directory listing. Where private content is served, use signed URLs or signed cookies backed by trusted key groups rather than the legacy key-pair model. These are the operational controls that turn a locked distribution into a monitored, governed one.

4. Make the secure configuration the enforced default

A one-off fix will not hold if new distributions ship without it. Bake OAC, the recommended TLS policy, HTTPS-only viewer policy, logging and WAF association into your infrastructure-as-code modules so every new distribution is compliant by default, and back it with AWS Config rules so any drift re-raises the finding automatically. The goal is that shipping an unhardened distribution takes deliberate effort, not that someone remembers to lock it afterward.

# 1. Lock an S3 origin: only THIS distribution may read the bucket.
cat > bucket-policy.json <<'JSON'
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "AllowCloudFrontServicePrincipalReadOnly",
    "Effect": "Allow",
    "Principal": { "Service": "cloudfront.amazonaws.com" },
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-downloads-bucket/*",
    "Condition": { "StringEquals": {
      "AWS:SourceArn": "arn:aws:cloudfront::111122223333:distribution/E2QWRUHAPOMQZL"
    } }
  }]
}
JSON
aws s3api put-bucket-policy --bucket my-downloads-bucket --policy file://bucket-policy.json

# 2. Raise the minimum TLS version (the field is nested in ViewerCertificate, so send the whole config back).
aws cloudfront get-distribution-config --id E2QWRUHAPOMQZL > dist.json
ETAG=$(python3 -c "import json;print(json.load(open('dist.json'))['ETag'])")
# ... edit dist.json: ViewerCertificate.MinimumProtocolVersion = TLSv1.2_2021, ViewerProtocolPolicy = redirect-to-https ...
aws cloudfront update-distribution --id E2QWRUHAPOMQZL \
  --distribution-config file://distribution-config.json --if-match "$ETAG"

# 3. Confirm the second door is shut: a raw S3 GET should now return 403.
curl -s -o /dev/null -w '%{http_code}\n' https://my-downloads-bucket.s3.amazonaws.com/file.pdf

Quick quiz

Question 1 of 5

Security Hub fails several CloudFront controls on a distribution that serves traffic perfectly. What is the most efficient way to think about them?

You can now treat CloudFront hardening as one capability rather than a scatter of findings: lock every origin to its distribution with OAC and a scoped policy, fix dangling origins before an attacker claims the name, enforce strong HTTPS on both the viewer and origin sides, turn on logging, WAF and a default root object, and bake the secure configuration into infrastructure-as-code so new distributions ship hardened by default. The Controls this lesson covers section below links every control in this group to its deep page and fix.

Back to the library

Controls this lesson covers

One capability, many AWS Security Hub controls. This lesson is the shared playbook; each control below keeps its own deep page with the exact check, severity and a copy-and-paste fix.