Use mTLS to Prevent Users from Bypassing Your Cloudflare Firewall
Intro
Many organisations use Cloudflare as the primary entry point to their applications, with various internal load balancers (LBs) behind it serving traffic. However, users can sometimes bypass Cloudflare’s firewall, Workers, and other security controls by directly targeting your cloud provider’s load balancer. For example:
curl -sk https://echo.elb.$region.amazonaws.com \
-H "Host: echo.test.com"
When this happens, all your allowlists, firewall rules, rate-limiting, and even business-critical features built on Cloudflare become effectively useless — which is a serious security gap.
We recently encountered this problem with one of our clients. Since Cloudflare publicly posts the IP ranges of its edge servers, our initial idea was to restrict ingress access to only Cloudflare IPs. While this works in theory, it comes with a major downside: you must constantly track and update the Cloudflare IP ranges. This means building yet another monitoring system just to detect Cloudflare IP changes and updating your rules accordingly.
It works, but it’s far from ideal.
After further research, we moved to an mTLS-based solution, which solves the problem in a cleaner and more secure way.
Why mTLS Is a Better Solution
-
No need to maintain IP allowlists — Cloudflare IP changes become irrelevant.
-
More secure — There is no list to modify or spoof; only Cloudflare can present the correct client certificate.
-
Simpler and more agile — You can enable mTLS selectively on specific ingresses without affecting the whole cluster.
-
No external monitoring system required — Once configured, it “just works.”
Solutions
To stop users from bypassing Cloudflare and reaching your systems directly, the idea is to ensure that only Cloudflare is allowed to talk to your backend, and everyone else is blocked automatically.
We achieve this by requiring your backend to accept connections only from clients that can prove their identity, and the only client with that identity is Cloudflare. This removes the need to maintain IP lists or build monitoring systems.
In practice, all we do is:
-
Turn on Cloudflare’s feature that proves requests truly come from Cloudflare.
-
Give your backend the trusted certificate that Cloudflare uses for this proof.
-
Tell your backend to accept connections only if this proof is present.
-
Apply this rule only to the routes or services you want to protect.
Once this is set up, any direct request to your load balancer — even if it uses the correct hostname — will be rejected automatically because it cannot provide Cloudflare’s identity.
This keeps Cloudflare as the only entry point to your application, with no extra systems to maintain and no IP ranges to track.
Tests
Before
Before we turned on mTLS, it was perfectly possible to talk directly to the AWS ELB by overriding DNS with curl:
curl -v \
--connect-to $host:443:$elb-name.elb.$region.amazonaws.com:443 \
"https://$host"
The request goes straight to the load balancer and completes successfully:
curl -v \
--connect-to $host:443:$elb-name.elb.$region.amazonaws.com:443 \
"https://$host"
* Connecting to hostname: $elb-name.elb.$region.amazonaws.com
* Connecting to port: 443
* Host $elb-name.elb.$region.amazonaws.com:443 was resolved.
* IPv6: (none)
* IPv4: $ips
* Trying $ips:443...
* Connected to $elb-name.elb.$region.amazonaws.com ($ips) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=$host
* start date: Nov 18 15:15:31 2025 GMT
* expire date: Feb 16 15:15:30 2026 GMT
* subjectAltName: host "$host" matched cert's "$host"
* issuer: C=US; O=Let's Encrypt; CN=R12
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://$host/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: $host]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: $host
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
< HTTP/2 307
So even though Cloudflare is supposed to be the “front door,” anyone who knows the ELB endpoint can still walk in through the back.
After
After we enable mTLS and require a valid client certificate from Cloudflare, the exact same curl trick no longer works:
curl -v \
--connect-to $host:443:$elb-name.elb.$region.amazonaws.com:443 \
"https://$host"
Now the handshake looks like this
* Connecting to hostname: $elb-name.elb.$region.amazonaws.com
* Connecting to port: 443
* Host $elb-name.elb.$region.amazonaws.com:443 was resolved.
* IPv6: (none)
* IPv4: $ips
* Trying $ips:443...
* Connected to $elb-name.elb.$region.amazonaws.com ($ips) port 443
* ALPN: curl offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
* CAfile: /etc/ssl/cert.pem
* CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Request CERT (13):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Certificate (11):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256 / [blank] / UNDEF
* ALPN: server accepted h2
* Server certificate:
* subject: CN=$host
* start date: Nov 18 15:15:31 2025 GMT
* expire date: Feb 16 15:15:30 2026 GMT
* subjectAltName: host "$host" matched cert's "$host"
* issuer: C=US; O=Let's Encrypt; CN=R12
* SSL certificate verify ok.
* using HTTP/2
* [HTTP/2] [1] OPENED stream for https://$host/
* [HTTP/2] [1] [:method: GET]
* [HTTP/2] [1] [:scheme: https]
* [HTTP/2] [1] [:authority: $host]
* [HTTP/2] [1] [:path: /]
* [HTTP/2] [1] [user-agent: curl/8.7.1]
* [HTTP/2] [1] [accept: */*]
> GET / HTTP/2
> Host: $host
> User-Agent: curl/8.7.1
> Accept: */*
>
* Request completely sent off
* LibreSSL SSL_read: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
* Failed receiving HTTP2 data: 56(Failure when receiving data from the peer)
* Connection #0 to host $elb-name.elb.$region.amazonaws.com left intact
curl: (56) LibreSSL SSL_read: error:1404C45C:SSL routines:ST_OK:reason(1116), errno 0
The important part here is that the server asks for a client certificate (Request CERT (13)) and, because curl does not present the Cloudflare client certificate, the connection fails with an SSL error instead of returning a normal HTTP response.
From this point on, the only way to reach your origin successfully is through Cloudflare, which does have the correct client certificate. All direct ELB access is effectively blocked.
Want to Strengthen Your Edge Security?
If your team is exploring mTLS, tightening ingress controls, or improving your overall security posture, we’re here to help. Get in touch for support with Cloudflare integration, certificate management, or secure traffic routing.


