DEV Community

Henley Wing
Henley Wing

Posted on

Detecting paying Cloudflare customers (for fun and profit)

A while back I got curious about whether you could tell the difference between a company paying Cloudflare serious money versus one that signed up for the free plan and forgot about it.

First thing I tried: response headers. Don’t bother. Cloudflare returns identical header names whether you’re running on Workers, sitting on an Enterprise contract, or using the free tier. That’s intentional – Cloudflare doesn’t want their product tier to leak through HTTP.

What isn’t intentional is everything else. Here’s a full walkthrough, ordered from weakest to strongest signal. Code samples are Python, but everything here is just DNS lookups and HTTP requests – translate to whatever language you want.


Baseline: Are They Even Using Cloudflare?

Before anything else you need to confirm you’re actually looking at a Cloudflare-proxied domain.

Cloudflare publishes their IP ranges publicly at cloudflare.com/ips. Resolve the domain’s A record, check if it falls inside those ranges. Or check the nameservers – Cloudflare customers using the full proxy will have ns1.cloudflare.com / ns2.cloudflare.com as their NS records.

def on_cloudflare(domain):
    a_records = dns_lookup_a(domain)
    if any(ip in cloudflare_ip_ranges for ip in a_records):
        return True

    ns_records = dns_lookup_ns(domain)
    return any("cloudflare" in ns for ns in ns_records)
Enter fullscreen mode Exit fullscreen mode

Cloudflare Radar publishes the top million domains by traffic. DNS-resolve all of them against the Cloudflare IP list and you’ve got a large working dataset in under an hour.

The catch: this puts you at roughly 20% of the web. That’s a lot of personal blogs and parked domains. The signals below are about separating the serious customers from the noise.


Signal #1: Dashboard SSO TXT Record

Straightforward to check, surprisingly revealing.

When a company configures SSO for their Cloudflare dashboard login (wiring it into Okta, Azure AD, or another SAML identity provider), the setup process adds a TXT record to their DNS zone:

cloudflare_dashboard_sso=1111111
Enter fullscreen mode Exit fullscreen mode
def has_dashboard_sso(domain):
    txt_records = dns_lookup_txt(domain)
    return any("cloudflare_dashboard_sso=" in r for r in txt_records)
Enter fullscreen mode Exit fullscreen mode

This used to be locked to Enterprise. Cloudflare has since relaxed that, so it’s not a guaranteed paid signal anymore. But practically speaking: nobody running a hobby project or a small business on the free tier is going to spend their afternoon setting up a SAML connector. The configuration effort alone filters out the noise. Treat it as a soft signal of intentional, serious usage.

Fast to run across the full Radar million since it’s just a DNS query.


Signal #2: Cloudflare Email Products (MX Records)

Two completely different Cloudflare email products, both detectable via MX record inspection.

Email Routing is a free forwarding service. When enabled, Cloudflare replaces the domain’s MX records with their own mail servers. The pattern looks like this:

MX 52  route1.mx.cloudflare.net
MX 98  route2.mx.cloudflare.net
MX 91  route3.mx.cloudflare.net
Enter fullscreen mode Exit fullscreen mode

Some zones use named variants instead: amir.mx.cloudflare.net, linda.mx.cloudflare.net, isaac.mx.cloudflare.net. Either way, the *.mx.cloudflare.net suffix is the tell. Since this is a free product it’s a weak signal on its own – but it does confirm the domain is actively managed in Cloudflare rather than just having DNS parked there.

Email Security (formerly Area 1) is the paid product. It’s an anti-phishing and email threat detection platform aimed squarely at enterprise security teams. When deployed in inline mode, it sits in front of the customer’s mail provider as the primary MX record, inspecting every inbound message before it reaches Google Workspace or Microsoft 365. The MX records point to Area 1’s inbound gateways rather than the customer’s mail provider directly – look for *.area1security.com or Cloudflare-owned inbound gateway hostnames.

def email_signals(domain):
    mx_records = dns_lookup_mx(domain)
    mx_hosts = [mx.lower() for mx in mx_records]
    return {
        "email_routing": any("mx.cloudflare.net" in mx for mx in mx_hosts),
        "email_security": any("area1security.com" in mx for mx in mx_hosts),
    }
Enter fullscreen mode Exit fullscreen mode

Email Security is Enterprise territory – it’s a dedicated security product with per-seat pricing and a sales process. Seeing those MX records on a domain is a strong indicator of an Enterprise relationship.


Signal #3: Bot Defense Cookies

Passive signal – no probing needed, just inspect cookies on a normal response.

