Exam Room · Advanced Security Specialist

Stopping People Bypassing Your CDN

February 07, 2028 · 24 min read

Cloud Security · part of The Exam Room

The situation

A retailer runs its storefront behind CloudFront, with AWS WAF attached carrying managed rule groups, rate-based rules, and a bot control rule set that took two months to tune. The origin is an internet-facing Application Load Balancer, plus an S3 bucket for static assets.

A penetration test found the problem in an afternoon. The ALB’s DNS name is resolvable and it answers requests directly. Certificate transparency logs listed the origin hostname, and the tester simply pointed at it. Every rule in the WAF was bypassed by not going through CloudFront, and the rate limiting that protects the checkout endpoint was bypassed with it.

The S3 bucket has a similar shape: it is not public, but a legacy path grants read to an IAM role several applications assume, so objects can be fetched without going through the distribution.

The requirement now is that the origin accepts requests from CloudFront and from nothing else, without breaking the deployment pipeline that talks to the ALB’s health check endpoint from a build account.

What actually matters

The first thing that matters is that a CDN in front of an origin is a request path, not a boundary. It becomes a boundary only when the origin refuses everything else, and nothing about putting CloudFront in front of an ALB does that on its own.

The second is that hiding the origin is not the same as protecting it. Removing a DNS record, using an obscure hostname, or relying on nobody looking at certificate transparency logs are all measures that delay discovery rather than prevent access. Any control that depends on the origin address being secret fails the moment somebody finds it, and they will.

The third is that S3 origins and load balancer origins have different mechanisms available, and the S3 case is genuinely solved while the ALB case is a set of trade-offs. Origin access control for S3 signs requests from CloudFront and the bucket policy requires that signature, which is a cryptographic control. For an ALB, the equivalents are a shared secret in a header and a network-level restriction, both weaker in different ways.

The fourth is that whatever is chosen has to survive rotation and change. A shared secret header that never rotates is a secret that leaks eventually; an IP allow-list of CloudFront ranges is a list that changes and has to be maintained automatically or it becomes an outage.

Underneath it, the pipeline’s access to the health check endpoint is a legitimate second path, and any design that forgets it will be discovered at the next deployment rather than during the change.

What we’ll filter on

  1. Does it prevent access, or delay discovery?
  2. Is the control cryptographic, network-level, or a shared secret?
  3. Does it need maintenance as AWS ranges change?
  4. Can it be rotated without an outage?
  5. Does it accommodate the legitimate non-CDN paths?
  6. What is the failure mode if it is misconfigured?

The landscape

Origin access control for S3. CloudFront signs requests to the origin with SigV4, and the bucket policy grants access only to the distribution’s service principal with a condition on the distribution ARN. Combined with Block Public Access, the bucket becomes reachable only through the distribution. This is the solved case, it needs no secret and no address list, and it supersedes the older origin access identity.

Origin access control for other origin types. OAC also supports Lambda function URLs, MediaStore, MediaPackage and S3 origins configured as websites, signing requests the same way. It does not cover an Application Load Balancer, which is why the ALB case needs a different answer.

A shared secret custom header. CloudFront adds a custom header with a secret value to every origin request; a WAF rule on the ALB blocks any request lacking it. It is simple, effective against a tester who has found the origin hostname, and it is a bearer secret: anyone who obtains it can replay it. Rotation means adding the new value as an accepted second value, updating CloudFront, then removing the old, which is doable without an outage if planned.

Security groups referencing the CloudFront managed prefix list. AWS publishes a managed prefix list of CloudFront origin-facing addresses, and a security group rule can reference it directly rather than enumerating ranges. This restricts the ALB to accepting connections from CloudFront’s fleet, and AWS maintains the list. Its limitation is that it permits all CloudFront distributions, including somebody else’s, so it is a strong reduction and not an identity check.

VPC origins. CloudFront can send requests to an origin inside a VPC that has no public address at all, over an AWS-managed private path. This removes the internet-facing origin entirely rather than restricting it, which is the strongest available answer for a load balancer origin and the one to reach for when the architecture allows an internal ALB.

AWS WAF on the ALB. A second web ACL attached to the ALB itself, carrying the rule that enforces whichever of the above is chosen, and providing defence in depth if the edge is bypassed. Rules that are expensive at scale belong at the edge; the origin ACL can be minimal.

Removing public DNS. Deleting the origin’s DNS record. It is worth doing for tidiness and it is not a control, because the ALB’s AWS-assigned name still resolves.

S3 bucket policies with condition keys. Beyond OAC, conditions such as aws:SourceVpce or aws:SourceIp restrict which paths may read objects, and are how the legacy IAM role’s direct access gets removed or narrowed.

Evaluation

Side by side

Control Origin type Strength Maintenance Distinguishes your distribution
Origin access control S3, Lambda URL, Media* Cryptographic None
VPC origins ALB in a VPC No public path at all None
Shared secret header Any Bearer secret Rotation
CloudFront managed prefix list Any Network-level AWS maintains it ✗ any distribution
Deleting DNS records Any None n/a
WAF on the origin ALB, API GW Enforces the above Rules Depends

