Why aren’t Authenticated Origin Pulls a third option? Verifying Cloudflare’s client cert is less troublesome than maintaining a list of Cloudflare IPs and is easy to configure in reverse proxies.
Also, unlike IPs, the client cert can’t be spoofed/man-in-the-middled by the ISP or a TCP reverse proxy. Thus the backend can trust the Cf-Access-Authenticated-User-Email header.
Though there are some disadvantages:
Blocking non-Cloudflare IPs at the firewall or reverse proxy reduces the strain of DDoS attacks directly on the origin (since the attacker can’t reach the costly TLS handshake step). So IP restriction should be done anyway.
An insider attacker with the ability to bypass Cloudflare and a TLS-terminating reverse proxy could spoof the Cf-Access-Authenticated-User-Email header to the backend, allowing them to escalate their access in the backend app to impersonate any user. So the JWT should be verified in the backend regardless. (But then why does Cloudflare provide this scary header in the first place?)
It is a case of documentation. But in this case the documentation is super important. If the admin messes this up, their server is completely insecure and unprotected by Cloudflare Access. And worse, they think it is protected.
The confusing part to me is that you need to verify the JTWs when allowing only Cloudflare IPs. I was under the impression that limiting the IPs would be enough, and the documentation doesn’t go into the details of why that isn’t the case. The note @slixpk has abut IP spoofing would be useful in the docs to understand why a seemingly “secure” way of securing the origin server is not enough.
The confusing part to me is that you need to verify the JTWs when allowing only Cloudflare IPs. I was under the impression that limiting the IPs would be enough
Anyone can create a record on their domain pointing to your IP so there has to be more than just checking access is from a Cloudflare IP. That can be host name checks, Auth origin, jwt etc. But it has to be something and some are spoofable. I’d stick with using jwt.