__cf_bm shows up when any of Cloudflare’s bot defense products are active: Bot Management (Enterprise-only), Super Bot Fight Mode (Pro+), or Bot Fight Mode (free). The cookie itself doesn’t tell you which tier, but its presence means someone has actively configured bot protection beyond the defaults.

_cfuvid appears when a site is using cf.unique_visitor_id inside a WAF Rate Limiting Rule to track unique visitors behind shared IPs (corporate NATs, etc.). Available on any plan, but writing custom rate limiting rules signals intentional configuration rather than a default install.

def cookie_signals(response):
    cookies = response.headers.get("set-cookie", "")
    return {
        "bot_management": "__cf_bm=" in cookies,
        "rate_limiting":  "_cfuvid=" in cookies,
    }
Enter fullscreen mode Exit fullscreen mode

Neither cookie alone proves paid status. Together with other signals they help build the picture.


Signal #4: Custom Error Pages

A bit more involved, but one of the more reliable signals for Pro+ customers.

Cloudflare’s default error pages have consistent fingerprints you can match against:

  • "Attention Required! | Cloudflare" – WAF block page
  • _cf_chl_opt – challenge/CAPTCHA page
  • cf-error-details – diagnostic error pages

Pro plan and above lets customers replace these with custom error responses. Plenty of paying customers do this because Cloudflare’s default error page clashes with their brand.

The detection relies on a quirk: Cloudflare’s edge always writes the cf-ray header into every response. This Ray ID is generated at the edge – the customer’s origin server has no way of knowing it in advance. But when Cloudflare renders a custom error page, the edge also writes that same Ray ID into the response body.

So if the Ray ID from the response header appears in the body, and the body doesn’t match any default Cloudflare template, you’re looking at a custom error page from a paying customer.

def has_custom_error_page(response):
    if not (400 <= response.status < 600):
        return False
    if "cloudflare" not in response.headers.get("server", ""):
        return False

    ray_id = response.headers["cf-ray"].split("-")[0]
    if ray_id not in response.body:
        return False

    default_markers = [
        "Attention Required! | Cloudflare",
        "_cf_chl_opt",
        "cf-error-details",
        "__CF$cv$params",
        "/cdn-cgi/challenge-platform/scripts/jsd/main.js",
    ]
    return not any(m in response.body for m in default_markers)
Enter fullscreen mode Exit fullscreen mode

One false positive trap: Cloudflare’s bot detection JS beacon also embeds the Ray ID in normal HTML responses. The default_markers exclusion list catches this.

To trigger the error page, hit a nonexistent API path: https://api.example.com/api/v1/zzzz_not_real. APIs reliably return 4xx on unknown paths, which triggers Cloudflare’s error rendering. Marketing sites require probing paths like /wp-admin or /.env – more aggressive and more likely to get your scanner flagged.

Running this against api.* subdomains at scale, the hits were Swiss classifieds platforms, central banks, major retailers, government agencies. Exactly the profile you’d expect for paid customers.


Signal #5: Cloudflare Access (Zero Trust)

Cloudflare Access gates any web application behind a login screen, typically used to protect internal tooling: Grafana dashboards, GitLab instances, Jenkins, internal admin panels. In 2026 it’s also being used to lock down MCP servers.

When you hit an Access-protected endpoint without a valid session, Cloudflare redirects to the customer’s team page on cloudflareaccess.com. That redirect is the signal.

def has_cloudflare_access(url):
    response = http_get(url, follow_redirects=False)
    if response.status not in (301, 302, 303, 307, 308):
        return False
    return "cloudflareaccess.com" in response.headers.get("location", "")
Enter fullscreen mode Exit fullscreen mode

Internal tooling subdomains follow predictable patterns. For each domain, probe:

gitlab.<domain>
grafana.<domain>
jenkins.<domain>
internal.<domain>
admin.<domain>
wiki.<domain>
mcp.<domain>
Enter fullscreen mode Exit fullscreen mode

Most won’t resolve. Check the ones that do. Access has a free tier (up to 50 users) so it’s not a hard Enterprise gate, but the engineering investment of configuring Access policies and integrating an identity provider correlates strongly with paid usage.


Signal #6: OV or EV TLS Certificates

This one requires understanding the three tiers of SSL certificate validation.

DV (domain-validated) is what every free service issues, including Cloudflare’s Universal SSL. The certificate authority just checks you control the domain. Takes seconds. The cert subject contains only CN=.

OV (organization-validated) requires the CA to verify your company is a real legal entity – business registration, phone verification, the works. Takes days. The company name is embedded in the cert subject: O=, L=, ST=, C=.

