Cloudflare Application Services - Lab Guide (Legacy)
Unified Lab Guide & Technical Reference. Courseware Version 4.0 (AcmeCorp Edition), Legacy / Cloudflare-Managed Lab. Prepared for AcmeCorp IT Security & Infrastructure. Scenario: Edge Security Modernization.
This edition targets the Cloudflare-managed lab (the labs.cloudflare.com "Implement: Application Services" pod). Use it only if you cannot provision the self-hosted lab environment that the primary guide targets. Because the managed lab shares one fixed origin that you do not control, a few chapters are Concept-only (Load Balancing, Advanced Caching, Threat Intel, Spectrum, and the origin-sealing capstone) and a few individual steps are presented as concepts rather than hands-on tasks. Everything else is fully hands-on.
You do not install anything or manage cloud servers. Your Cloudflare account and one pre-onboarded Enterprise zone are provided for you, and you run all terminal commands from the in-browser Ubuntu workstation described below.
Typographical Conventions
This guide uses the following conventions to distinguish between user input, system output, and explanatory notes.
| Convention | Meaning | Example |
|---|---|---|
| Bold | Names of selectable items in the web interface | Click Security to open the Security Rule window. |
Monospace | Text that you enter and coding examples | Enter the following command: dig example.com |
| Result text | Lab step results and explanations, expected system output | HTTP/2 200 OK |
| Italics | Contextual notes explaining "why" a task is necessary | This ensures the legacy application is not exposed directly to the internet. |
<in angle brackets> | A variable parameter. The value is defined in the guide or by your instructor. | Type ping <student-domain> and press Enter. |
How to Use This Lab Guide & The AcmeCorp Scenario
You are AcmeCorp's new Lead Application Security Engineer. Your mandate is to modernize the edge with Cloudflare Application Services, changing the origin as little as possible, and take AcmeCorp from exposed to defended. Over the next 18 labs you will prove how exposed the origin is, route and encrypt its traffic, make it fast and resilient, build a layered perimeter (WAF, bot management, rate limiting, API Shield, threat intelligence, and client-side protection), then seal the origin so the perimeter cannot be bypassed, and replay the opening attacks to prove nothing gets through.
Each lab solves one concrete part of that story. Reading the lab titles from top to bottom tells it end to end: exposed, onboarded, encrypted, fast, resilient, defended, sealed, and proven. Every lab opens with a "This chapter" box, highlighted in yellow, that states the part of the story you are about to solve.
In this managed lab, your Cloudflare account already contains one pre-onboarded Enterprise zone assigned to you (its name follows the pattern adjective-noun.sxplab.com, for example happy-balancer.sxplab.com). The zone is onboarded (Enterprise plan, Cloudflare nameservers assigned) but starts with no DNS records: you create them as the labs need them. You create the apex record (<student-domain>) in Lab 2 to publish the AcmeCorp website, then add public-api.<student-domain> (the orders API used in the API Shield labs) and ps.<student-domain> (the Page Shield demo page) in the labs that use them. All of these resolve to the shared origin that Cloudflare operates for the class.
| Component | Address | Role |
|---|---|---|
| Ubuntu workstation | in-browser (Guacamole) | The machine you drive the labs from. Open its Terminal to run curl, dig, and openssl, and to simulate attacks. It is reached through your browser; there is no SSH and nothing to install. |
| Shared web origin | 20.88.188.200 | Serves the AcmeCorp website (apex) and, via a name-based vhost, the public API. This is the raw origin IP you attack directly in Lab 1. It is shared by the class and you cannot reconfigure it. |
| Orders API origin | 4.157.169.241 | The backend for the orders API used in Lab 14. You create an api DNS record pointing here. |
Your instructor assigns you a student domain (the name of your pre-onboarded zone). Type it below and every matching placeholder across this entire guide, including inside the copy blocks, is replaced with your real value. The value saves to your browser only; nothing is sent anywhere.
Lab 1 - The Exposed Origin
1.1 Confirm the Origin Is Exposed
From the workstation, request the origin directly by IP and read the response headers:
curl -sI http://20.88.188.200/ | head -n 6
Server header. An attacker learns what to target and can reach it directly, so any protection added later can simply be bypassed by talking to this IP.Now look at how the origin serves HTTPS:
echo | openssl s_client -connect 20.88.188.200:443 2>/dev/null | openssl x509 -noout -issuer -subject
HTTP/1.1 200 OK and a Server: header naming the origin's web server. The second shows the certificate's issuer and subject are the same self-signed value, which is why a real browser would throw a certificate warning. The origin is reachable, fingerprinted, and not properly encrypted.1.2 Leak Sensitive Files from the Origin
Developers accidentally left a source-control artifact on the web root. Request it directly:
curl -s http://20.88.188.200/.git/secrets.txt
.git directory. This is exactly the kind of exposure you will virtually patch with the managed WAF in Lab 9.200 and prints credentials, including USERNAME=acmeadmin and PASSWORD=#Super.Secret-5+. A file that should never be public is served straight off the origin.1.3 Scrape the Product Catalog
Simulate a competitor scraping AcmeCorp's catalog. Hammer a product category page and tally the responses:
for i in $(seq 1 50); do curl -s -o /dev/null -w "%{http_code}\n" http://20.88.188.200/services/luggages/; done | sort | uniq -c
200 (the output reads 50 200). Every scrape succeeded; nothing slowed the client down.1.4 Probe the Locked Admin Panel
Finally, knock on the administrative panel and see how it responds:
curl -s -o /dev/null -w "%{http_code}\n" http://20.88.188.200/admin
401 Unauthorized. That is the origin's own control, not an edge control: the panel is still directly reachable on the public IP, and it is up to you to build a real perimeter around it (which you do with an IP list in Lab 10).401. The leaked credentials from 1.2 do not unlock it (it is a separate, locked page), and there is no login form to brute-force (/login returns 404). What matters for the baseline is that /admin is reachable directly on the raw IP at all.Lab 2 - DNS Onboarding & Proxy Control
2.1 Cloudflare as Authoritative DNS
From the workstation Terminal, confirm the nameservers are Cloudflare's:
dig +short NS <student-domain>
howard.ns.cloudflare.com.
mia.ns.cloudflare.com.The onboarded zone starts with no DNS records. In the dashboard, open DNS > Records and create an A record for the apex (name @, which represents <student-domain>) pointing to the shared origin 20.88.188.200, with Proxy Status set to Proxied (orange cloud).
public-api and ps subdomains later, in the labs that use them.2.2 Partial (CNAME) Setup for a Stubborn Partner (Concept)
Prerequisites you would confirm first. The domain must be on a Business or Enterprise plan, it must keep its current authoritative DNS provider, and it must not be a domain where Cloudflare is already authoritative. Only proxyable record types (A, AAAA, CNAME) can be onboarded this way, and the root/apex cannot be proxied in a partial setup, only subdomains.
Convert the zone to CNAME setup. On the domain's Overview page, in the DNS card on the right, you would select Convert to CNAME DNS Setup, then Convert to confirm. This switches the zone type from Full to Partial.
Prove ownership with the verification TXT record. Cloudflare then generates a Verification TXT Record (also found on DNS > Records). You add that exact record at the partner's authoritative DNS provider; Cloudflare polls for it, marks the zone active, and emails a confirmation. The TXT record proves you control the domain and must stay in place for as long as the zone remains partial.
Onboard the hostname. In Cloudflare you create a proxied (orange-cloud) record for the hostname, for example portal. Then at the partner's provider you create a CNAME for that hostname pointing to Cloudflare's target, for example portal.example.com.cdn.cloudflare.net, and remove any old A/AAAA/CNAME for that hostname. That CNAME is what reroutes visitor traffic into Cloudflare's edge while every other record on the domain keeps resolving straight from the partner's DNS.
2.3 Controlling Traffic with Proxy Status
In Cloudflare DNS > Records, create a new A record named ftp pointing to 20.88.188.200, and set its Proxy Status to DNS only (grey cloud).
From the workstation, query that record and compare it to a proxied record:
dig +short ftp.<student-domain>
dig +short <student-domain>
ftp record returns the raw origin IP 20.88.188.200, while the proxied apex returns Cloudflare edge IPs (for example addresses in 104.x / 172.67.x ranges).ftp record (or leave it grey) and make sure the apex stays Proxied so the web application remains protected.Lab 3 - SSL/TLS & Encryption Standards
3.1 Universal SSL, Flexible Mode & Edge Encryption
Go to SSL/TLS > Edge Certificates and confirm the Universal SSL certificate is Active.
<student-domain> and *.<student-domain>). It is what removes the browser certificate warning for visitors.For immediate mitigation, go to SSL/TLS > Overview, open Configure encryption mode, select Flexible under Encryption mode, then click Save. (The manual modes appear as radio options beneath "Automatic SSL/TLS (recommended)".)
On the same Edge Certificates page, turn Always Use HTTPS On.
http:// request to https:// at the edge, so visitors are never served over an unencrypted connection.Still on Edge Certificates, set Minimum TLS Version to TLS 1.2.
Verify the redirect and the negotiated protocol from the workstation:
curl -sI http://<student-domain>/ | head -n 4
curl -sI https://<student-domain>/ | head -n 1
301/308 redirect to the https:// URL (Always Use HTTPS), and the HTTPS request returns 200. The site now presents a valid, Cloudflare-issued certificate to browsers.3.2 Full (Strict) + Origin Certificate
Go to SSL/TLS > Origin Server and click Create Certificate.
Leave the defaults selected: Generate private key and CSR with Cloudflare, RSA (2048) key type, the pre-filled hostnames (*.<student-domain> and <student-domain>), and 15 years validity. Click Create.
Copy the generated Origin Certificate and Private Key, save each to a file (for example cert.pem and key.pem), then click OK.
*.<student-domain>, <student-domain>) with an expiry roughly 15 years out.3.3 Advanced Certificates for Subdomains
First, in Cloudflare DNS > Records, create an A record named payments.platform pointing to 20.88.188.200 with Proxy Status set to Proxied (orange cloud).
payments.platform.<student-domain>) that Universal SSL's single-level wildcard (*.<student-domain>) does not cover, which is exactly why an Advanced Certificate is required.Go to SSL/TLS > Edge Certificates and click Order an advanced certificate. In Certificate Hostnames, type payments.platform and select the autocompleted full hostname payments.platform.<student-domain>. The form pre-fills the apex (<student-domain>) and wildcard (*.<student-domain>); remove both with their Remove buttons so the certificate covers only the payments hostname.
Set Certificate Authority to Google Trust Services, leave Certificate validation method on TXT Validation, keep the default Certificate Validity Period, then click Save.
payments.platform.<student-domain> appears in the Edge Certificates list and moves from Initializing to Pending Validation (TXT) to Active (usually a few minutes), and the plan line shows 1 of 100 advanced certificates used.Wait for the certificate to activate, then confirm from the workstation that it has taken precedence over Universal SSL for that specific subdomain by inspecting the presented certificate:
echo | openssl s_client -connect payments.platform.<student-domain>:443 -servername payments.platform.<student-domain> 2>/dev/null | openssl x509 -noout -issuer -ext subjectAltName
O = Google Trust Services) and the Subject Alternative Name lists DNS:payments.platform.<student-domain>. That proves the dedicated advanced certificate, not the Universal SSL wildcard, is being served for the deep subdomain.Lab 4 - Caching Foundations
4.1 Establishing Caching Baselines
From the workstation, inspect the cache status of a product category page:
curl -sSI https://<student-domain>/services/luggages/ | grep -i cf-cache-status
cf-cache-status: DYNAMIC, and there is no age header. Every request is going all the way to the struggling origin.4.2 Control caching with a Cache Rule
Go to Caching > Cache Rules and click Create rule. Name it "Cache luggages category". Under When incoming requests match, set Field = URI Path, Operator = equals, Value = /services/luggages/.
Under Then, set Cache eligibility to Eligible for cache. Then set Edge TTL to Ignore cache-control header and use this TTL, and enter 30 seconds. Click Deploy.
From the workstation, request the path twice and watch the status change:
curl -sSI https://<student-domain>/services/luggages/ | grep -i cf-cache-status
curl -sSI https://<student-domain>/services/luggages/ | grep -i cf-cache-status
MISS (or DYNAMIC on the very first hit), and the second returns HIT (or REVALIDATED), with an age header present. The Cache Rule successfully offloaded the path from the origin.4.3 Leverage cache-control directives
Go to Caching > Configuration and confirm Browser Cache TTL is set to Respect Existing Headers.
Cache-Control header, rather than from dashboard rules. "Respect Existing Headers" tells Cloudflare to defer to the origin's instructions.Disable the Cache Rule you created in 4.2 (toggle it off on Caching > Cache Rules).
Cache-Control directives take effect so you can observe origin-driven caching.Purge the path, then reload it a couple of times and inspect the headers:
curl -sSI https://<student-domain>/services/luggages/ | grep -iE "cf-cache-status|age|cache-control"
Cache-Control header for the category page, so there is no cache-control line and cf-cache-status falls back to DYNAMIC (Cloudflare will not cache HTML on its own). That confirms the edge is now deferring to the origin rather than a dashboard rule.Cache-Control the origin sends. If the application added a directive such as Cache-Control: max-age=300, the edge would cache the page for that duration and you would see HIT with an age; because this origin sends none, the page stays DYNAMIC. Either way the cache behavior is driven by the application, exactly as the developers intended.4.4 Troubleshooting a Cached 404
Diagnose a reported problem: a page that was briefly missing is now returning a stale 404 even after the origin was fixed. The fix is a targeted purge.
404) when configured to, so a transient origin error can "stick" at the edge. A Custom Purge for the specific URL clears the stale response immediately, and you can control error caching with a Status Code TTL in a Cache Rule (for example cache 404 for only 10 seconds).Go to Caching > Configuration, use Custom Purge, choose Purge by URL, and enter the affected URL (for example https://<student-domain>/services/luggages/). Then reload and confirm the fresh response is served.
Now make the negative-caching behavior explicit and controlled with a Status Code TTL. By default Cloudflare does not cache 404 responses, so a burst of requests to a missing URL (a broken link, or an attacker fuzzing paths) reaches the origin every time. Create a Cache Rule (Caching > Cache Rules > Create rule) that matches a path known to 404, for example the custom filter expression starts_with(http.request.uri.path, "/missing/"):
- Set Cache eligibility to Eligible for cache.
- Under Edge TTL, select a mode: choose Use cache-control header if present, bypass cache if not. The Edge TTL card requires a mode before it will deploy; this option leaves normal edge caching to the origin's headers while the Status Code TTL below governs the
404. - Still under Edge TTL, in the Status code TTL subsection click Add status code setting.
- Set Scope to Single code, the Status code to
404, and the Duration to10seconds, then Deploy.
curl -sSI https://<student-domain>/missing/nope | grep -iE 'HTTP|cf-cache-status'
curl -sSI https://<student-domain>/missing/nope | grep -iE 'HTTP|cf-cache-status'
Both return 404, but the first shows cf-cache-status: MISS and the second cf-cache-status: HIT, proving the negative response is now served from cache. Wait more than 10 seconds and the next request returns to MISS as the TTL expires.Lab 5 - Advanced Caching (Concept)
5.1 Cache Everything & Cookie Bypass (Concept)
You would create a Cache Rule for a marketing landing path with Cache eligibility = Eligible for cache so Cloudflare caches the HTML itself, not just static file types. This lets the page survive a massive traffic spike because most requests are answered from the edge.
Caching HTML is dangerous if users can log in, because a cached personalized page could be served to the wrong user. To make it safe, you would add a second rule that bypasses cache when a session cookie is present (for example Cookie contains session_id). Anonymous visitors get the fast cached page; logged-in users always bypass to the origin.
cf-cache-status: DYNAMIC (bypassed), while an anonymous request reports HIT. That split is what proves the security logic works.5.2 Custom Cache Key (Concept)
Marketing tracking tags (?utm_source=twitter, ?utm_source=email) create unique URLs that each miss the cache and hit the origin. You would define a Custom Cache Key that ignores query strings, so every variant of a URL collapses into one cached object.
5.3 Tiered Caching (Concept)
You would enable Tiered Cache (Caching > Tiered Cache) and select Smart Tiered Cache Topology. Instead of every global PoP fetching from the origin, lower-tier PoPs ask an upper-tier PoP first.
Lab 6 - Traffic Management & Rules Engine
6.1 Geo-Redirects (Portugal Localization)
Create a Redirect Rule so visitors from Portugal are sent to the localized landing page. Go to Rules > Overview, and in the Redirect Rules section click Create rule.
- Rule name:
Lab 6.1 - Portugal localization - When incoming requests match: set Field = Country, Operator = equals, Value = Portugal (the expression reads
ip.src.country eq "PT"). - Then: under URL redirect choose Dynamic, set the Expression to
concat("https://", http.host, "/pt"), Status code302, and enable Preserve query string. Click Deploy.
/pt path.curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/
This returns 200 (not a 3xx), confirming the rule is scoped to Portuguese source IPs only. A visitor geolocated to Portugal receives a 302 to the localized page.6.2 Campaign Redirects
Marketing is running a campaign whose short link (/promo) must bounce visitors to AcmeCorp's LinkedIn page. Create a second Redirect Rule that matches the campaign path and sends it to an external URL.
- Rule name:
Lab 6.2 - Campaign redirect - When incoming requests match: Field = URI Path, Operator = equals, Value =
/promo. - Then: URL redirect > Static, URL = your LinkedIn profile URL (for example
https://www.linkedin.com/company/cloudflare), Status code301. Click Deploy.
curl -sSI https://<student-domain>/promo | grep -iE 'HTTP|location'
You should see a 301 and a location: header pointing at the LinkedIn URL. When you are done, disable this rule so it does not interfere with later labs.6.3 Response Header Transform
First, observe a security win Cloudflare gives you for free. The origin advertises its exact software in the Server response header, which attackers use to fingerprint the backend. Inspect the header on your proxied site:
curl -sSI https://<student-domain>/ | grep -i "^server:"
Server header with its own value (cloudflare). The origin's real software and version never reach the visitor, with zero configuration.Now go to Rules > Overview, click Create rule and choose Response Header Transform Rule. Name it "Add X-Frame-Options". Apply it to All incoming requests, choose Set static, header name X-Frame-Options, value SAMEORIGIN, then Deploy.
X-Frame-Options: SAMEORIGIN tells browsers to refuse to render the site inside a frame on another origin, which defends against clickjacking. Injecting it at the edge means AcmeCorp gets the protection without changing origin code.curl -sSI https://<student-domain>/ | grep -iE "^server:|^x-frame-options:"
You should see server: cloudflare and x-frame-options: SAMEORIGIN.6.4 Managed Transforms (Security Headers)
You just added one header by hand. Cloudflare can also apply a curated set of best-practice header changes with a single click, using Managed Transforms. Go to Rules > Settings > Managed Transforms (or, from Rules > Overview, click Go to Managed Transforms). Under HTTP response headers, enable Remove "X-Powered-By" headers and Add security headers.
x-content-type-options: nosniff, x-xss-protection: 1; mode=block, x-frame-options: SAMEORIGIN, referrer-policy: same-origin, and expect-ct: max-age=86400, enforce. Remove "X-Powered-By" headers strips another backend fingerprint, the same way Cloudflare already masks Server. (Note: HSTS / Strict-Transport-Security is not part of this set; you enable HSTS separately under SSL/TLS.)x-powered-by is gone:
curl -sSI https://<student-domain>/ | grep -iE "x-content-type-options|x-frame-options|x-xss-protection|referrer-policy|x-powered-by"
You should see x-content-type-options: nosniff and x-frame-options: SAMEORIGIN, and no x-powered-by line.6.5 URL Rewrites
AcmeCorp discontinued the "arrows" product line and wants requests for it served the "luggages" content instead, invisibly, without a public redirect. Go to Rules > Overview, and in the URL Rewrite Rules section click Create rule. Name it "Retire arrows path". Choose Custom filter expression and match URI Path equals /services/arrows/. Under Then rewrite the path and/or query, in the Path group choose Rewrite to with type Static. The value field already shows a leading /, so type services/luggages/ without a leading slash (the final path becomes /services/luggages/). Typing /services/luggages/ here would produce a broken //services/luggages/. Leave the Query untouched, then Deploy.
200 (not a redirect):
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/services/arrows/
You should get 200, and fetching the page body shows the luggages content served under the arrows URL.6.6 Host Header Overrides (Concept)
Host the origin receives so one hostname's traffic is routed to a different origin virtual host. In this managed lab the public API already has its own preconfigured hostname (public-api.<student-domain>) wired straight to the API vhost, so there is no routing mismatch to fix by hand. The pattern below is what you would use on an origin that routes by Host.You would create an Origin Rule (Rules > Origin Rules) that matches the API path (for example starts_with(http.request.uri.path, "/api/")) and, under Host Header, selects Rewrite to and enters public-api.<student-domain>, leaving SNI, DNS Record, and Destination Port set to Preserve. The origin would then route /api/* to the API virtual host, without changing the public URL or touching origin code.
Lab 7 - Load Balancing & Failover (Concept)
7.1 Origin Pools & Health Checks (Concept)
All Load Balancing objects live under Traffic > Load Balancing, and you build bottom-up: monitor first, then pools, then the load balancer. You would create an HTTP monitor (for example Path /, Port 80, expecting 200) that Cloudflare uses to probe each origin from its edge.
You would then create two Origin Pools, each with one endpoint (the primary origin in one pool, the backup origin in the other) and attach the monitor to both.
7.2 Implementing Failover (Concept)
You would create a load balancer, add the primary pool first and the backup pool second (order is the failover order), set the fallback pool to the backup, and choose Off (Failover) steering so 100% of traffic goes to the first healthy pool.
To prove it, you would take the primary origin's web server offline. Within a health-check cycle the primary pool goes Unhealthy and traffic shifts entirely to the backup pool, then returns to the primary once it recovers.
Lab 8 - Advanced Health Checks & Steering (Concept)
8.1 Advanced Health Checks (Concept)
A basic HTTP monitor only proves the web server answered 200. You would create a monitor that also inspects the response body, setting Response Body to a required substring such as "status":"healthy" under the monitor's advanced settings, then attach it to the primary pool.
If an origin still returns 200 but its /status body no longer contains that substring (a "zombie"), the content-aware monitor marks the pool unhealthy and traffic fails over, even though a basic check would have kept sending users to the broken app.
8.2 Weighted Steering (Concept)
You would change the load balancer's steering policy from Off (Failover) to Random and set pool weights (for example primary 0.6, secondary 0.4) so traffic is split roughly 60/40 across pools.
8.3 Session Affinity (Concept)
Weighted Random steering picks a pool per request, so the same visitor can bounce between origins on consecutive clicks, which breaks a stateful flow (a login session or a shopping cart held on one origin). You would fix this with Session Affinity: on the load balancer's Hostname step (below the load balancer description), tick the Session Affinity checkbox. Cloudflare then issues an affinity cookie that pins each client to one endpoint for as long as that endpoint stays healthy.
Lab 9 - WAF Managed Rulesets
9.1 Deploy the WAF and understand OWASP
First, from the workstation, confirm the exposure is still live through Cloudflare (not just on the raw IP):
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/.git/secrets.txt
200. With no WAF rules deployed, Cloudflare forwards the request and the origin serves the secrets file.Go to Security > Security rules. In the Managed rules section, click Deploy managed ruleset and deploy the Cloudflare Managed Ruleset.
.git and other sensitive paths.Deploy the Cloudflare OWASP Core Ruleset as well. On its configuration page, leave Anomaly Score Threshold at Medium (40) and Paranoia Level at PL1, and deploy it in Log action to start.
Now retest the exposed file through Cloudflare:
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/.git/secrets.txt
403 and the Cloudflare block page. The managed ruleset recognized the request for the exposed source-control path and blocked it at the edge, even though the origin would still happily serve it. Go to Security > Analytics > Events and confirm a matching Block event under Managed rules.9.2 Dealing with False Positives
The contact form at /contact/ accepts free-text that can look like an injection payload, so legitimate submissions can trip the WAF's SQL-injection rules. First reproduce the false positive from the workstation:
curl -s -o /dev/null -w "%{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
403 and the Cloudflare block page: a legitimate contact-form submission is hard-blocked. In Security > Analytics > Events a Block event appears for path /contact/ under Managed rules; expand it to see the matched rule (SQLi - Comment) in the Cloudflare Managed Ruleset. The Managed Ruleset blocks this pattern on its own default action, before the OWASP ruleset (running in Log) ever scores it.Create a surgical exception. Go to Security > Security rules, click Create rule > Managed rules (the New managed rules exception page). Name it "Contact form exclusion". Under When incoming requests match set Field = URI Path, Operator = wildcard, Value = /contact/*. Keep Skip all remaining rules. Change Place at from Last to First, then Deploy.
&n=1, &n=2 to avoid de-duplication), and send a control request to a different path. In Security > Analytics > Events the /contact/ requests now show Action = Skip while other paths still show managed-rule events. The exception surgically disabled managed rules on the contact path only.9.3 Reading Analytics & Identifying Threats
Go to Security > Analytics. The Events tab groups firewall events by Action and by Service (for example, Managed rules) and lists the top source IPs, paths, and countries. The Traffic tab shows sampled request logs with Cloudflare's threat classifications.
Use the filters to answer: which rules or services triggered most, and is a single source responsible? Drill into an event to view the Ray ID, User Agent, ASN, and matched rule.
.git virtual patch plus the blocked contact payload) and Skip (your exception rule). Under Events by service > Managed rules the matched rules are listed by name (for example Version Control - Information Disclosure, SQLi - Comment, and Contact form exclusion). Because all of your test traffic came from the workstation, a single entry under Source IP Addresses accounts for every event, Paths shows /.git/secrets.txt and /contact/, and User Agents shows curl. This "one IP, one user agent" pattern is exactly how you distinguish a single automated actor from a distributed attack.Lab 10 - WAF Custom Rules
10.1 Geo Blocking + Different Actions
Go to Security > Security rules. In the Custom rules card click Create rule (or use the Create rule menu at the top right and choose Custom rules). Name it "Geo Block".
Click Edit expression (top-right of the "When incoming requests match" box) to switch to the raw expression editor, then paste an expression that matches everything outside the United States and Canada. Under "Then take action" choose Block, leave the response as the default Cloudflare WAF block page (403), and click Deploy:
not (ip.src.country in {"US" "CA"})
ip.src.country field is a two-letter ISO country code derived from the visitor's IP.Create a second, separate custom rule named "Competitor Monitoring". Set the action to Log and use an expression that matches a single country you want to watch, such as China:
ip.src.country eq "CN"
First, from the workstation, confirm that normal traffic is allowed by your Geo Block rule:
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/
200. Because the workstation is not outside US/CA, the Geo Block expression does not match it, so the request passes.Now open Security > Security rules > Custom rules, edit Geo Block, temporarily change its expression to match your own country (for example ip.src.country eq "US"), click Save, wait about 15 seconds for the edge to update, then re-run the same curl:
403 and the Cloudflare block page. This proves the geo match and the Block action are working. Change the expression back to not (ip.src.country in {"US" "CA"}) and Save; the curl should return 200 again.Finally, review the results at Security > Analytics > Events.
10.2 Challenge the Contact Form
Go to Security > Security rules. In the Custom rules card click Create rule. Name it "Challenge Contact Form", click Edit expression, and match the contact path:
http.request.uri.path eq "/contact/"
Under Then take action choose Managed Challenge, and Deploy.
cf_clearance cookie to a client that passes, so subsequent requests in that session are not re-challenged.curl client cannot solve the challenge:
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/contact/
curl -s -D - -o /dev/null https://<student-domain>/contact/ | grep -i "cf-mitigated"
You should get 403 and cf-mitigated: challenge. A real browser would be offered the challenge, solve it, and receive a cf_clearance cookie.10.3 Lock Down the Admin Panel with an IP List
First, find the workstation's public IP by running this on the workstation:
curl -s https://api.ipify.org; echo
Lists are defined at the account level. Go to Manage account > Configurations > Lists, click Create IP list, set the Identifier to corporate_ips (lowercase letters, numbers, and underscores only; it cannot be changed later), and Create. On the next screen add the workstation's public IP and click Add to list.
$corporate_ips, without rewriting the rule when membership changes.Return to Security > Security rules, create a custom rule named "Allow Corp Admin Only", click Edit expression, and block /admin for anyone not on the corporate list. Set the action to Block and Deploy:
(http.request.uri.path eq "/admin") and not (ip.src in $corporate_ips)
/admin; everyone else is blocked at the edge. "Block when NOT in the list" is the correct pattern, because the custom-rule Allow action only skips remaining custom rules; it does not block anyone by itself.From the workstation (whose IP is on the list), confirm you are allowed through:
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/admin
401. Your IP is in corporate_ips, so the rule does not match and the request reaches the origin's Basic-auth challenge. To prove the block, remove your IP from the list, wait ~15 seconds, and re-run the curl: it now returns 403. Re-add your IP to restore access.10.4 Skip Security for a Partner API
Go to Security > Security rules, create a custom rule named "Skip Partner API Security", and click Edit expression. Match requests to the partner API host that carry a trusted-partner header:
(http.host eq "public-api.<student-domain>") and any(http.request.headers["x-partner-api"][*] eq "true")
x-partner-api: true identifies that trusted traffic. HTTP header names are lowercased in expressions, and http.request.headers[...] returns an array, so it is wrapped in any(...[*] eq "true").Under Then take action choose Skip. In "WAF components to skip" tick All managed rules, leave Log matching requests enabled, and Deploy.
P="q=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -H "x-partner-api: true" "https://public-api.<student-domain>/v1/orders?$P"
curl -s -o /dev/null "https://public-api.<student-domain>/v1/orders?$P"
The request with the header appears as a Skip event and produces no Managed rules event; the request without the header still triggers the managed rules. This proves the bypass works only for trusted partner traffic.Lab 11 - Bot Management
11.1 Bot Analytics & Baseline Rule
First confirm Bot Management is on. Go to Security > Settings and find the Bot traffic section: Bot management should read Always active on this Enterprise zone.
cf.bot_management.score. That field only carries a meaningful value when Bot Management is active.Review live bot traffic in Security > Analytics: click Add filter, choose Bot score, and inspect the low-score band plus the Source user agents and Source ASNs lists.
Create the rule: Security > Security rules > Custom rules > Create rule. Name it "Bot Management - Baseline", click Edit expression, and paste the expression below. Scope it to the bot-demo path and start the action on Log, then Deploy. (A ready-made Templates option, "Mitigate definite bot traffic", is also available under Security rules if you prefer a guided starting point.)
(cf.bot_management.score lt 30) and not cf.bot_management.verified_bot and http.request.uri.path contains "/hellobots"
not cf.bot_management.verified_bot excludes verified crawlers such as Googlebot, so SEO is never harmed. Scoping to /hellobots keeps the test focused on the lab's bot-demo path.Once the Log events look right, edit the rule, change the action to Managed Challenge, and Deploy. Test from the workstation (a single-egress curl client scores as automated, so it is your stand-in scraper):
curl -s -o /dev/null -w "%{http_code}\n" "https://<student-domain>/hellobots"
curl -s -D - -o /dev/null "https://<student-domain>/hellobots" | grep -i "cf-mitigated"
403 and the headers include cf-mitigated: challenge. That header is the definitive proof the challenge fired (a real browser would solve it silently and get through). You will also see the rule firing in Security > Analytics > Events.11.2 Refine BM Rule to Exclude APIs
Legitimate automation also scores low: a nightly internal BI tool identifies itself with the user agent B1-Bot/1.11, and a trusted catalog-sync job hits /services/luggages/. Both would be challenged by the baseline rule. Edit "Bot Management - Baseline", click Edit expression, and replace the expression with:
(cf.bot_management.score lt 30) and not cf.bot_management.verified_bot and not (http.request.uri.path eq "/services/luggages/") and not (http.user_agent eq "B1-Bot/1.11")
and not (...) clauses carve trusted automation out of the challenge while every other low-score request is still caught. This is the normal Bot Management tuning loop: start broad, then add precise exclusions for known-good clients.With the action on Managed Challenge, test all three cases from the workstation:
D=<student-domain>
curl -s -o /dev/null -w "hellobots: %{http_code}\n" "https://$D/hellobots"
curl -s -o /dev/null -w "luggages: %{http_code}\n" "https://$D/services/luggages/"
curl -s -o /dev/null -w "B1-Bot UA: %{http_code}\n" -A "B1-Bot/1.11" "https://$D/hellobots"
/hellobots request is still challenged (403), while /services/luggages/ and the B1-Bot/1.11 request pass through (200). The exclusions work without weakening the main defense.Lab 12 - Rate Limiting
12.1 Rate Limit By IP (Brute Force Protection)
Go to Security > Security rules, scroll to Advanced rate limiting rules, and click Create rule. Name it "Safes Page Rate Limit", click Edit expression, and match the target path:
http.request.uri.path contains "/services/safes/"
Under With the same characteristics leave IP selected. Under When rate exceeds set Requests = 5 and Period = 1 minute. Under Then take action choose Block, and set the duration to 1 minute. Click Deploy.
Test from the workstation by bursting past the threshold:
D=<student-domain>
for i in $(seq 1 8); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/services/safes/"; done
200, then requests 6, 7, 8 return 429 (Too Many Requests). The Block engaged once the per-IP threshold was crossed.12.2 Rate Limit By Origin Response Code
In the same Advanced rate limiting rules section, click Create rule and name it "Search Fuzzing Protection". Click Edit expression and match a search-style path:
http.request.uri.path eq "/services/search"
404 responses, which is the signal we count.Tick Use custom counting expression. In the Increment counter when box click Edit expression and enter the origin-response condition:
http.response.code eq 404
404. Normal successful lookups (200) never fill the counter, so real users are never limited. Only clients that keep generating "not found" errors trip the rule. This proves Cloudflare can rate-limit on the origin's response, not just the incoming request.Set Requests = 10, Period = 1 minute. Under Then take action choose Block with duration 1 minute, then Deploy.
Test from the workstation. The nonexistent path returns 404, so every request feeds the counter:
D=<student-domain>
for i in $(seq 1 12); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/services/search?q=$i"; done
404 (passed to the origin and counted), then requests 11 and 12 return 429. The switch from 404 to 429 proves the counter advanced only on the origin's 404 responses and the Block engaged at the threshold.Lab 13 - API Discovery & Endpoint Management
13.1 Session Identifiers, Discovery & Endpoint Management
Confirm the session identifier. Go to Security > Settings, open the API abuse category, and click into Session identifiers. This managed lab preconfigures a Header identifier named session-id. If it is not present, click Add identifiers, set Type = Header and Name = session-id, and Save.
Generate API traffic from the workstation. Send a distinct session-id per simulated user across several endpoints, including the undocumented shadow endpoint:
H=public-api.<student-domain>
for u in $(seq 1 20); do
S="session-id: user-$u"
curl -s -o /dev/null -H "$S" "https://$H/status"
curl -s -o /dev/null -H "$S" "https://$H/v1/orders"
curl -s -o /dev/null -H "$S" -X POST -H "Content-Type: application/json" -d "{\"createdBy\":\"user$u\",\"items\":[\"sku-$u\"]}" "https://$H/v1/orders"
curl -s -o /dev/null -H "$S" "https://$H/internal/accessLogs"
done
Discovered endpoints appear under Security > Web assets > Operations (Endpoint Management) via Add operations > Select from Discovery. Until Discovery populates, add operations manually: on Add operations use Add custom operations and enter Method, Hostname (public-api.<student-domain>), and Path (for example POST /v1/orders), then Save operations.
/internal/accessLogs shows up under Add operations > Select from Discovery, and (optionally) accept its rate-limiting recommendation to govern it.Lab 14 - API Schema Validation & Sequence Analytics
14.1 Schema Validation
First, onboard the orders API. In Cloudflare DNS > Records, create an A record named api (the name must be exactly api) pointing to 4.157.169.241, with Proxy Status set to Proxied (orange cloud).
api record puts it behind Cloudflare so API Shield can inspect and validate its traffic at api.<student-domain>.Obtain the OpenAPI schema for the orders API. Use the lab schema generator at schema-generator.labs.cfdata.org to produce an OpenAPI document whose servers URL points at https://api.<student-domain>.
servers block. If it points at a placeholder host, the uploaded operations will not match your live traffic and validation never fires.Go to Security > Web assets > Schema validation, click Upload schema, select the file, review that the endpoints map to api.<student-domain>, and click Add schema and endpoints. Then tick the POST /v1/orders row, click Change action, choose Block, and Set action.
Test from the workstation, one compliant request and a few violations:
H=api.<student-domain>
CT="Content-Type: application/json"
curl -s -o /dev/null -w "valid: %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"sku1\"]}" "https://$H/v1/orders"
curl -s -o /dev/null -w "extra field: %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"sku1\"],\"evil\":\"x\"}" "https://$H/v1/orders"
curl -s -o /dev/null -w "missing items: %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\"}" "https://$H/v1/orders"
200/201 (it reaches the origin and creates the order); the violations return 403. The 403 is Cloudflare's schema-validation block at the edge, distinct from the origin's own 400 for bad input, so seeing 403 (not 400) proves the request never reached the backend.Finally, add a fallthrough block. In Schema validation, use the Fallthrough template / setting to Block requests to the API host that do not match any known operation.
14.2 Sequence Analytics (Concept)
With session identifiers configured (Lab 13), you would generate traffic that follows the intended order (for example: check status, list orders, then create an order) plus some out-of-order calls (create an order with no preceding GETs). Sequence Analytics groups requests by session identifier and highlights the common ordered "journeys" through the API.
You would review the results under Security > Web assets > Sequences. When a risky sequence is identified (for example jumping straight to order creation without the normal preceding steps), you would mitigate it with a custom rule that references the API Shield sequence, or gate the sensitive operation behind a required precondition so out-of-order requests are challenged or blocked.
Lab 15 - Threat Intel with Managed Lists (Concept)
15.1 Using Managed Lists (Concept)
You would go to Security > Security rules > Custom rules, create a rule named "Block Malicious IPs by Managed List", and build the match with the expression editor: Field = IP Source Address, Operator = is in list, and select the managed Open Proxies list, then add an Or condition for the Anonymizers list. The Expression Preview reads:
(ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)
You would set the action to Block and deploy.
Lab 16 - Spectrum for TCP/UDP (Concept)
16.1 Protecting TCP/UDP Applications (Concept)
Spectrum lives at (domain) > Spectrum. The page lists any existing applications and offers Create an Application.
The Create an Application dialog collects, in order: the Application Type (protocol carried edge-to-origin, for example TCP or UDP); a Domain subdomain that becomes the public entry point (Cloudflare creates the DNS record); Edge IP Connectivity (IPv4, IPv6, or both); an Edge Port (single port or a range); the Origin (an IP/DNS record, a Load Balancer, or a Virtual Network); and optional Edge TLS Termination, IP Access Rules, and Proxy Protocols.
A representative configuration would front an SSH jump host: Application Type TCP, Domain ssh.<student-domain>, Edge Port 22, Origin the jump host's IP on port 22. Users would connect to ssh.<student-domain> and reach the origin through Cloudflare, with the origin IP never exposed.
Lab 17 - Page Shield: Client-Side Protection
17.1 Client-side Resource Monitoring
Enable monitoring. Go to Security > Settings, open the Client side abuse category, and turn on Continuous script monitoring.
Generate traffic by loading the demo page in the workstation browser: https://ps.<student-domain>/contact/received/. This page deliberately loads several third-party scripts and opens several outbound connections, including a malicious sample. Then review the inventory under Security > Web assets > Client-side resources, across its Scripts, Connections, and Cookies tabs.
useinsider.com, assets.api.useinsider.com, www.rtb123.com, jsdelivr.at) alongside the first-party /js/scripts.min.<hash>.js, and a malicious sample cryptomining.testcategory.com/1.js is surfaced and flagged. The Connections tab lists outbound endpoints the page contacts (for example a hookb.in beacon and a cf-malicious-test.domain.example.com endpoint).17.2 Content Security Rules (CSP)
Monitoring tells you what is running; a Content security rule enforces what is allowed to run. Go to Security > Security rules, find Content security rules, and click Create rule. Name it "Page Shield - Script Allowlist".
Scope the rule to your site host, and build the allowlist from the legitimate resources you saw in 17.1 (leaving the malicious ones off, so they become violations):
- Script sources:
self,useinsider.com,*.useinsider.com,www.rtb123.com, andjsdelivr.at. - Connection sources:
self.
Set Action = Log to start in report-only mode, then Deploy. Once you confirm nothing legitimate is caught, switch the action to Allow to enforce the allowlist.
cryptomining.testcategory.com script and the hookb.in / cf-malicious-test.domain.example.com connections fall outside and are reported as violations. Log emits the report-only CSP header so browsers report violations without blocking; Allow emits the enforcing header.Confirm the policy is live by checking the response header from the workstation:
curl -s -D - -o /dev/null https://ps.<student-domain>/contact/received/ | grep -i content-security
content-security-policy-report-only header listing your allowed script-src and connect-src sources; in Allow mode it is the enforcing content-security-policy header. A resource outside the allowlist is reported (or blocked) accordingly.End of hands-on labs. Lab 18 recaps and, as a concept, describes sealing the origin.
Lab 18 - Seal & Validate the Perimeter (Concept)
18.1 Seal the Origin (Concept)
Proxying through Cloudflare hides the origin IP, but it does not by itself stop someone who already knows the IP (as you did in Lab 1) from connecting directly and skipping every edge control. Closing that gap requires the origin to reject any request that did not come from Cloudflare. There are two standard ways:
- Authenticated Origin Pulls (mTLS): Cloudflare presents a client certificate on every origin request, and the origin's web server is configured to require and verify it (for example nginx
ssl_client_certificate+ssl_verify_client on). Direct-to-IP requests, which lack the certificate, are refused at the TLS layer. You enable this in the zone's SSL/TLS settings and configure the web server to enforce it. - Network IP allowlist: restrict the origin's firewall or cloud security group so only Cloudflare's published IP ranges may reach ports
80and443. Everything else is dropped before it reaches the web server.
18.2 Replay the Lab 1 Attacks (Concept)
On a sealed origin, you would rerun the Lab 1 attacks through the Cloudflare hostname (<student-domain>) and against the raw IP, and compare to your baseline:
- Sensitive file, now virtually patched. The Lab 9 managed ruleset blocks
/.git/secrets.txtthrough Cloudflare with a403, where Lab 1 returned the secrets. - Volumetric abuse, now throttled. The Lab 12 rate limits cap repeated hits with
429, where Lab 1 let every request through. - Direct-to-IP, now refused. A request to
http://20.88.188.200/would fail (connection refused, timeout, or TLS handshake failure) once the origin only accepts Cloudflare traffic.
18.3 Architecture Recap (Concept)
You took AcmeCorp from wide open to a defended edge. Here is the perimeter you built, layer by layer, and the Lab 1 problem each layer answers.
| Layer | Chapters | What it defends |
|---|---|---|
| DNS onboarding & proxy | 2 | Hides the origin IP so attackers cannot target it directly. |
| SSL/TLS encryption | 3 | Ends plaintext and certificate warnings; Full (Strict) end-to-end encryption on a self-hosted origin. |
| Caching & performance | 4, 5 | Absorbs traffic spikes at the edge so the origin stays up. |
| Load balancing & health | 7, 8 | Removes the single point of failure and routes around broken origins (concept in this lab). |
| Traffic & rules engine | 6 | Runs business and hardening logic at the edge, off the origin. |
| WAF (managed + custom) | 9, 10 | Virtually patches the exposed file and enforces access policy. |
| Bot management | 11 | Challenges scrapers while allowing trusted automation. |
| Rate limiting | 12 | Caps volumetric abuse and API fuzzing. |
| API Shield | 13, 14 | Discovers the API, enforces its schema, and flags abnormal sequences. |
| Threat intelligence | 15 | Preemptively blocks anonymizers and open proxies (concept in this lab). |
| Page Shield | 17 | Watches the browser for supply-chain and skimming attacks. |
| Sealed origin | 18 | Forces all traffic through Cloudflare so nothing above can be bypassed (concept in this lab). |
End of Unified Lab Guide (Legacy Edition).