The two rows without maintenance and without a shared secret are the ones to want. OAC handles the S3 origin completely. VPC origins handle the ALB completely by removing the public path. Where an internal ALB is not possible, the practical answer is the combination of the prefix list and the secret header, because each covers the other’s weakness: the prefix list stops anything not coming from CloudFront, and the secret distinguishes your distribution from anyone else’s.

The solution

Origin access control for the bucket, VPC origins for the load balancer, and the header-plus-prefix-list combination as the fallback if the ALB has to stay internet-facing.

The S3 origin is the easy half and should go first, because it is complete rather than partial. Configure origin access control on the distribution’s S3 origin, set the bucket policy to grant s3:GetObject only to the CloudFront service principal with a condition on the distribution ARN, and turn on Block Public Access at the account level. Then deal with the legacy IAM role: establish which applications actually need direct object access, and either narrow the policy to the specific prefixes they use or move them to fetching through the distribution. A bucket policy that allows a broad role is a bypass path even with OAC configured.

The ALB is the interesting half. If the load balancer can become internal, VPC origins is the answer, and it is a different quality of answer from the others: there is no public address to find, so there is nothing to bypass and nothing to maintain. CloudFront reaches the internal ALB over the AWS-managed path, and a penetration test has nothing to point at.

If the ALB must stay internet-facing, use both remaining controls together. Add a security group rule allowing inbound only from the CloudFront origin-facing managed prefix list, which stops everything that is not CloudFront and is maintained by AWS. Then add a custom header with a secret from Secrets Manager, and a WAF rule on the ALB blocking requests without it, which stops requests from other people’s CloudFront distributions. Neither alone is sufficient: the prefix list permits any distribution, and the header alone permits anyone who obtains the value.

Plan the header rotation before deploying it, because a secret that cannot be rotated will not be. The rotation is a three-step sequence: configure the WAF rule to accept either the old or the new value, update the CloudFront origin custom header to the new value, then remove the old from the rule. Schedule it, and automate it with Secrets Manager rotation so it happens whether or not anyone remembers.

Handle the pipeline explicitly. The build account’s health check calls are a legitimate second path, and they should be permitted by a specific rule rather than by leaving the origin open: a security group rule for the build account’s NAT addresses, or better, moving the health check to something that does not require reaching the ALB from outside.

Keep a minimal WAF on the origin regardless. The expensive rule groups belong at the edge where they run once and cache; the origin ACL needs only the enforcement rule and a broad rate limit, which is the defence in depth that matters if the edge configuration is ever wrong.

Why not rely on the prefix list alone. It is the answer people reach for because AWS maintains the list, and it permits every CloudFront distribution in the world. An attacker creates their own distribution pointing at your origin and is inside the allow-list.

Why not rely on the secret header alone. It distinguishes your distribution and it is a bearer token in a header, travelling to your origin on every request. Paired with the prefix list it is the practical answer; alone it is one leak from being useless.

Worked example

The S3 half takes a morning. OAC on the distribution, a bucket policy conditioned on the distribution ARN, and Block Public Access. The legacy role turns out to need two prefixes rather than the whole bucket, which is a policy narrowing rather than a removal.

The ALB cannot become internal this quarter, because a third-party payment provider posts callbacks directly to it on a path that does not go through CloudFront. That is a genuine second public path, and discovering it is the reason the change did not simply proceed to VPC origins.

The fallback goes in instead. The prefix list rule is one security group entry. The secret header takes longer, mostly on getting the WAF rule to accept two values during rotation, which is the part that makes the control sustainable and the part a first implementation usually skips.

The payment callback path gets its own treatment: a separate listener rule on the ALB restricted to the provider’s published address ranges, with its own WAF rules, so it is a deliberate exception with controls rather than a hole.

The pentest is repeated four weeks later. The origin hostname is still discoverable in certificate transparency logs, which was never going to change, and requests to it return a WAF block. The tester then creates their own CloudFront distribution pointing at the origin, which passes the prefix list and fails on the missing header, which is exactly the scenario the pairing exists for and exactly the one the prefix list alone would have missed.

The payment callback path is flagged as the remaining exposure, correctly, and moving that provider onto a dedicated endpoint becomes next quarter’s work along with making the ALB internal.

What’s worth remembering

  1. A CDN in front of an origin is a request path, not a boundary; it becomes a boundary only when the origin refuses everything else, which nothing about adding CloudFront does on its own.
  2. Hiding an origin is not protecting it. Certificate transparency logs, DNS history and the AWS-assigned load balancer name all leak the address, so any control depending on secrecy of the address fails.
  3. Origin access control solves the S3 case cryptographically, with no secret and no address list, and it also covers Lambda function URLs and the Media services; it does not cover an ALB.
  4. VPC origins is the strongest ALB answer because it removes the public path entirely rather than restricting it, leaving nothing to discover.
  5. The CloudFront managed prefix list permits every CloudFront distribution, not just yours, so pairing it with a secret custom header is what makes the fallback sound; either alone has a specific bypass.
  6. Plan the header rotation before deploying it, by having the origin rule accept two values during the change, or the secret will never be rotated.

These posts are LLM-aided. Backbone, original writing, and structure by Craig. Research and editing by Craig + LLM. Proof-reading by Craig.