EV (extended validation) is OV with a deeper background check: jurisdiction, physical address, operational existence. Subject fields include businessCategory, serialNumber, jurisdictionCountryName. Used primarily in regulated industries where compliance frameworks mandate it.

The detection angle: Cloudflare cannot issue OV or EV certificates. Universal SSL is DV only. So if a domain resolves to Cloudflare IPs but its certificate is OV or EV, the customer purchased a commercial cert and uploaded it via Custom Certificates – a Business plan feature ($200/month minimum) used almost exclusively by Enterprise customers.

You can identify certificate tiers programmatically via the certificatePolicies OIDs:

  • 2.23.140.1.2.1 – DV
  • 2.23.140.1.2.2 – OV
  • 2.23.140.1.1 – EV
def cert_tier(host):
    cert = fetch_tls_cert(host)
    if has_ev_subject_fields(cert) or "2.23.140.1.1" in cert.policy_oids:
        return "EV"
    if "O=" in cert.subject and "2.23.140.1.2.2" in cert.policy_oids:
        return "OV"
    return "DV"

def has_paid_cert(domain):
    if not any(ip in cloudflare_ip_ranges for ip in dns_lookup_a(domain)):
        return False
    return cert_tier(domain) in ("OV", "EV")
Enter fullscreen mode Exit fullscreen mode

Testing against fendt.com: OV cert issued by DigiCert with O=AGCO GmbH, served from Cloudflare IPs. AGCO is a $14B multinational – the finding makes sense. OV/EV on a Cloudflare-served domain is one of the strongest single signals for Enterprise status.


Signal #7: Static IPs

By default, Cloudflare is anycast. A single IP like 104.21.3.47 could be serving thousands of completely unrelated domains simultaneously. The IP belongs to Cloudflare and is shared across their entire customer base.

Static IPs are an Enterprise-only feature where Cloudflare allocates IPs from their range exclusively to a single customer. Nothing else resolves to those IPs. Common in financial services and B2B SaaS where clients require stable IP addresses for firewall allowlisting.

Detection requires aggregate data. Build a frequency map across your full domain dataset:

def find_static_ip_candidates(all_cloudflare_domains):
    ip_to_domains = {}
    for domain in all_cloudflare_domains:
        for ip in dns_lookup_a(domain):
            if ip in cloudflare_ip_ranges:
                ip_to_domains.setdefault(ip, []).append(domain)

    # Anycast IPs serve thousands of domains
    # Static IPs serve 1-3 (apex, www, maybe a subdomain)
    return {
        ip: domains
        for ip, domains in ip_to_domains.items()
        if len(domains) <= 3
    }
Enter fullscreen mode Exit fullscreen mode

Additional tell: Static IPs tend to be allocated in sequential blocks. If a single domain has two A records where only the third octet differs, and both are rare in your frequency map, that’s a strong Static IP signal.


Signal #8: Secondary DNS

One of the cleanest Enterprise signals and the easiest to detect.

Standard Cloudflare customers let Cloudflare host their DNS entirely – nameservers become ns1.cloudflare.com and ns2.cloudflare.com. Secondary DNS is a different model: the customer keeps their own primary DNS infrastructure and adds Cloudflare as a secondary authoritative server, receiving zone transfers via AXFR/IXFR as a resilient backup.

The NS records tell the story immediately. A Secondary DNS customer has both their own nameservers and Cloudflare’s secondary ones listed together:

ns1.company.com
ns2.company.com
ns0227.secondary.cloudflare.com
ns0022.secondary.cloudflare.com
Enter fullscreen mode Exit fullscreen mode

The *.secondary.cloudflare.com pattern only ever appears for this product. The four-digit number is a per-customer or per-zone identifier.

def has_cloudflare_secondary_dns(domain):
    ns_records = dns_lookup_ns(domain)
    return any("secondary.cloudflare.com" in ns for ns in ns_records)
Enter fullscreen mode Exit fullscreen mode

Per Cloudflare’s DNS feature matrix, Secondary DNS requires Enterprise plus the Foundation DNS add-on specifically – it’s not included in a standard Enterprise contract. That makes it a stronger indicator than most Enterprise features. Organizations running Secondary DNS have made a deliberate investment in DNS as critical infrastructure.

The IRS has it configured. Government agencies, central banks, and large financial institutions are the profile to expect when scanning for this.


Signal #9: Magic Transit (BGP)

Every other signal on this list works by inspecting a single domain. This one works differently – you’re looking at public BGP routing tables.

