As a principal architect who has deployed Cloudflare Access in complex, multi-cloud environments, I can state unequivocally that policy ordering is one of the most critical—and initially perplexing—concepts to master. It is the foundational logic layer that determines your zero-trust posture, and misunderstanding it will lead to either insecure exposures or user access denials that cripple productivity. Your confusion is warranted; the evaluation order is not immediately intuitive if you're coming from traditional firewall rule sets or IAM policies.
The core principle is this: Cloudflare Access evaluates policies from **top to bottom**, and the **first matching policy** for a given request is the one that is applied. This is a classic "first-match" wins algorithm. The evaluation stops at that first match. This is fundamentally different from systems that evaluate all rules and apply the most restrictive or most permissive; here, order is absolute.
Therefore, your primary design task is to sequence your policies with intentionality. A common and secure pattern is to structure your policy list like so:
1. **Specific, restrictive policies** (e.g., `Allow` for `user@company.com` to `admin.internal-app.example.com`).
2. **Broader, but still restrictive policies** (e.g., `Allow` for `group:engineering@idp` to `*.internal.example.com`).
3. **A final, explicit `Block` rule** for the entire application domain (e.g., `Deny` for `Everyone` to `prod-app.example.com`).
Consider this Terraform snippet that illustrates a dangerously incorrect order versus a correct one. The incorrect order would place a broad allow before a specific block, rendering the block useless.
```hcl
# INCORDER - DANGEROUS: The broad allow is evaluated first, so the block never triggers.
resource "cloudflare_access_policy" "bad_allow" {
application_id = cloudflare_access_application.app.id
zone_id = var.zone_id
name = "Broad Engineering Allow"
precedence = 1 # <-- Evaluated FIRST
decision = "allow"
includes {
group = [cloudflare_access_group.eng_group.id]
}
}
resource "cloudflare_access_policy" "bad_block" {
application_id = cloudflare_access_application.app.id
zone_id = var.zone_id
name = "Block Contractors"
precedence = 2 # <-- Evaluated SECOND, but the first match already allowed engineers in.
decision = "deny"
includes {
group = [cloudflare_access_group.contractor_group.id]
}
}
# CORRECT ORDER: Specific block evaluated before broad allow.
resource "cloudflare_access_policy" "good_block" {
application_id = cloudflare_access_application.app.id
zone_id = var.zone_id
name = "Block Contractors"
precedence = 1 # <-- Evaluated FIRST. Contractors are blocked.
decision = "deny"
includes {
group = [cloudflare_access_group.contractor_group.id]
}
}
resource "cloudflare_access_policy" "good_allow" {
application_id = cloudflare_access_application.app.id
zone_id = var.zone_id
name = "Engineering Allow"
precedence = 2 # <-- Evaluated SECOND. Engineers are allowed.
decision = "allow"
includes {
group = [cloudflare_access_group.eng_group.id]
}
}
```
My operational advice is to start with a deny-all as your final policy (highest precedence number) for every application as a safety net. Then, build your allow policies *before* it. Always ask: "Is there a scenario where a more general rule above could unintentionally match a request I want to block below?" Use service tokens for machine-to-machine access with very specific allow rules placed high in the order. Finally, integrate this ordering logic into your Infrastructure as Code review process; a peer should validate the precedence sequence just as they would validate network security group rules.
Boring is beautiful
That's a perfect explanation of the "first-match wins" logic, and you're absolutely right about it being a foundational shift in thinking.
One nuance I'd add for newcomers reading this is that the "specific, restrictive policies first" pattern is the secure default, but there's an important exception for **blocking policies**. You'd want a policy that explicitly blocks a known malicious country or IP range to be placed *before* a more general "allow employees" policy, even if the allow policy is more specific to your users. Otherwise, a request matching the general allow policy would be granted before the evaluation reaches the block rule.
It's less about "specific vs. general" and more about ensuring your highest-priority security rule, whether it's an allow or a deny, is positioned to be evaluated first.
Reviews build trust.