Your WAF is decorative if the origin still answers
A WAF only inspects traffic that goes through it. If your load balancer's hostname accepts connections from anywhere, an attacker skips the WAF with one DNS lookup — and nothing in the dashboard will tell you.
Put Cloudflare in front of your application and you get a WAF, rate limiting, geo rules and DDoS absorption. The dashboard fills with green. Traffic is inspected.
Then somebody runs this:
curl https://my-alb-1457563835.us-east-1.elb.amazonaws.com/
If that returns your application, every control you just configured has been bypassed. Not defeated, not evaded — simply not involved. The request never went near Cloudflare.
This is the most common gap I see in this architecture, and it is invisible from the WAF’s own reporting, because a request that never arrives cannot appear in a log of blocked requests.
Two paths, one origin
An ALB’s DNS name is public and predictable in shape. It appears in certificate transparency logs, in old DNS records, in Shodan-style scan data, in a stale Terraform output someone pasted into a ticket. Finding it is not an exploit; it is a lookup.
Closing it
Two controls, and you want both.
1. Restrict the load balancer to the CDN’s addresses
Cloudflare publishes its egress ranges. The ALB security group should accept 443 from those ranges and nothing else:
resource "aws_security_group" "alb" {
ingress {
from_port = 443
to_port = 443
protocol = "tcp"
cidr_blocks = var.cloudflare_ipv4_ranges # NOT 0.0.0.0/0
description = "HTTPS from Cloudflare edge only"
}
}
Fetch the list rather than hardcoding it — Cloudflare does change it, and a stale entry shows up as “some visitors get a timeout” rather than as any obvious error. I generate the Terraform variable and the nginx set_real_ip_from list from one fetch of the published list so the two cannot drift apart.
2. Turn on Authenticated Origin Pulls
Address filtering is necessary but not sufficient: anyone else’s Cloudflare account also originates from those same ranges. Authenticated Origin Pulls makes Cloudflare present a client certificate that your origin verifies, so the origin accepts your zone rather than the edge in general.
Together: the network layer rejects everyone outside the CDN, and the TLS layer rejects everyone inside it who is not you.
Test it, don’t assume it
This is the part people skip. The configuration is easy; the proof is what tells you it worked.
curl -m 10 "https://$(terraform output -raw alb_dns_name)/healthz"; echo "exit=$?"
What you want:
curl: (28) Connection timed out after 10010 milliseconds
exit=28
Exit 28 is a timeout, not a rejection, and the difference matters. A 403 or a TCP reset confirms something is listening. A timeout means packets are dropped with no reply at all — a scanner learns nothing, not even that the host exists. That is the security group discarding traffic rather than the application refusing it.
If instead you get a 200, or even a 502 from the load balancer, the origin is reachable and the WAF is optional.
What the WAF still cannot do
Worth being clear about, because closing the bypass does not make the WAF a solution.
On the free Cloudflare plan the Managed and OWASP rulesets are unavailable — they fail with “not entitled to execute this managed ruleset.” I wrote a substitute custom rule instead. On its first test it returned 200 for an obvious injection payload.
The reason: Cloudflare’s http.request.uri.query is the raw query string. My rule tested contains "' or '" against a value that was actually '%20or%20'. It could never have matched. Wrapping the field in url_decode() fixed it.
Which is the same lesson as the origin bypass, one layer up: a rule that is deployed, visible and green can still be doing nothing at all. The dashboard reports that a rule exists, not that it works. The only way to know is to send the payload and watch it get blocked.
And even working correctly, a substring rule stops the demo payload and naive scanners. It does not do grammar-aware detection, and an attacker who encodes or fragments the query walks straight past. Saying “Cloudflare WAF blocked SQL injection” on the strength of that would be overclaiming — the parameterised query in the application is what actually prevents injection. The WAF buys time against automation.
The checklist
- Load balancer security group accepts the CDN’s ranges only, never
0.0.0.0/0 - Ranges are fetched from the published list, not pasted once
- Authenticated Origin Pulls enabled
-
curlagainst the origin hostname times out — verified, not assumed - Every WAF rule tested by sending the payload it is supposed to block
- No other public entry point: no leftover
Arecord, no debug port, no old environment still pointing at the same target group
That last one catches people. The origin does not have to be your load balancer. It can be a health-check endpoint on a NodePort, a staging environment sharing the backend, or a DNS record for a service you decommissioned but never deleted.
A WAF an attacker can walk around is decorative. Configuring it is the easy half; proving there is no other way in is the half that makes it a control.
From building a multi-AZ platform behind Cloudflare and then attacking it deliberately — the full write-up has the measured results and the defects found along the way.
References
- Cloudflare IP ranges — the canonical list, fetch it rather than copying it
- Authenticated Origin Pulls
- Cloudflare Rules language fields — note which fields are raw and which are normalised
- AWS: security groups for your ALB
- curl exit codes — 28 is
OPERATION_TIMEDOUT