Magic Transit lets Enterprise customers route their own IP space through Cloudflare’s network. The customer brings IP prefixes they own (or lease), and Cloudflare announces those prefixes to the internet from AS13335. All traffic destined for those IPs flows through Cloudflare’s scrubbing infrastructure before reaching the customer’s network.

The detection approach: pull every prefix currently announced by AS13335, then WHOIS each one. Prefixes registered to non-Cloudflare organizations are Magic Transit customers.

Browse it manually first to validate the concept:

https://bgp.he.net/AS13335#_prefixes

For programmatic detection, RIPE stat is the most reliable data source:

import requests

def get_as13335_prefixes():
    r = requests.get(
        "https://stat.ripe.net/data/announced-prefixes/data.json?resource=AS13335"
    )
    # Skip IPv6 -- Magic Transit customers are almost exclusively IPv4
    return [p for p in r.json()["data"]["prefixes"]
            if ":" not in p["prefix"]]

NOISE_PATTERNS = ["ip-ripe", "ip manager", "-mnt", "private customer"]
CLOUDFLARE_HANDLES = ["CLOUD14", "CLOUDF"]
NOISE_HANDLES = ["ripe", "arin", "apnic", "afrinic", "lacnic"]

def find_magic_transit_customers(prefixes):
    customers = []
    for p in prefixes:
        ip = p["prefix"].split("/")[0]
        org = whois_org(ip)  # ARIN RDAP with redirect following
        if not org or is_noise(org["name"], org["handle"]):
            continue
        customers.append({
            "prefix":  p["prefix"],
            "name":    org["name"],
            "bgpview": f"https://bgpview.io/prefix/{p['prefix']}",
        })
    return customers
Enter fullscreen mode Exit fullscreen mode

Running this against the full AS13335 prefix list surfaces names like these:

Prefix Organization
23.227.37.0/24 Shopify, Inc.
199.68.19.0/24 VISA International Service Association
131.167.255.0/24 Battelle Memorial Institute
161.248.134.0/24 Canadian Association of Blue Cross Plans
203.15.65.0/24 University of Sydney
103.77.7.0/24 FUJIFILM Data Management Solutions
351.0/24 Genetec
2109 Yardi Systems, Inc.

Three things to filter out:

ISPs and carriers use Magic Transit for their own DDoS protection – it’s actually one of Cloudflare’s largest use cases by traffic volume. Skip anything whose org name or netname contains ISP, TELECOM, CARRIER, or DATACENTER, or anything that has its own ASN with a large prefix count. Legitimate enterprise Magic Transit customers typically have one or two prefixes total and no BGP presence of their own.

WHOIS placeholders – entries showing as IP-RIPE, IP Manager, or handles ending in -MNT are administrative placeholders or IP broker records, not end customers.

IP leasing – some customers bring leased IP space rather than IPs they own outright. The WHOIS points to the leasing company (IPXO is common). The tell is mnt-lower: IPXO-MNT in the RIPE record.

After filtering, what remains is unambiguously Enterprise-tier. Magic Transit requires a dedicated sales engagement, a minimum committed bandwidth, and a manual BGP peering session with Cloudflare’s network team. You don’t stumble into it.


Watch Out For

Platform contamination. Hosting platforms like Kinsta run all customer sites through Cloudflare Enterprise by default. The tenant didn’t configure it, may not know it’s there, and isn’t a Cloudflare customer in any meaningful sense. This is part of why stacking paid signals matters – a Kinsta-hosted blog won’t have a custom error page or an OV cert or Cloudflare Access configured.

Scale carefully. Probing endpoints with WAF-triggering patterns (SQLi strings, common exploit paths) across thousands of domains will get your scanner blocked fast. Everything in this list can be detected with benign requests – non-existent paths, standard User-Agents, no exotic payloads.


Summary

Signal Plan Method
IP / NS check Free DNS lookup
Dashboard SSO TXT Free (high friction) DNS TXT lookup
Email Routing Free MX record lookup
Bot cookies (__cf_bm, _cfuvid) Pro+ Passive HTTP
Custom error pages Pro+ ($25/mo) HTTP probe + body parse
Email Security (Area 1) Enterprise MX record lookup
Cloudflare Access Free tier (strong proxy) HTTP redirect check
OV/EV certificate Business+ ($200/mo) TLS cert inspection
Static IPs Enterprise Aggregate DNS map
Secondary DNS Enterprise + Foundation DNS NS record lookup
Magic Transit Enterprise BGP + WHOIS

None of these signals is definitive in isolation. Stack them and you get a surprisingly clear picture of where a company actually sits on the Cloudflare tier ladder – all from public data, no credentials required.


Spotted a signal I missed? Drop it in the comments.

Top comments (0)