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 is the Legacy (safety-net) edition

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, and Spectrum) 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.

ConventionMeaningExample
BoldNames of selectable items in the web interfaceClick Security to open the Security Rule window.
MonospaceText that you enter and coding examplesEnter the following command: dig example.com
Result textLab step results and explanations, expected system outputHTTP/2 200 OK
ItalicsContextual notes explaining "why" a task is necessaryThis 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

AcmeCorp is a market leader in mock company frontends, and its infrastructure has outgrown its security. Its origin server sits on a public IP address, serving both the corporate website and a public orders API, with no CDN, no real encryption, and no security layer in front of it. It shows: browsers throw certificate warnings, traffic spikes knock the site offline, attackers openly probe the site, scrape pricing, leak sensitive files, and fuzz the API, and because the origin IP is public, anyone can reach it directly and walk around any protection added later.

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. In Lab 2 you onboard the three web hostnames the rest of the guide relies on: the apex (<student-domain>, the AcmeCorp website), ps.<student-domain> (the Page Shield demo page), and public-api.<student-domain> (the public orders API). You add a few more records in the labs that introduce them (ftp in Lab 2, payments.platform in Lab 3, and api in Lab 13). The apex and ps resolve to the shared web origin; public-api (and api) resolve to a separate orders API origin, as the table below shows.

ComponentAddressRole
Ubuntu workstationin-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 origin20.88.188.200Serves the AcmeCorp website. Both the apex (<student-domain>) and the ps Page Shield demo host point here. 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 origin4.157.169.241The backend for the public orders API. Both public-api.<student-domain> (used in the API Shield labs 10.4 and 13) and the api.<student-domain> record you create in Lab 13 point here. It is reachable only from inside the lab network.
One shared origin, not your server. Unlike the self-hosted edition, the origin here is a shared machine operated by Cloudflare for the class. You never log in to it, and you cannot change its configuration or firewall. That is why steps that require origin-side changes (installing an Origin CA certificate, stopping a web server to test failover, sealing the origin behind a firewall) are presented in this edition as Concept walkthroughs rather than hands-on tasks.
Fill in here: your lab value

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

Before you deploy a single Cloudflare feature, you attack AcmeCorp the way the open internet already can. You hit the origin server directly by IP, fingerprint its software, read a sensitive file it should never expose, scrape the product catalog, push a SQL injection string straight through, and knock on the locked admin panel. Nothing stops you. This is AcmeCorp's "before" picture and the baseline every later chapter is measured against.
Estimated time: 25 minutes.
Why this lab exists: you cannot prove Cloudflare improved anything without a documented "before". This chapter is your control group: a run of real attacks against a completely unprotected origin, with every result written down so you can replay the exact same attacks in Lab 14 and watch them fail.
What you'll build: a "before" scorecard, a short table of attacks and their outcomes against the raw origin, that you carry forward to the final lab.
Prerequisites: the in-browser Ubuntu workstation Terminal. There is no Cloudflare configuration in this chapter and nothing to install. Every command runs from the workstation against the origin's public IP.
AcmeCorp Scenario: Before you deploy a single Cloudflare feature, you need to prove how bad the "before" picture is. Cloudflare is not in front of anything yet, so the origin answers directly on its public IP address (20.88.188.200) on both port 80 and port 443, with a self-signed certificate. In this chapter you play the attacker: confirm the origin is exposed, leak a sensitive file, scrape the catalog, and probe the admin panel, all unimpeded. Record every result. It is the baseline that later chapters must beat.
Run everything from the workstation. Open the Terminal on the in-browser Ubuntu workstation and run the commands there. The workstation reaches the origin over the public internet, exactly as any attacker would. There is no SSH step: the terminal is already on the workstation.

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
Why: With no proxy in front, the raw origin IP is publicly routable and the response advertises the web server software in the 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
Verify (exposed): the first command returns HTTP/1.1 200 OK and a Server: header naming the origin's web server (for example nginx/1.29.1). The second shows the certificate's issuer and subject are the same self-signed value (both read O = Cloudflare Labs, OU = Technical Enablement, CN = 20.88.188.200), which is why a real browser would throw a certificate warning. A certificate whose Common Name is a bare IP, self-signed by the server it protects, is exactly what you replace with a real edge certificate in Lab 3. 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
Why: There is no Web Application Firewall inspecting Layer 7 traffic, so nothing blocks access to obviously sensitive paths like an exposed .git directory. This is exactly the kind of exposure you will virtually patch with the managed WAF in Lab 7.
Verify (secrets leaked): the request returns 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
Why: With no bot management or rate limiting, an automated client can pull the entire catalog as fast as it likes. This is the scraping you will challenge in Lab 9 and throttle in Lab 12.
Verify (unlimited scrape): all 50 requests return 200 (the output reads 50 200). Every scrape succeeded; nothing slowed the client down.

1.4 Push a SQL Injection String Straight Through

The website has a contact form. Submit a classic SQL injection payload to its comment field and watch how the origin responds:

curl -s -o /dev/null -w "%{http_code}\n" "http://20.88.188.200/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Why: The decoded payload is admin' OR '1'='1' -- , the textbook tautology used to subvert a naive SQL query. With no Web Application Firewall inspecting Layer 7 traffic, nothing recognizes or blocks the pattern: the request sails straight to the origin and is answered normally. This is precisely the attack you virtually patch with the OWASP and Cloudflare managed rulesets in Lab 7.
Verify (injection accepted): the request returns 200. The malicious string was accepted and served without objection. Note this result: in Lab 7 the identical request through Cloudflare returns 403 and a block page.

1.5 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
Why: The admin panel is protected by HTTP Basic authentication at the origin, so an unauthenticated request returns 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 8).
Verify (locked, but reachable): the request returns 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.

1.6 Record the "Before" Scorecard

Write down what you just observed. This is the baseline you replay in Lab 15, after the full perimeter is in place, to prove each gap is closed.

AttackTargetResult against the raw origin
Direct hit and fingerprinthttp://20.88.188.200/200, Server: nginx/1.29.1, self-signed cert
Leak the secrets file/.git/secrets.txt200, prints acmeadmin / #Super.Secret-5+
Scrape the catalog/services/luggages/ (50x)50 200, unlimited
SQL injection probe/contact/?comment=...200, accepted
Probe the admin panel/admin401, reachable on the public IP
You just landed a run of attacks against a completely unprotected origin: exposure and fingerprinting, a leaked secrets file, unlimited catalog scraping, an accepted SQL injection string, and a directly reachable admin panel. Keep this scorecard. Lab 14 seals the perimeter and replays every one of these attacks through Cloudflare to prove each gap is closed.

Lab 1 Wrap-up

MilestoneHow you confirmed it
Origin is directly reachable and fingerprintedcurl -sI returned 200 and a Server header
Origin serves an invalid (self-signed) certificateopenssl showed issuer equal to subject
A sensitive file is publicly served/.git/secrets.txt returned 200 with credentials
No rate control on the catalog50 of 50 scrape requests returned 200
No Layer 7 filteringthe SQL injection probe returned 200
Admin panel is locked at the origin but exposed on the IP/admin returned 401

Lab 1 Troubleshooting

SymptomLikely causeFix
curl hangs or times outYou are testing from your own machine, not the lab workstationRun every command in the in-browser Ubuntu workstation Terminal, which sits on the lab network
openssl prints nothingThe pipe swallowed the certificate or port 443 was slow to answerRe-run the command; the self-signed certificate is served on 20.88.188.200:443
The secrets file returns 404Typo in the pathThe path is /.git/secrets.txt exactly; /secrets.txt and /.git/config both return 404
The admin probe returns 401, not 200That is the expected resultThe panel is locked by Basic auth at the origin; the leaked credentials do not open it and there is no /login form (it returns 404). The point is only that /admin is reachable on the raw IP
Lab 1 to Lab 2: Every attack landed because the origin answers on a public IP that anyone can reach. The first move toward a real perimeter is to put Cloudflare in front of that IP and hide it. In Lab 2 you confirm Cloudflare is your authoritative DNS, create and proxy the web records so the origin IP disappears behind Cloudflare's edge, and learn when to deliberately leave a record unproxied.

Lab 2 - DNS Onboarding & Proxy Control

Those attacks landed because the origin IP is public. You put Cloudflare in front by confirming authoritative DNS, proxying the web records so the real origin IP disappears, and learning when to leave a record unproxied (grey cloud).
Estimated time: 28 minutes.
Why this lab exists: in Lab 1 every attack landed on a public origin IP. Hiding that IP behind Cloudflare's proxy is the foundation for every security feature in the rest of the guide; a feature at the edge is worthless if an attacker can skip it by talking to the origin directly.
What you'll build: proxied A records for the apex, ps, and public-api hostnames, one deliberately unproxied ftp record, and a hands-on demonstration that a grey-cloud record re-exposes the origin.
Prerequisites: Lab 1 complete. Dashboard access to the pre-onboarded zone and the workstation Terminal for dig.
AcmeCorp Scenario: AcmeCorp's first priority is to place Cloudflare in front of their server to hide the origin IP (20.88.188.200). In the managed lab the zone is already onboarded (nameservers assigned) for you, so you will confirm that Cloudflare is authoritative, create and proxy the web record so it hides the origin, and practice the proxy on/off decision for a legacy protocol.

2.1 Cloudflare as Authoritative DNS

Concept - onboarding is already done for you. In a real deployment you would click Add a site in the dashboard, enter <student-domain>, select the Enterprise plan, let the quick scan import existing records, and finally change the nameservers at your registrar to the pair Cloudflare assigns (for example howard.ns.cloudflare.com and mia.ns.cloudflare.com). That nameserver change is what makes Cloudflare authoritative. In this managed lab the zone is already onboarded on the Enterprise plan (nameservers assigned), so you confirm the nameservers below, then create the DNS records the labs need.
Why the Enterprise plan matters: the advanced WAF, Bot Management, and API Shield features that later labs use are Enterprise capabilities, which is why the provided zone is on that plan.

From the workstation Terminal, confirm the nameservers are Cloudflare's:

dig +short NS <student-domain>
Expected output (two Cloudflare nameservers, names vary):
howard.ns.cloudflare.com.
mia.ns.cloudflare.com.
Why: Seeing Cloudflare nameservers confirms the domain is fully onboarded to the Cloudflare control plane, which is the prerequisite for every proxy and security feature that follows.

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).

Why: The apex record is what publishes the AcmeCorp website through Cloudflare. Proxying it (orange cloud) ensures all traffic hits Cloudflare's security edge first, hiding the true origin IP. This is the primary web record the rest of the guide relies on; you onboard the other two web hostnames next.

Still in DNS > Records, create the other two web hostnames the later labs rely on, each as an A record with Proxy Status Proxied (orange cloud):

Why onboard them now: onboarding all three web hostnames up front keeps the later chapters self-contained, so every request in the WAF, bot, rate-limiting, API Shield, and Page Shield labs resolves through Cloudflare from the start. Note that the apex and ps share the web origin, while public-api is served by a separate orders API origin: that is why they point to different IP addresses.

2.2 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).

Why: Cloudflare's standard proxy only intercepts HTTP/HTTPS (ports 80/443). A legacy protocol such as FTP (port 21) would be dropped if it were proxied. Grey-clouding exposes the origin IP but lets the legacy protocol function, which is the trade-off you are demonstrating.

From the workstation, query that record and compare it to a proxied record:

dig +short ftp.<student-domain>
dig +short <student-domain>
Expected output: the grey-clouded 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).
Why: The contrast proves the point: a grey-cloud record leaks the origin IP, while an orange-cloud record hides it behind Cloudflare. When you are done, delete the ftp record (or leave it grey) and make sure the apex stays Proxied so the web application remains protected.

2.3 Experience: A Grey Cloud Re-Exposes the Origin

Proxy status is not a cosmetic toggle. To feel what a mistake costs, temporarily break one of your web records and watch the origin IP come back into the open. In DNS > Records, edit the ps record and switch its Proxy Status from Proxied to DNS only (grey cloud), then Save.

From the workstation, resolve the hostname (query a public resolver so no local cache hides the change):

dig +short @1.1.1.1 ps.<student-domain>
Observe the break: the query now returns the raw origin IP 20.88.188.200. By grey-clouding the record you told the world exactly where the server lives, and every edge protection you build later could be bypassed by hitting that address directly. This is the single most common way teams accidentally undo their own perimeter.

Diagnose and fix. A grey cloud means Cloudflare only answers DNS for the name and does not proxy its traffic, so the record must publish the true origin address. The fix is to put the record back behind the proxy: edit ps again and set Proxy Status to Proxied (orange cloud), then Save.

dig +short @1.1.1.1 ps.<student-domain>
Verify the fix: the query returns Cloudflare edge addresses again (for example 104.18.x), and the origin IP is hidden. Lesson: orange cloud is what puts Cloudflare in the request path; any web hostname you leave grey is a hole straight to the origin. Confirm the apex, ps, and public-api are all Proxied before you move on.

Lab 2 Wrap-up

MilestoneHow you confirmed it
Cloudflare is authoritative for the zonedig NS returned Cloudflare nameservers
Apex, ps, and public-api are proxieddig +short returned Cloudflare edge IPs, not the origin
The origin IP is hidden behind the proxynone of the proxied names resolve to 20.88.188.200
A grey-cloud record exposes the originthe ftp record (and the temporarily grey ps) resolved to 20.88.188.200

Lab 2 Troubleshooting

SymptomLikely causeFix
dig still returns the origin IP for a proxied nameLocal resolver cached the old (or negative) answerQuery a public resolver: dig +short @1.1.1.1 <name>, or run sudo resolvectl flush-caches
A proxied name returns no answer at allThe record was just created and the resolver cached an earlier NXDOMAINWait a few seconds and query @1.1.1.1; the authoritative record is live immediately
A grey-clouded web record still leaks the originExpected: a grey cloud publishes the true origin IPSet the record back to Proxied (orange cloud) so Cloudflare hides the origin
Lab 2 to Lab 3: The origin IP is now hidden and all web traffic flows through Cloudflare. But visitors still get a certificate warning, because the origin only has a self-signed certificate (Lab 1). In Lab 3 you put a real, trusted certificate at the edge, enforce modern TLS, and encrypt the Cloudflare-to-origin hop, deliberately breaking and then fixing the connection along the way.

Lab 3 - SSL/TLS & Encryption Standards

The browser padlock and modern TLS need to be in place. You turn on edge encryption (Always Use HTTPS and a modern minimum TLS version), inspect the Universal SSL certificate Cloudflare issues for free, and order an advanced certificate for a deep subdomain that Universal SSL cannot cover.
Estimated time: 30 minutes.
Why this lab exists: Lab 1 showed the origin serving a self-signed certificate that browsers reject. Real encryption has two hops (client to Cloudflare, and Cloudflare to origin) and this lab hardens both, while showing how the wrong SSL mode can silently break part of your site.
What you'll build: edge encryption (Always Use HTTPS, Min TLS 1.2), an Origin CA certificate, the correct encryption mode for this environment, and a dedicated advanced certificate for a deep payments subdomain.
Prerequisites: Lab 2 complete (apex, ps, and public-api proxied). Workstation Terminal for curl and openssl.
AcmeCorp Scenario: AcmeCorp needs a valid certificate on the public site and modern encryption standards enforced at the edge. You will secure the "first mile" (client to Cloudflare) with Universal SSL and HTTPS settings, understand what full end-to-end encryption would require, and order an Advanced Certificate for a deep payments subdomain.

3.1 Universal SSL, Flexible Mode & Edge Encryption

Go to SSL/TLS > Edge Certificates and confirm the Universal SSL certificate is Active.

Why: Universal SSL provides the free, automated certificate that covers the apex and one level of subdomains (<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)".)

Why: Flexible mode encrypts traffic from the user to Cloudflare but connects to the origin over HTTP. It provides immediate relief for browser warnings without any server-side change, which is why it is the fastest first step. You then layer stricter edge settings on top of it, so you can see the difference between raw Flexible mode and a hardened edge.

On the same Edge Certificates page, turn Always Use HTTPS On.

Why: This redirects any plaintext 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.

Why: TLS 1.0 and 1.1 are deprecated and insecure. Enforcing a modern floor is a baseline compliance requirement.

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
Verify: the plaintext request returns a 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.

Flexible restored the browser padlock, but it quietly broke something. From the workstation, compare the website against the orders API, both proxied through Cloudflare since Lab 2:

curl -s -o /dev/null -w "website:    %{http_code}\n" https://<student-domain>/
curl -s -o /dev/null -w "orders API: %{http_code}\n" https://public-api.<student-domain>/v1/orders
Observe: the website returns 200, but the orders API returns 522. Flexible connects to the origin over plain HTTP on port 80. The website origin answers on port 80, so it works, but the orders API origin listens only on HTTPS port 443, so Cloudflare's connection on port 80 times out and the edge returns 522. Flexible cannot reach a TLS-only origin. You fix this in 3.2 by encrypting the Cloudflare-to-origin hop.

3.2 Full (Strict) + Origin Certificate

AcmeCorp Scenario: Security compliance requires strict end-to-end encryption, not just client-to-Cloudflare. That means Cloudflare must encrypt the connection to the origin and mathematically validate the origin's certificate. The first step is generating the certificate the origin would present.

Go to SSL/TLS > Origin Server and click Create Certificate.

Why: A Cloudflare Origin CA certificate is a free, Cloudflare-signed TLS certificate for the backend server. Creating one avoids the cost and hassle of purchasing a third-party certificate for the origin.

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.

Why: The wildcard plus apex covers every hostname on the zone with a single certificate, and letting Cloudflare generate the key and CSR means you do not need OpenSSL on the workstation.

Copy the generated Origin Certificate and Private Key, save each to a file (for example cert.pem and key.pem), then click OK.

Why: Cloudflare does not store the private key, so this is your only opportunity to capture it. In a full deployment you would paste these onto the origin web server.
Verify: the new certificate appears in the Origin Certificates list showing 2 hosts (*.<student-domain>, <student-domain>) with an expiry roughly 15 years out.

Now encrypt the Cloudflare-to-origin hop. Go to SSL/TLS > Overview, open Configure encryption mode, select Full, and click Save.

Why: Full connects to the origin over HTTPS on port 443 and accepts the certificate the origin presents without validating it. Both origins already serve a self-signed certificate on port 443, so Full works immediately with no origin-side change, and it repairs the orders API that Flexible broke.

Re-run the comparison from the workstation:

curl -s -o /dev/null -w "website:    %{http_code}\n" https://<student-domain>/
curl -s -o /dev/null -w "orders API: %{http_code}\n" https://public-api.<student-domain>/v1/orders
Verify: both now return 200. The orders API went from 522 to 200 because Cloudflare now reaches it on port 443. From here on the website, the orders API, and every API Shield lab ride an encrypted Cloudflare-to-origin connection.
Why this lab lands at Full, not Full (Strict): Full (Strict) is the production goal because it also validates the origin certificate, which stops an attacker from impersonating your origin. Validation requires a trusted certificate (a Cloudflare Origin CA certificate, like the one you just created, or a CA-signed certificate) installed on the origin. This managed lab uses a shared, Cloudflare-operated origin with no shell access, so you cannot install that certificate. If you select Full (Strict) here, Cloudflare rejects the origin's self-signed certificate and returns 526 for the entire zone (try it, then switch back to Full). Full is therefore the strongest mode reachable in this environment. In the student-provisioned (self-provisioned) edition you have full origin access: you install the Origin CA certificate you just generated, reload the web server, and legitimately finish at Full (Strict).

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).

Why: The payment portal hostname must resolve through Cloudflare before you can order an edge certificate for it. It is a deep subdomain (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.

Why: Universal SSL already covers the apex and wildcard, so a dedicated single-hostname certificate for the deep subdomain is all you need here.

Set Certificate Authority to Google Trust Services, leave Certificate validation method on TXT Validation, keep the default Certificate Validity Period, then click Save.

Why: On this full-setup zone Cloudflare performs Domain Control Validation automatically for TXT validation, so you do not have to add any record by hand.
Verify: the new Advanced certificate for 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.
Why: Advanced Certificate Manager issues certificates for deep subdomain hierarchies that Universal SSL cannot reach, and lets you choose the CA and validation method.

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
Verify: the issuer is your chosen CA (for example 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.
Why: Cloudflare intelligently serves the most specific certificate available for a requested hostname, so the advanced certificate is presented for the payments subdomain while Universal SSL still covers the rest of the zone.

Lab 3 Wrap-up

MilestoneHow you confirmed it
Edge encryption enforcedplaintext http:// returned a 301/308 to https://
Flexible mode broke the TLS-only APIthe orders API returned 522 under Flexible
Full mode repaired it end to endwebsite and orders API both returned 200 under Full
Advanced certificate serves the deep subdomainopenssl showed the chosen CA issuer and a SAN for payments.platform.<student-domain>

Lab 3 Troubleshooting

SymptomLikely causeFix
Whole zone returns 526You selected Full (Strict) but the origin only has a self-signed certificateSwitch back to Full; Full (Strict) needs a trusted origin certificate, which this shell-less shared origin cannot install
Orders API returns 522SSL mode is Flexible, which connects to the origin on port 80; the API is HTTPS-onlySet the encryption mode to Full
Advanced certificate stuck on Pending ValidationDCV still in progressWait a few minutes; on this full-setup zone Cloudflare completes TXT validation automatically
openssl shows the wrong (wildcard) certificate for paymentsThe advanced certificate is not active yetRe-run once it reads Active; Cloudflare then serves the most specific certificate
Lab 3 to Lab 4: Traffic is now proxied and encrypted end to end. The next pressure on AcmeCorp is load: every page view still travels all the way to the origin. In Lab 4 you establish a caching baseline, offload a path with a cache rule, hand cache control back to the origin's headers, and clean up a stale cached response.

Lab 4 - Caching Foundations

Traffic spikes are overwhelming the origin. You establish a caching baseline, offload a dynamic path with a cache rule, honor the developers' cache-control headers, and purge a stale cached response on demand.
Estimated time: 27 minutes.
Why this lab exists: with traffic now flowing through Cloudflare, caching is the fastest way to take load off the struggling origin. You need to know what is cached by default, how to force caching where you want it, and how to reverse a cache mistake quickly.
What you'll build: a cache baseline reading, a cache rule that offloads a category page, an origin-driven cache configuration, a surgical purge, and a controlled cached-404 with a status-code TTL.
Prerequisites: Lab 3 complete (site reachable over HTTPS through Cloudflare). Workstation Terminal for curl -I.
AcmeCorp Scenario: Traffic spikes are overwhelming the AcmeCorp origin. You must diagnose what is and is not cached, force caching on a path that is served dynamically by default, respect developer cache headers, and clean up a stale cached response during a live change.
How to read cache headers: several steps ask you to check response headers such as cf-cache-status, age, and cache-control. From the workstation Terminal, the quickest way is:
curl -sSI https://<student-domain>/services/luggages/ | grep -iE "cf-cache-status|age|cache-control"
The cf-cache-status value is one of MISS, HIT, BYPASS, EXPIRED, REVALIDATED, or DYNAMIC. The age header only appears on a HIT and reports how long the object has been in Cloudflare's cache.

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
Why: AcmeCorp needs a known baseline before troubleshooting. HTML pages are not cached by default, so Cloudflare treats this path as dynamic content that is fetched from the origin every time.
Verify (baseline): the header reads 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/.

Why: Cache Rules let you override the default caching behavior for a specific path so a normally-dynamic page can be served from the edge, taking load off the origin.

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.

Why: "Eligible for cache" makes the dynamic page cacheable, and "Ignore cache-control header and use this TTL" makes the rule authoritative for 30 seconds regardless of what the origin sends. A short 30-second TTL keeps the lab quick to re-test.

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
Verify (offloaded): the first request is a 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 set Browser Cache TTL to Respect Existing Headers. On a newly onboarded zone this dropdown often defaults to a fixed time such as 4 hours, so change it if needed.

Why: AcmeCorp's developers want to control cache lifetime from their application code via a 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).

Why: A Cache Rule that ignores cache-control would override the origin's headers. Disabling it lets the developer's 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"
Verify: with the rule off, control returns to the origin. This origin sends no 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.
Why: With the rule off, Cloudflare honors whatever 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.

Why: Cloudflare can cache error responses (including 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.

Why: A custom purge is surgical: it clears exactly the one object without dumping the entire cache, so the rest of the site keeps its performance benefit while the stale response is removed.

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/"):

Why: A Status Code TTL caches responses based on the origin's HTTP status code, independent of the normal Edge TTL. Caching a 404 for even 10 seconds means a flood of requests for a missing URL is answered from the edge instead of hammering the origin, while the short TTL keeps things fresh once that URL exists again.
Verify the cached 404: request the missing URL twice within 10 seconds and watch the cache status:
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 shows cf-cache-status: EXPIRED: Cloudflare still had the 404 cached but the TTL lapsed, so it revalidates with the origin. The request right after that returns to HIT as the freshly revalidated 404 is served from the edge again.

Lab 4 Wrap-up

MilestoneHow you confirmed it
Established the cache baselinecf-cache-status: DYNAMIC on the category page
Offloaded a path with a cache rulecf-cache-status went MISS then HIT with an age
Handed cache control back to the originwith the rule off the page returned to DYNAMIC
Purged a stale object surgicallyCustom Purge by URL served a fresh response
Controlled a cached 404the 404 went MISS -> HIT -> EXPIRED under a 10s status-code TTL

Lab 4 Troubleshooting

SymptomLikely causeFix
Page stays DYNAMIC after adding the cache ruleThe rule did not match, or Edge TTL mode was left unsetConfirm the URI Path matches exactly and set an Edge TTL mode before Deploy
Browser Cache TTL will not "respect headers"A fresh zone often defaults this dropdown to a fixed time (for example 4 hours)Set Browser Cache TTL to Respect Existing Headers explicitly in Caching > Configuration
Cached 404 shows EXPIRED, not MISSExpected after the TTL lapses; Cloudflare revalidates a still-held 404Nothing to fix; the next request returns to HIT
Purge did not refresh the objectPurged a different URL than the one servedPurge the exact absolute URL, including trailing slash
Lab 4 to Lab 5: One cache rule on one path is not enough to survive a real surge. In Lab 5 you cache full HTML safely with a cookie bypass, normalize marketing URLs with a custom cache key, and enable tiered caching, and you fix a classic cookie-bypass ordering mistake using the Trace tool.

Lab 5 - Advanced Caching

One cache rule on one path will not carry AcmeCorp through a real surge. You cache entire HTML category pages, then make that safe for logged-in users with a cookie bypass, and along the way you trip over the single most common cache-rule mistake and fix it. You normalize marketing URLs with a custom cache key, then switch on tiered caching so the origin sees a fraction of the load.
Estimated time: 30 to 35 minutes.
Why this lab exists: Lab 4 offloaded a few asset paths, but the pages that actually melt an origin during a sale are the HTML category pages themselves. Caching HTML is powerful and dangerous: get the rule order wrong and you serve one shopper's session to another. This lab caches HTML safely and teaches the ordering rule that keeps it safe.
What you'll build: a Cache Everything rule on a category page; a session-cookie Bypass rule (and the fix for the classic ordering mistake); a custom cache key that ignores marketing query strings; and Smart Tiered Cache.
Prerequisites: Lab 4 complete (you can read cf-cache-status from curl -I). Dashboard access to Caching > Cache Rules and Caching > Tiered Cache. Workstation Terminal.

5.1 Cache an entire HTML page (Cache Everything)

The /services/safes/ category page is plain HTML that changes rarely, yet every hit currently goes to the origin (cf-cache-status: DYNAMIC). During a promotion, thousands of shoppers loading that page can overwhelm AcmeCorp. You will cache the HTML itself at the edge.

In the dashboard, go to Caching > Cache Rules and click Create rule, then choose Cache Rules from the dropdown. Configure:

From the Workstation Terminal, prime and confirm the cache. The first request is a MISS (fetched from origin and stored), the second is a HIT (served from the edge):

curl -sSI https://<student-domain>/services/safes/ | grep -i cf-cache-status
curl -sSI https://<student-domain>/services/safes/ | grep -i cf-cache-status
Verify: the first line reports cf-cache-status: MISS and the second cf-cache-status: HIT. The full HTML page is now served from Cloudflare's edge.

Caching HTML is dangerous the moment users can log in: a cached personalized page could be handed to the wrong visitor. The fix is a second rule that bypasses cache whenever a session cookie is present, so anonymous shoppers get the fast cached page while logged-in users always reach the origin.

Create a second cache rule (Caching > Cache Rules > Create rule > Cache Rules):

Break it and watch. Send an anonymous request and a request that carries a session cookie:

curl -sSI https://<student-domain>/services/safes/ | grep -i cf-cache-status
curl -sSI -H "Cookie: session_id=abc123" https://<student-domain>/services/safes/ | grep -i cf-cache-status
Observe: both requests report cf-cache-status: HIT. The cookied request was supposed to bypass, but it is being served from cache anyway. On a real site this is exactly how one user's logged-in page leaks to another.

Diagnose. Cache Rules are last matching rule wins: rules run top to bottom, and the last rule that sets a given behavior overrides earlier ones. Your cookied request matches both rules, but because Cache Everything sits below the Bypass rule, its "Eligible for cache" wins and cancels the bypass. Putting the bypass "first" is backwards.

Fix. In Caching > Cache Rules, use the Bypass rule's kebab menu > Move down (or edit it and set Place at > Last) so the order is Cache Everything, then Bypass. Deploy, then re-run the same two commands:

curl -sSI https://<student-domain>/services/safes/ | grep -i cf-cache-status
curl -sSI -H "Cookie: session_id=abc123" https://<student-domain>/services/safes/ | grep -i cf-cache-status
Verify: the anonymous request still reports HIT, but the cookied request now reports cf-cache-status: DYNAMIC (bypassed to origin). Lesson: with Cache Rules, order is a security control, not a cosmetic one. The bypass must come after (below) the rule it is meant to override.

5.3 Collapse marketing URLs with a custom cache key

Marketing appends tracking tags (?v=aaa, ?utm_source=email) to links. By default each unique query string is a separate cache object, so every tagged variant misses the cache and hits the origin, wrecking the Cache Hit Ratio. You will cache the /services/luggages/ page with a custom cache key that ignores the query string, collapsing every variant into one object.

Create a cache rule (Caching > Cache Rules > Create rule > Cache Rules):

curl -sSI "https://<student-domain>/services/luggages/?v=aaa" | grep -i cf-cache-status
curl -sSI "https://<student-domain>/services/luggages/?v=bbb" | grep -i cf-cache-status
Verify: ?v=aaa reports MISS (first fetch) and ?v=bbb reports HIT, even though the query strings differ. Both variants resolved to the same cached object, so tracking tags no longer bypass the cache.

5.4 Shield the origin with Tiered Cache

Even with HTML cached, every one of Cloudflare's global data centers can independently miss and reach back to the single AcmeCorp origin on a cold cache. Tiered Cache arranges data centers into lower and upper tiers so lower-tier PoPs ask an upper-tier PoP before ever touching the origin.

Go to Caching > Tiered Cache, select Smart Tiered Cache (Recommended), and click Apply.

Why: misses are funneled through a small number of upper-tier data centers instead of fanning out to the origin from everywhere, so origin bandwidth and CPU drop sharply during a surge and the Cache Hit Ratio climbs. There is nothing to break here; it is a global topology switch that only reduces origin load.

Lab 5 Wrap-up

MilestoneHow you confirmed it
Cached a full HTML page/services/safes/ went MISS then HIT under Cache Everything
Reproduced the ordering leakwith Bypass placed first, a cookied request wrongly returned HIT
Fixed it with rule orderwith Bypass placed last, the cookied request returned DYNAMIC while anonymous stayed HIT
Collapsed query-string variants?v=aaa and ?v=bbb shared one cached object (MISS then HIT)
Enabled Tiered CacheSmart Tiered Cache applied in Caching > Tiered Cache

Lab 5 Troubleshooting

SymptomLikely causeFix
Cookied request still HIT after you "fixed" the orderBypass rule is not actually last, or the change was not deployedConfirm the Cache Rules list shows Cache Everything above Bypass and that you clicked Deploy
Page never becomes HITCache eligibility left as Bypass, or the URI Path did not matchEdit the rule, set Eligible for cache, and match the exact path including the trailing slash
Different query strings still MISS separatelyIgnore query string not enabled in the cache keyEdit the luggages rule, expand Cache key, enable Ignore query string, Deploy
Cookied request returns DYNAMIC but so does anonymousCache Everything rule disabled or ordered after BypassEnsure Cache Everything is enabled and sits above Bypass
Lab 5 to Lab 6: The edge now absorbs surges and serves logged-in users safely. Next you put the edge to work on business logic the origin should never run: localized redirects, campaign redirects, security response headers, and URL rewrites, all without touching origin code.

Lab 6 - Traffic Management & Rules Engine

The business needs edge logic the origin should not have to run. You localize Portuguese visitors, stand up a campaign redirect, add a security response header, switch on Cloudflare's managed security headers, and rewrite a retired product URL to its replacement, all without touching origin code.
Estimated time: 30 to 35 minutes.
Why this lab exists: AcmeCorp keeps asking the origin to run business logic it should never run: geo-routing, campaign redirects, security headers, retiring old URLs. Every one of those is a request the struggling origin has to process. The edge can do all of it in front of the origin, faster and without a code change.
What you'll build: a geo redirect for Portugal, a campaign short-link redirect, a response-header transform, a managed set of security headers, and a transparent URL rewrite for a discontinued product line.
Prerequisites: Lab 3 complete (site reachable over HTTPS through Cloudflare). Dashboard access to Rules > Overview and Rules > Settings. Workstation Terminal for curl.

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.

Why: AcmeCorp is expanding into Europe. Executing geographic redirects at the edge is much faster than running geographic IP lookups on the struggling origin server, and the dynamic expression keeps the visitor on the same host while sending them to the localized /pt path.
Verify: the rule appears enabled in the Redirect Rules list. Because the workstation is not in Portugal, a normal request is a control and is not redirected:
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.

Why: This simulates a bulk campaign redirect. Doing it via Cloudflare Rules prevents the origin from having to process and return 301/302 responses, and scoping it to the campaign path keeps the rest of the site working. Cloudflare issues the redirect at the edge, so it behaves identically no matter what the origin serves.
Verify: from the workstation, request the campaign path and confirm the edge issues the redirect:
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:"
Why: when a request is proxied through Cloudflare, Cloudflare replaces the origin's 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.

Why: 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.
Verify: from the workstation, confirm both the masked server header and your new header appear:
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.

Why: Managed Transforms are pre-built, Cloudflare-maintained header changes you toggle instead of authoring a rule. Add security headers injects a standard browser-hardening set in one click: 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.)
Verify: reload the response headers and confirm the managed security headers now appear and 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.

Why: a URL rewrite changes the path the origin receives while the visitor's address bar is unchanged. Unlike a redirect (which sends a 3xx back to the browser), a rewrite is transparent, so old links and bookmarks keep working and the origin serves the replacement content.
Verify: from the workstation, request the retired path and confirm it returns the luggages content with a 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.

Lab 6 Wrap-up

MilestoneHow you confirmed it
Geo redirect scoped to Portugala non-PT request stayed 200 (control), proving the rule is country-scoped
Campaign short link redirects/promo returned 301 with a location: to the campaign URL
Added a security response headerx-frame-options: SAMEORIGIN appeared and server: read cloudflare
Enabled managed security headersx-content-type-options: nosniff present, x-powered-by gone
Retired a product URL transparently/services/arrows/ returned 200 with luggages content, no visible redirect

Lab 6 Troubleshooting

SymptomLikely causeFix
Rewrite returns 404 or a broken pathLeading slash typed into the Rewrite value produced //services/luggages/Type services/luggages/ without the leading slash (the field already shows the /)
Campaign redirect interferes with later labsThe /promo redirect rule is still enabledDisable the campaign redirect rule after verifying it
New security header does not appearRule not deployed, or scoped to a path instead of All incoming requestsConfirm the Response Header Transform rule is enabled and applies to all requests
Geo redirect never firesExpected from the workstation, which is not in PortugalNothing to fix; the country-scoped rule only redirects PT source IPs
Lab 6 to Lab 7: The edge now runs AcmeCorp's routing and header logic. But none of it stops an attacker. Starting next lab you build the security perimeter, beginning with the WAF Managed Rulesets that virtually patch the very SQL injection you landed in Lab 1.

Lab 7 - WAF Managed Rulesets

You start closing the holes you opened in Chapter 1. Two of the Lab 1 attacks still land through Cloudflare right now: the leaked /.git/secrets.txt file and the SQL injection on the contact form. In this chapter you deploy Cloudflare's managed rulesets the way a real rollout is done, in Log first and then Block, virtually patch both attacks at the edge, discover that your fix also hard-blocks a legitimate customer, repair that with a surgical exception, and finish by triaging the whole incident in Security Analytics.
Estimated time: 40 to 50 minutes.
Why this lab exists: in Lab 1 you pulled /.git/secrets.txt straight off the origin and pushed a SQL injection string through untouched, and you cannot fix the origin's code today. Cloudflare's managed WAF "virtually patches" the server: it recognizes and blocks requests for exposed source-control artifacts and Layer 7 attack patterns at the edge, before they ever reach the origin. Turning it on carelessly also blocks real customers, so you learn to stage it in Log, observe, and only then enforce.
What you'll build: the Cloudflare Managed Ruleset rolled out Log then Block, the OWASP Core Ruleset staged in Log as defense in depth, a surgical managed-rules exception that unblocks the contact form without disabling protection, and the Security Analytics workflow that tells an attacker from a customer.
Prerequisites: Lab 2 complete (traffic is proxied through Cloudflare). Dashboard access to Security > Security rules and Security > Analytics. Workstation Terminal for curl.

7.1 Prove the Lab 1 attacks still land through Cloudflare

The origin is proxied now, but proxying alone does not inspect traffic. From the workstation, replay two of the Lab 1 attacks against the proxied hostname (not the raw IP) and confirm they still succeed:

curl -s -o /dev/null -w "secrets.txt: %{http_code}\n" https://<student-domain>/.git/secrets.txt
curl -s -o /dev/null -w "contact SQLi: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Verify (still exposed): both return 200. The leaked .git secrets file (the same acmeadmin / #Super.Secret-5+ credentials from Lab 1) is served, and the SQL injection tautology on the comment field sails straight to the origin. With no WAF rules deployed, Cloudflare forwards both. These are the two attacks you close in this lab.

7.2 Deploy the Managed Ruleset the safe way: Log, observe, then Block

A managed WAF can block legitimate traffic the instant you enable it, so professionals roll it out in Log first, watch what it would do, and only then switch to Block. Go to Security > Security rules. In the Managed rules section, click Deploy managed ruleset and deploy the Cloudflare Managed Ruleset. On its configuration, set the ruleset action override to Log and deploy.

Why: The Cloudflare Managed Ruleset is the primary, Cloudflare-maintained defense against common Layer 7 threats and known-bad requests, including attempts to reach exposed .git and other sensitive paths. Staging it in Log lets you see every request it would act on without breaking a single customer.

Re-run the secrets-file attack, then look at what the WAF recorded:

curl -s -o /dev/null -w "secrets.txt: %{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify (observed, not yet blocked): the request still returns 200 because Log does not block. Go to Security > Analytics > Events: a new event for /.git/secrets.txt appears under Managed rules with Action = Log, matched rule Version Control - Information Disclosure. You can now see the attack the WAF caught without having risked blocking anyone.

Now enforce. Edit the Cloudflare Managed Ruleset and change the action override from Log to Block, then deploy. Wait 15 to 30 seconds for the change to reach the edge and re-run the attack:

curl -s -o /dev/null -w "secrets.txt: %{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify (virtually patched): the request now returns 403 and the Cloudflare block page. The managed ruleset blocks the request for the exposed source-control path at the edge, even though the origin would still happily serve it. In Security > Analytics > Events the same path now shows Action = Block. You just virtually patched a file leak you cannot fix at the origin.

7.3 Add OWASP Core as defense in depth (Log)

Back in Security > Security rules > Managed rules, click Deploy managed ruleset again and deploy the Cloudflare OWASP Core Ruleset. On its configuration page, leave Anomaly Score Threshold at Medium (40) and Paranoia Level at PL1, set the action to Log, and deploy.

Why: OWASP provides industry-standard protection against the Top 10 (SQLi, XSS, and more) using anomaly scoring: each matching rule adds points, and it acts only once the total crosses the threshold. That scoring model is powerful but prone to false positives, so you stage it in Log at PL1 and observe before ever enforcing. It runs alongside the Cloudflare Managed Ruleset as a second, independent layer.
Verify (staged): in Security > Security rules > Managed rules, the Cloudflare OWASP Core Ruleset is listed as deployed with Action = Log, Threshold = Medium (40), and Paranoia = PL1. You will tune and enforce OWASP as real traffic accumulates; for now it is observing safely.

7.4 The fix breaks a customer: reproduce, then repair with a surgical exception

The contact form at /contact/ accepts free text that can look like an injection payload, so a real customer message can trip the WAF. Reproduce the false positive from the workstation:

curl -s -o /dev/null -w "contact: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Verify the false positive: you get 403 and the Cloudflare block page. A legitimate contact-form submission is hard-blocked, which for AcmeCorp is lost customer contact and lost revenue. In Security > Analytics > Events a Block event appears for /contact/ under Managed rules; expand it to see the matched rule SQLi - Comment in the Cloudflare Managed Ruleset.

Now the experience: break it, watch it fail, then fix it. The instinct is to add a skip 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. Leave Place at on its default of Last and Deploy. Wait 15 to 30 seconds and re-test with a fresh marker:

curl -s -o /dev/null -w "contact: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20&n=1"
Observe and diagnose: the contact request is still 403. The exception did nothing. A skip exception only skips rules that run after it, and Place at = Last put your exception below the rulesets, so they run and block first. Placing the skip last is backwards.

Fix. Edit the "Contact form exclusion" exception and change Place at from Last to First, then Deploy. Wait 15 to 30 seconds, then send the contact request a few times with unique markers and one control request to the still-protected secrets path:

for n in 1 2 3; do curl -s -o /dev/null -w "contact n=$n: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20&n=$n"; done
curl -s -o /dev/null -w "control secrets.txt: %{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify the repair: the three /contact/ requests now return 200 (the exception runs first and skips the managed rules), while the control /.git/secrets.txt stays 403. In Security > Analytics > Events the /contact/ requests show Action = Skip and the secrets path still shows Block. Lesson: with managed-rules exceptions, order is a security control, not a cosmetic setting. A skip must be placed before the rules it is meant to bypass, and it must be scoped tightly (here /contact/* only) so it never bleeds onto other paths, such as the API host you protect in Labs 12 and 13.

7.5 Triage the incident in Security Analytics

You have now generated blocks, a skip, and log events. Use Security > Analytics the way an analyst does during an incident. The Events tab groups firewall events by Action and by Service (for example, Managed rules) and lists the top source IPs, paths, user agents, and countries. The Traffic tab shows sampled request logs with Cloudflare's threat classifications.

Why: the core triage question is whether you are looking at one automated actor or a distributed campaign, because that decides whether you block an IP, a pattern, or a whole region. The dashboard is where you answer it.

With Group view by: Action, read the summary cards, then drill in to answer three questions: which service and which rules fired most, which paths were targeted, and whether a single source is responsible. Expand one Block event to view its Ray ID, User Agent, ASN, and matched rule.

Verify (Security > Analytics > Events): grouped by Action you see Block (the .git virtual patch and the blocked contact payload), Skip (your exception), and Log (the staged rulesets). Under Events by service > Managed rules the matched rules are listed by name, including Version Control - Information Disclosure, SQLi - Comment, and the Contact form exclusion skip. Because every test 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. That "one IP, one user agent, two paths" fingerprint is exactly how you distinguish a single automated actor from a distributed attack.

Lab 7 Wrap-up

MilestoneHow you confirmed it
Confirmed the Lab 1 attacks still landed/.git/secrets.txt and the /contact/ SQLi both returned 200 through Cloudflare
Rolled out the Managed Ruleset Log then Block/.git/secrets.txt stayed 200 and logged in Log, then returned 403 in Block
Staged OWASP Core as defense in depthruleset shows Log at threshold Medium (40), PL1
Reproduced a false positivea legitimate /contact/ submission returned 403 (SQLi - Comment)
Diagnosed and fixed the exception orderPlaced Last it stayed 403; Placed First it showed Action = Skip and 200, while .git stayed 403
Triaged the incidentEvents grouped by Action and Service, single source IP and user agent across two paths

Lab 7 Troubleshooting

SymptomLikely causeFix
/.git/secrets.txt returns 403 immediately, before you switch to BlockThe Managed Ruleset deployed with its default Block action instead of LogEdit the ruleset, set the action override to Log to observe first, then back to Block to enforce
/.git/secrets.txt still returns 200 after switching to BlockManaged Ruleset action still Log, not deployed, or change not propagatedConfirm the ruleset action is Block; wait 15-30s and retry
Exception does not stop the blockPlace at left on Last, so the rulesets run before itSet the exception to Place at > First and Deploy
Repeated test requests do not appear as new eventsCloudflare de-duplicates identical requestsAdd a unique marker (&n=1, &n=2) to each request
Contact form still blocked after the exceptionReordering not yet applied at the edgeWait 15-30 seconds, then re-send with a fresh marker
Lab 7 to Lab 8: Managed rules are the floor. They stop known-bad patterns, but AcmeCorp also has business-specific rules to enforce: geography, an admin lockdown, a trusted-partner bypass. In Lab 8 you write custom rules for exactly that.

Lab 8 - WAF Custom Rules

Managed rules are the floor, not the ceiling. Now you enforce AcmeCorp's own policy with custom rules: refuse a sanctioned country while quietly watching a competitor's region, add a challenge to the abused contact form, lock down the admin panel the attacker reached in Lab 1, and let one trusted partner API through untouched. Along the way you fall into the most common custom-rule trap, the Allow action that does not actually block anyone, and climb back out.
Estimated time: 55 to 70 minutes.
Why this lab exists: managed rules stop known-bad signatures, but they do not enforce AcmeCorp's own policy. You need sanctions geofencing, least-privilege access to the admin panel, an API method contract, and a trusted-partner bypass that a leaked header cannot abuse. Custom rules express all of that, and getting the logic and the rule interactions right is the job. In this lab you build that policy as a layered set of custom rules, step by step. Each step tells you exactly what expression to type and gives you a copy-pasteable command that verifies it.
What you'll build: a sanctioned-country block set and a logged competitor set, a Managed Challenge that re-closes the contact form and beats your own Lab 7 skip, an IP-list admin lockdown built the wrong way then fixed, and a three-rule least-privilege perimeter on the API, then a triage of the whole policy in Analytics.
Prerequisites: Lab 7 complete (WAF deployed, and the /contact/* skip exception in place). Dashboard access to Security > Security rules, Manage account > Configurations > Lists, and Security > Analytics. Workstation Terminal for curl.

8.1 The threat brief and campaign baseline

Security hands you a brief: a single actor is probing AcmeCorp. They scrape the catalog, push SQL injection at the contact form, knock on /admin, and abuse the partner API. Managed rules from Lab 7 catch known-bad signatures, but they do not enforce AcmeCorp policy: geography, least-privilege admin access, and an API contract. You author that policy in this lab.

First, replay the Lab 1 campaign through Cloudflare and record what your current posture does. Run all four and note each code:

curl -s -o /dev/null -w "scrape:       %{http_code}\n" https://<student-domain>/services/luggages/
curl -s -o /dev/null -w "contact SQLi: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -w "admin probe:  %{http_code}\n" https://<student-domain>/admin
curl -s -o /dev/null -w "secrets file: %{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify the baseline: the scrape is 200 (nothing throttles it yet, that is Lab 11), the admin probe is 401 (origin Basic auth, still reachable), and the secrets file is 403 (your Lab 7 managed ruleset). The uncomfortable one: the contact SQLi is 200, not 403. Your own Lab 7 exception (the /contact/* skip placed first) reopened the injection path. Note it; you re-close it in 8.3. This is the before picture, and each objective below closes one gap.

8.2 Geofence sanctioned countries, quietly watch a competitor

Goal: refuse traffic from a set of sanctioned countries (regulatory compliance), and separately log, not block, a competitor's region for intelligence.

Go to Security > Security rules. In the Custom rules card click Create rule, name it "Geo Block - Sanctioned", and click Edit expression (top-right of the "When incoming requests match" box). Type this expression, which matches when the source country is in a set of sanctioned ISO codes. Set the action to Block and Deploy:

ip.src.country in {"KP" "IR" "SY" "CU"}
Why a set: in {..} keeps one rule readable as the sanctions list grows, instead of a chain of or clauses. The ip.src.country field is the two-letter ISO code of the visitor's IP.

Your turn (build it yourself): you just built one set-based geo rule step by step. Now author a similar one on your own. Create a rule named "Competitor Monitoring" that matches AcmeCorp's two competitor regions, China and Russia, but sets the action to Log instead of Block, so the match is recorded for intelligence without mitigating. Use the same ip.src.country in {..} pattern you just learned: click Edit expression, type your expression, set the action to Log, and Deploy.

Why Log: Log records matches in Security Analytics but does not mitigate, so evaluation continues to your other rules. AcmeCorp gets intelligence on the competitor's region without blocking it yet.
Check your work: your expression should read exactly ip.src.country in {"CN" "RU"} with the action set to Log. If you reached for eq joined by or instead of a set, it still works, but the set form is what keeps the rule readable as the list grows.

Prove the block from one location. Your workstation has a single source country, so you cannot originate traffic from a sanctioned country. Find your country, then confirm normal traffic is allowed:

curl -s https://<student-domain>/cdn-cgi/trace | grep '^loc='
curl -s -o /dev/null -w "control /: %{http_code}\n" https://<student-domain>/
Verify (allowed): loc= shows your two-letter country (call it <your-cc>), which is not in the sanctioned set, and / returns 200.

To prove the Block action fires, edit "Geo Block - Sanctioned", temporarily add your own country to the set (for example ip.src.country in {"KP" "IR" "SY" "CU" "<your-cc>"}), Save, wait about a minute, and re-run the control:

curl -s -o /dev/null -w "control /: %{http_code}\n" https://<student-domain>/
Verify (blocked): the control now returns 403 and the Cloudflare block page. Remove your country from the set and Save; it returns 200 again. You have proven the geofence works and kept your own access. Edge propagation can take up to a minute, so wait and re-run if you still see the old result.

Finally, review the results at Security > Analytics > Events.

Verify (events): with Group view by: Action you will see a Block event from your temporary Geo Block test. Expand an event to confirm the matched rule name, the country, and the action.

8.3 Re-close the contact form your own exception reopened

In 8.1 you saw the uncomfortable result: the contact-form SQLi returned 200, not 403. Your Lab 7 exception (the /contact/* skip, placed first) told the managed ruleset to leave that path alone, and the injection walked straight through. You will not remove that exception, the contact form still needs it for legitimate submissions, so instead you add a custom rule in a different phase that closes the gap.

Go to Security > Security rules. In the Custom rules card click Create rule. Name it "Challenge Contact Form", click Edit expression, and type this expression, which matches the contact path exactly. Under Then take action choose Managed Challenge, and Deploy:

http.request.uri.path eq "/contact/"
Why this beats your own skip: the Lab 7 exception only skips the managed ruleset. Custom rules run in a separate, earlier phase, so a Managed Challenge here fires no matter what the managed ruleset is told to skip. A real browser solves the interstitial silently and receives a cf_clearance cookie; an automated client cannot, so scripted injection and form-spam are filtered without blocking customers.
Verify (re-run the 8.1 probe): the contact SQLi that returned 200 in 8.1 now returns 403 with a challenge:
curl -s -o /dev/null -w "contact SQLi: %{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -D - -o /dev/null "https://<student-domain>/contact/" | grep -i "cf-mitigated"
You get 403 and cf-mitigated: challenge. You closed the gap your own exception opened, from a different phase, without touching the Lab 7 rule.

8.4 Lock down the admin panel, the wrong way then the right way

The attacker reached /admin in Lab 1. It is protected by origin Basic auth (that is the 401 you keep seeing), but AcmeCorp policy says only corporate egress IPs should even reach it. You will build that lockdown, and on the way you will fall into the single most common custom-rule mistake and climb back out.

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.

Why a list: Lists let you manage sets of IP addresses centrally and reference them in any rule as $corporate_ips, without rewriting the rule when membership changes.
How to read the results: the origin protects /admin with HTTP Basic auth, so any request that reaches the origin returns 401. A request blocked by a custom rule returns 403 with the Cloudflare block page. So 401 means "the WAF let it through" and 403 means "the WAF blocked it".

Build it the wrong way first, on purpose. The intuitive design is "Allow the corporate list into /admin". Return to Security > Security rules, create a custom rule named "Admin Allow Corp (broken)", click Edit expression, type the expression below, set the action to Allow, and Deploy:

(http.request.uri.path eq "/admin") and (ip.src in $corporate_ips)

Observe the trap. Simulate an outsider by removing your IP from corporate_ips (Manage account > Configurations > Lists > corporate_ips, delete your entry), wait about 15 seconds, then probe /admin from the workstation:

curl -s -o /dev/null -w "admin (off-list): %{http_code}\n" https://<student-domain>/admin
Verify (the trap): you still get 401, not 403. The outsider was NOT blocked. The custom-rule Allow action only skips the remaining custom rules; it never blocks anyone by itself. With just an Allow rule, every off-list request sails past to the origin. Your "lockdown" protected nothing.

Fix it. Edit the rule (rename it "Allow Corp Admin Only" if you like), click Edit expression, and invert the logic to block /admin for anyone not on the list. Change the action to Block and Deploy:

(http.request.uri.path eq "/admin") and not (ip.src in $corporate_ips)
Why "Block when NOT in the list" is the correct pattern: a positive security model needs an explicit deny for everyone outside the trusted set. Block is a terminating mitigation; Allow is not. "Block when not in list" denies outsiders and leaves insiders untouched.

Confirm both sides. With your IP still off the list:

curl -s -o /dev/null -w "admin (off-list): %{http_code}\n" https://<student-domain>/admin
Verify (off-list blocked): now you get 403 and the Cloudflare block page. Re-add your workstation IP to corporate_ips, wait about 15 seconds, and re-run: it returns 401 again (reached the origin's Basic-auth challenge). Off-list 403, on-list 401: the lockdown works and you kept your own access.

8.5 A least-privilege perimeter on the partner API

The partner API accepts orders, but the Lab 1 recon showed the origin answering more HTTP methods than the contract needs. AcmeCorp policy: the public API surface should accept only the methods it actually uses, and a trusted partner should be exempt from the managed ruleset but never from the method contract. You will build two rules and then prove how they interact.

Rule 1, the method allowlist (build this one step by step). Go to Security > Security rules, create a custom rule named "API Method Allowlist", click Edit expression, and type the expression below. It matches partner-API requests whose method is not one of the two the contract allows. Set the action to Block and Deploy:

(http.host eq "public-api.<student-domain>") and not (http.request.method in {"GET" "POST"})
Why a method allowlist: the API only reads (GET) and creates (POST) orders. Blocking everything else (DELETE, PUT, PATCH, TRACE) shrinks the attack surface to exactly the contract: a positive security model at the method layer.

Prove it from the workstation:

D=<student-domain>
curl -s -o /dev/null -w "GET orders:    %{http_code}\n" "https://public-api.$D/v1/orders"
curl -s -o /dev/null -w "DELETE orders: %{http_code}\n" -X DELETE "https://public-api.$D/v1/orders"
Verify: GET returns 200 (an allowed method, reaches the origin) and DELETE returns 403 (blocked by your method allowlist at the edge, it never reaches the origin).

Rule 2, the trusted-partner bypass (your turn, build it yourself). You just built one scoped custom rule end to end. Now author a similar one on your own to a new objective. A trusted B2B partner's automated calls trip the managed ruleset and generate false positives, so you want to Skip the managed rules for them, but only when you are confident it really is the partner. Require two factors: the request must carry the header x-partner-api: true and originate from an IP on the corporate_ips list you built in 8.4. Create a rule named "Skip Managed for Partner API", scope it to the partner host, action Skip with All managed rules ticked, and place it above the method allowlist. Remember: header names are lowercased in expressions, and http.request.headers[...] returns an array, so wrap the header test in any(...[*] eq "true").

Check your work: your expression should read exactly:
(http.host eq "public-api.<student-domain>") and any(http.request.headers["x-partner-api"][*] eq "true") and (ip.src in $corporate_ips)
with action Skip -> All managed rules. The two-factor test (header AND corporate IP) means a leaked header alone cannot buy a bypass from an untrusted network.

Now prove how Rule 1 and Rule 2 interact. This is the teaching point of the lab. Send the partner's credentials three ways from the workstation (your workstation IP is on corporate_ips, so it satisfies the second factor):

D=<student-domain>
P="q=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -w "partner GET (SQLi):   %{http_code}\n" -H "x-partner-api: true" "https://public-api.$D/v1/orders?$P"
curl -s -o /dev/null -w "partner DELETE:       %{http_code}\n" -H "x-partner-api: true" -X DELETE "https://public-api.$D/v1/orders"
curl -s -o /dev/null -w "no-header GET (SQLi): %{http_code}\n" "https://public-api.$D/v1/orders?$P"
Verify (the interaction):
  • partner GET with the SQLi payload -> 200: the Skip bypassed the managed ruleset for the trusted partner, and GET is an allowed method, so it reaches the origin.
  • partner DELETE -> 403: this is the lesson. Skip only bypassed the managed rules; it did NOT skip your custom method allowlist. The partner is exempt from signatures but still bound by the method contract.
  • no-header GET with the SQLi payload -> 403: no bypass, so the managed ruleset blocks the injection as in Lab 7.
Skip scope is surgical: "All managed rules" does not mean "all security". Your least-privilege method contract still governs even your most trusted partner.

8.6 Re-run the campaign and triage in Analytics

Your custom policy is live. Replay the Lab 1 and 8.1 campaign one more time to see how the posture changed, then read the story in Analytics.

D=<student-domain>
curl -s -o /dev/null -w "scrape:       %{http_code}\n" "https://$D/services/luggages/"
curl -s -o /dev/null -w "contact SQLi: %{http_code}\n" "https://$D/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -w "admin probe:  %{http_code}\n" "https://$D/admin"
curl -s -o /dev/null -w "secrets file: %{http_code}\n" "https://$D/.git/secrets.txt"
curl -s -o /dev/null -w "API DELETE:   %{http_code}\n" -X DELETE "https://public-api.$D/v1/orders"
Verify (posture shift from 8.1):
  • scrape -> 200 (still open; volumetric abuse is Lab 11's job).
  • contact SQLi -> 403 (challenge), where 8.1 showed 200: closed by your 8.3 rule.
  • admin probe -> 401 on-list, or 403 off-list: governed by your 8.4 lockdown.
  • secrets file -> 403: still your Lab 7 managed ruleset.
  • API DELETE -> 403: blocked by your 8.5 method allowlist.
Four of the five campaign probes now hit a policy you authored. Only the scrape is still open, by design.

Read the whole policy at once in Security > Analytics > Events. Set Group view by: Action to see Block, Managed Challenge, and Skip side by side, then switch Group view by to Rule to attribute each mitigation to the custom rule that produced it.

Verify (triage): you can see, per rule, which of your custom rules fired and how often: the geo test Block, the contact-form challenge, the admin Block, the partner Skip, and the API method Block. Expand any event to confirm the matched rule name, the action, and the request that triggered it.

Lab 8 Wrap-up

MilestoneHow you confirmed it
Recorded the campaign baselinethe four 8.1 probes showed scrape 200, contact SQLi 200, admin 401, secrets 403
Geofenced sanctioned countries, logged a competitora temporary self-country expression flipped / to 403, then back to 200; the competitor set logs without blocking
Re-closed the contact form your exception reopenedthe 8.1 contact SQLi went from 200 to 403 with cf-mitigated: challenge
Learned the Allow trap, then locked /admin correctlythe Allow rule left an off-list request at 401; "Block when not in list" made it 403, on-list stayed 401
Built a least-privilege API method contractGET returned 200, DELETE returned 403 at the edge
Proved Skip scope with a two-factor partner bypasspartner GET passed 200, but partner DELETE was still 403 under the method contract

Lab 8 Troubleshooting

SymptomLikely causeFix
Geo block returns 200 during your testThe expression matches other countries, not yoursTemporarily set ip.src.country eq "<your-cc>", test, then restore the original set
Contact SQLi still returns 200 after 8.3The challenge rule path does not match, or the edge has not propagatedConfirm the expression is http.request.uri.path eq "/contact/" and wait about a minute
Off-list /admin still returns 401You are still on the Allow-only design, which blocks no oneSwitch the action to Block with not (ip.src in $corporate_ips)
/admin returns 403 from your own workstationYour public IP is not on corporate_ipsAdd your api.ipify.org IP to the list, wait about 15 seconds, retry
Partner GET still hits managed rulesSkip rule sits below the block, or a factor is missingPlace the Skip rule above the allowlist; confirm the header and that your IP is in corporate_ips
Partner DELETE unexpectedly returns 200Skip is set to skip custom rules too, not just managedIn the Skip action tick only All managed rules, not remaining custom rules
Lab 8 to Lab 9: Your rules now enforce policy by geography, IP, and headers. But the pricing scrapers from Lab 1 do not announce themselves with a header or a country you can block. In Lab 9 you use Cloudflare's Bot Management score to separate automated traffic from real customers.

Lab 9 - Bot Management

Competitors are scraping AcmeCorp's pricing with automated bots. You challenge low-scoring automated traffic while explicitly allowing known-good internal tooling.
Estimated time: 35 to 45 minutes.
Why this lab exists: the pricing scrapers from Lab 1 do not carry a header or a country you can block, so the Lab 8 tools miss them. Cloudflare's machine-learning bot score rates every request from 1 (automated) to 99 (human), letting you challenge scrapers while explicitly allowing known-good internal tooling.
What you'll build: a scoped baseline challenge rule (built step by step), a tuning pass that you author yourself to carve out two trusted low-scoring clients, and a controlled over-broad-rule incident where you predict the blast radius, watch it hit the whole zone, and fix it.
How this lab works: the first rule is spelled out, expression and all. The tuning rule is yours to write from an objective, with a "Check your work" you can compare against. Twice you will predict a result before you run the command, then confirm it.
Prerequisites: Lab 7 complete (you can create Security rules). Enterprise Bot Management (Always active). Workstation Terminal for curl.

9.1 Bot analytics and the baseline challenge 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.

Why: Cloudflare's ML scores every request from 1 (almost certainly automated) to 99 (almost certainly human) and exposes it as 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.

Why: real scrapers cluster at a low bot score and often share a user agent or ASN. Those patterns tell you where to set your threshold and which traffic to exclude later. A threshold of lt 30 catches the clearly-automated band while leaving the ambiguous middle (roughly 30 to 60) alone.

Create the rule: Security > Security rules > Custom rules > Create rule. Name it "Bot Management - Baseline", click Edit expression, and paste the expression below. It fires on low-scoring, non-verified traffic to the bot-demo path only. 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"
Why: Starting in Log lets you confirm the rule matches the right traffic before challenging anyone. 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.

Predict before you flip it. Once the Log events look right, edit the rule, change the action to Managed Challenge, and Deploy. Before running anything, predict: a scripted curl from a single egress IP scores low and is not a verified bot, so when it requests /hellobots, what status code and what mitigation header should it get? Note your answer, then test:

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"
Verify: the request returns 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). If you predicted 403 plus a challenge mitigation, your mental model of score plus action is right. You will also see the rule firing in Security > Analytics > Events.

9.2 Tune out trusted automation (your turn)

The baseline rule now challenges everything low-scoring on /hellobots, and that includes traffic you trust. Two legitimate low-scoring clients must keep working: a nightly internal BI tool that identifies itself with the user agent B1-Bot/1.11, and a trusted catalog-sync job that hits /services/luggages/. Both currently score low and would be challenged.

Your turn (build it yourself): you saw the baseline expression built step by step. Now extend it on your own. Edit "Bot Management - Baseline", click Edit expression, and add two and not (...) clauses so the rule still catches low-scoring bots but carves out the two trusted clients: exclude the exact path /services/luggages/ and the exact user agent B1-Bot/1.11. Keep the score and verified-bot logic. Leave the action on Managed Challenge and Deploy.

Why: the two 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.
Check your work: your expression should read exactly:
(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")
Field names are lowercase, and each exclusion is its own parenthesized and not (...) clause.

With the action on Managed Challenge, test all three cases from the workstation:

D=<student-domain>
curl -s -L -o /dev/null -w "hellobots: %{http_code}\n" "https://$D/hellobots"
curl -s -L -o /dev/null -w "luggages:  %{http_code}\n" "https://$D/services/luggages/"
curl -s -L -o /dev/null -w "B1-Bot UA: %{http_code}\n" -A "B1-Bot/1.11" "https://$D/hellobots"
Verify: the /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. (The -L flag follows the origin's trailing-slash redirect on /hellobots so a passed-through request reports 200; a challenged request has no redirect to follow and stays 403.)

9.3 The over-broad rule: predict the blast radius, then fix it

Look closely at the expression you just wrote. In carving out the two trusted clients, the path scope http.request.uri.path contains "/hellobots" is gone. The rule no longer targets the bot-demo path; it now evaluates every request on the zone.

Predict first. Your workstation curl scores as automated and is not a verified bot, and the homepage / is neither /services/luggages/ nor the B1-Bot agent. Predict what the homepage returns now, then check it:

curl -s -D - -o /dev/null "https://$D/" | grep -iE "HTTP/|cf-mitigated"
Verify (the collateral damage): the homepage returns HTTP/2 403 with cf-mitigated: challenge. You never targeted the homepage, but your single-egress curl scores as automated and the un-scoped rule challenges it anyway. Every low-score client is now challenged across the whole zone: your own uptime monitors, partner integrations, and the curl tests in the labs that follow. This is the classic over-broad security rule. It looks perfect in the demo and quietly breaks legitimate automation everywhere else.

Fix it. Edit the "Bot Management - Baseline" rule, change the action from Managed Challenge to Log, and Deploy. Re-run the homepage check:

curl -s -D - -o /dev/null "https://$D/" | grep -iE "HTTP/|cf-mitigated"
Verify (restored): the homepage is back to HTTP/2 200 and the cf-mitigated header is gone. In Log the rule still records every low-score request under Security > Analytics > Events for tuning, but it no longer challenges anyone, so the rest of the labs are testable from the workstation.
Why Log, not Managed Challenge, for the rest of the guide: a Managed Challenge is a terminating action that runs in the custom-rules phase, before rate limiting. If you leave this un-scoped rule on Managed Challenge, your curl traffic in Labs 11 to 14 is challenged and never reaches the rate limiter or the page you are testing. In production you would instead scope the challenge narrowly (by path, ASN, or user agent) rather than leaving it zone-wide; here we set it to Log so it keeps observing without blocking. You could also simply restore the original /hellobots scope; either way the events keep flowing for the later labs.

Lab 9 Wrap-up

MilestoneHow you confirmed it
Confirmed Bot Management is activeBot management: Always active in Security > Settings
Baseline challenge on the bot pathpredicted, then confirmed /hellobots -> 403 with cf-mitigated: challenge
Authored your own exclusion pass/services/luggages/ and the B1-Bot/1.11 agent passed with 200 while /hellobots stayed 403
Predicted and saw the over-broad blast radiusthe homepage was wrongly challenged (403), then returned to 200 after switching to Log

Lab 9 Troubleshooting

SymptomLikely causeFix
cf.bot_management.score has no valueBot Management not active on the zoneConfirm Always active under Security > Settings > Bot traffic
Your tuned rule still challenges luggages or B1-BotAn exclusion clause is mistyped or uses the wrong operatorMatch exactly not (http.request.uri.path eq "/services/luggages/") and not (http.user_agent eq "B1-Bot/1.11")
A passed-through /hellobots shows 403, not 200Missing -L, so the trailing-slash redirect is not followedAdd -L to the curl so it follows the redirect
Every request across the zone is challengedThe rule lost its path scope and now matches all trafficRe-scope it, or set the action to Log as the lab directs
Later labs' curl tests all failThe bot rule was left on Managed Challenge zone-wideSet the baseline rule to Log before continuing
Lab 9 to Lab 10: You now separate bots from humans by behavior. Some clients, though, are known bad before they send a single byte. In Lab 10 you block them with zero maintenance using Cloudflare's managed threat-intelligence lists.

Lab 10 - Threat Intel with Managed Lists

Some clients are known bad before they ever reach you. You preemptively block anonymizers and open proxies using Cloudflare's continuously updated managed threat-intelligence lists, with zero maintenance.
Estimated time: 25 to 30 minutes.
Why this lab exists: the Lab 9 bot score reacts to behavior, but some sources are known bad before they send a single byte: anonymizers, open proxies, botnet command-and-control. Cloudflare maintains and continuously updates threat-intelligence lists of these networks, so AcmeCorp can block them preemptively with zero maintenance and cut scanning noise to the origin.
What you'll build: a two-list Block rule for anonymizing networks (built step by step), a higher-confidence Block rule that you author yourself for command-and-control and malware networks, and a reasoning pass on action tiers and how a managed-list rule interacts with the trusted-partner Skip you built in Lab 8.
How this lab works: the list contents are real-world malicious IPs, so you cannot originate a matching request from the lab workstation. You verify by confirming rule state and that your own traffic is untouched, and by reasoning precisely about rule interactions. One rule is spelled out; one is yours to build from an objective.
Prerequisites: Lab 8 complete (the partner Skip rule exists). Enterprise plan (managed lists available). Dashboard access to Security > Security rules. Workstation Terminal for curl.

10.1 Block open proxies and anonymizers

Go to Security > Security rules > Custom rules and click Create rule. Name it "Block Malicious IPs by Managed List". Build the match with the visual expression builder:

The Expression Preview should read exactly. Set the action to Block, set Status = Active, and click Deploy:

(ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)
Why: Cloudflare curates and continuously updates lists of actively malicious and anonymizing IP addresses so you do not build or maintain blocklists by hand. The Anonymizers list already includes Tor exit nodes, so there is no separate "Tor" list. This gives a large, zero-maintenance reduction in scanning noise hitting the origin.
Available managed lists on this zone: Anonymizers, Botnets / Command and Control Servers, Malware, Open Proxies, and VPNs. They appear in the Value dropdown once you choose is in list. If a list is missing on another zone, it is not enabled for that zone's plan.

Confirm the rule is live and that it does not touch your own traffic. From the workstation:

curl -s -o /dev/null -w "control /: %{http_code}\n" https://<student-domain>/
Verify: the rule appears in the Custom rules list with the exact expression above, action Block, status Active, and the control request returns 200. Because the list contents are real-world malicious IPs, you will not match from the lab workstation; the deployed rule is what enforces the block for any request that originates from an open proxy or anonymizer.

10.2 Add a high-confidence block tier (your turn)

Two more managed lists describe networks that are outright malicious rather than merely anonymizing: Botnets / Command and Control Servers and Malware. Traffic from these should be blocked with even higher confidence than a proxy.

Your turn (build it yourself): you just built one managed-list Block rule with the visual builder. Now author a second one on your own, named "Block C2 and Malware Networks". Use the same builder steps: Field = IP Source Address, Operator = is in list, choose the Botnets / Command and Control Servers list, click Or, then add a second is in list condition for the Malware list. Set the action to Block, Status = Active, and Deploy.

Check your work: the Expression Preview shows two ip.src in $cf.<list> clauses joined by or, one referencing the Botnets / Command and Control list and one referencing Malware, with action Block and status Active. The exact list tokens are filled in by the builder when you pick each Value, so do not type them by hand.
Why a second rule instead of one giant clause: keeping the anonymizing lists (10.1) and the outright-malicious lists (this rule) as two rules lets you tier actions and read Analytics per rule. Note which list you did not block: VPNs. A VPN is a legitimate tool for remote workers, so blocking it outright would create false positives; if you wanted VPN visibility you would add a Log rule, never a Block.

10.3 Predict how this interacts with your partner bypass

In Lab 8 you built a partner rule with the Skip -> All managed rules action for trusted B2B traffic. Suppose one of that partner's calls arrives from an IP that happens to be on the Open Proxies list.

Predict: does the partner Skip let that request through, or does your 10.1 managed-list rule still block it? Decide before reading on.

Answer: it is still blocked. The Skip action you chose skips managed rules; your managed-list rule is a custom rule, and "All managed rules" does not skip custom rules. This is the same scoping lesson your Lab 8 partner DELETE proved: a bypass is only as wide as the components you tick. Threat-intel blocking of proxies and command-and-control applies even to otherwise-trusted partners, which is exactly what you want.

Lab 10 Wrap-up

MilestoneHow you confirmed it
Blocked anonymizing networks by threat intelthe rule is Active with (ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer), control / stayed 200
Authored a high-confidence block tieryour Botnets/C2 + Malware rule is Active with two is in list clauses
Reasoned about action tiers and bypass scopeblocked proxies, C2, and malware, left VPNs alone, and confirmed the partner Skip does not exempt threat-list blocks

Lab 10 Troubleshooting

SymptomLikely causeFix
The managed list does not appear in ValueThe list is not enabled for the zone's planConfirm the zone is Enterprise; managed lists require the entitlement
Nothing is blocked from the workstationExpected: the workstation IP is not on a threat listNothing to fix; the rule enforces for real anonymizer/proxy source IPs
Expression preview differs from the labWrong list chosen or missing the Or conditionRebuild so it reads exactly (ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)
Your second rule would block legitimate VPN usersYou added the VPNs list to a Block ruleRemove VPNs from the Block rule; for visibility use a separate Log rule instead
Lab 10 to Lab 11: You have now filtered who may connect by reputation. Attackers who slip through can still pivot to sheer volume, hammering a page or fuzzing an endpoint. In Lab 11 you cap that abuse with rate limiting.

Lab 11 - Rate Limiting

Attackers pivot to volumetric abuse. You cap requests to a hot product page per IP, then throttle API-style fuzzing by counting the origin's own error responses.
Estimated time: 30 to 40 minutes.
Why this lab exists: attackers who cannot beat the WAF pivot to sheer volume: brute-forcing a login, hammering a hot page, or fuzzing an endpoint with junk queries. Rate limiting caps how many requests one client may make in a window, and it can even count the origin's own error responses to catch fuzzing that looks fine request by request.
What you'll build: a per-IP rate limit on a hot product page (built step by step), and a response-code rate limit that you author yourself to throttle search fuzzing by counting the origin's 404s.
How this lab works: you build the first rule with every setting spelled out and predict where the burst flips to 429 before you run it. The second rule is yours to configure from an objective, with a "Check your work" to compare against.
Prerequisites: Lab 9's baseline bot rule set to Log (so workstation curl reaches the rate limiter). Advanced rate limiting (Enterprise). Workstation Terminal for curl.

11.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/"
Why: A hot page being hammered by a single client is a classic abuse pattern. A rate limit caps how many requests one client can make in a window, so an automated client cannot pound the endpoint.

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.

Why: Counting per IP limits one abusive client without affecting everyone else. A legitimate visitor will not request the same page five times in sixty seconds; an automated script will.
Gotcha: the mitigation duration cannot be shorter than the counting period. With a 1-minute period the shortest valid duration is 1 minute. We use 1 minute so the lab resets quickly for re-testing.

Predict, then burst. The threshold is 5 requests per minute per IP, so predict: in a rapid loop of 8 requests from one IP, roughly which request number should first return 429? Note your answer, then run the burst from the workstation:

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
Verify: the first several requests return 200, then once the per-IP threshold is crossed (around request 6) the remaining requests return 429 (Too Many Requests). The counter is eventually consistent, so the exact request where 429 first appears can vary by one; if you predicted "the sixth, give or take one", your model is right. Wait a minute for the 1-minute block to clear before the next test.

11.2 Rate limit by origin response code (your turn)

Attackers fuzz search endpoints with junk queries to find hidden parameters. Those probes overwhelmingly generate 404 responses. A plain per-IP limit is crude here: you only want to throttle clients that keep generating errors, not real users who search successfully. Cloudflare can count the origin's response code instead of raw requests.

In the same Advanced rate limiting rules section, click Create rule and name it "Search Fuzzing Protection". Click Edit expression and match the search path:

http.request.uri.path eq "/services/search"

Your turn (configure it yourself): you built a per-IP limit in 11.1. This one is similar but adds one twist you now configure on your own. Tick Use custom counting expression, and in the Increment counter when box author the condition so the counter advances only when the origin responds with a not-found. Set the threshold to 10 requests per 1 minute, action Block with a 1 minute duration, and Deploy.

Why count the response, not the request: the counter only advances when the origin responds 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.
Check your work: the custom counting expression should read exactly http.response.code eq 404, with Requests = 10, Period = 1 minute, action Block, duration 1 minute. Sustained 404-generating traffic is almost always malicious, so a hard Block at the threshold is appropriate.

Test from the workstation. The nonexistent path returns 404, so every request feeds the counter:

D=<student-domain>
for i in $(seq 1 25); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/services/search?q=$i"; done
Verify: the first 10 requests return 404 (passed to the origin and counted), then the remaining requests (11 onward) 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.
Note: counters that increment on the origin response are eventually consistent, so a short burst that only just crosses the threshold may return all 404 with no 429 on the very first pass. The 25-request loop gives the counter time to catch up and trip mid-run; if you still see no 429, simply run the loop again.

Lab 11 Wrap-up

MilestoneHow you confirmed it
Capped a hot page per IPpredicted the flip point, then a burst on /services/safes/ went from 200 to 429 around request 6
Authored a response-code rate limityou configured http.response.code eq 404; the first 10 /services/search hits returned 404, then 429
Understood eventual consistencycounters may overshoot by one, so the flip point varies slightly

Lab 11 Troubleshooting

SymptomLikely causeFix
Deploy fails with a duration errorMitigation duration is shorter than the counting periodSet the duration to at least the period (1 minute for a 1-minute window)
Burst never returns 429Bot rule still challenging curl, or counter not yet consistentSet the Lab 9 bot rule to Log; re-run the loop to let the counter catch up
Response-code rule never tripsCustom counting expression not set to http.response.code eq 404Enable Use custom counting expression and set it in the Increment counter when box
429 persists too long between testsMitigation duration longer than you want for re-testingUse a 1-minute duration so the limit resets quickly
Lab 11 to Lab 12: The website perimeter is now layered and volumetric abuse is capped. The public orders API, though, is a different attack surface with its own rules. In Lab 12 you make that API visible so you can protect it.

Lab 12 - API Discovery & Endpoint Management

You cannot protect an API you cannot see. You configure a session identifier, generate traffic so API Shield can learn the real API surface, and bring those operations under management.
Estimated time: 25 to 35 minutes.
Why this lab exists: you cannot protect an API you cannot see. AcmeCorp's public orders API (public-api.<student-domain>) serves GET /v1/orders, GET /v1/orders/{id}, and POST /v1/orders. Before any schema, rate-limit, or auth control can attach, API Shield needs to attribute traffic to sessions, learn the real surface from live traffic, and hold those operations in an inventory.
What you'll build: a session-identifier header, generated API traffic across the real endpoints, one operation brought under management step by step, and the remaining operations that you add yourself.
How this lab works: the session identifier and the first managed operation are spelled out; adding the remaining operations is your turn. Discovery itself is time-gated (up to 24 hours), so you populate Endpoint Management manually now and revisit Discovery later.
Prerequisites: Lab 2 complete (public-api onboarded and proxied). Dashboard access to Security > Settings (API abuse) and Security > Web assets. Workstation Terminal for curl.

12.1 Session identifier and attributed traffic

Add the session identifier. Go to Security > Settings, open the API abuse category, and click into Session identifiers. This zone starts with no identifier configured, so click Add identifiers, set Type = Header and Name = session-id, and Save. The page then shows 1 / 10 identifiers added with the status "Identifier not seen in requests" until the traffic you generate below starts carrying the header.

Why: API Shield uses a per-client identifier (a header, cookie, or JWT claim unique per user) to attribute requests to a session. Without it, Discovery is weaker, and Sequence Analytics and per-endpoint rate-limit recommendations cannot be generated.

Predict, then confirm the surface is reachable. Before generating bulk traffic, predict what an authorized list call should return, then check one request carrying your header:

H=public-api.<student-domain>
curl -s -o /dev/null -w "GET /v1/orders: %{http_code}\n" -H "session-id: user-1" "https://$H/v1/orders"
Verify: GET /v1/orders returns 200. The API is reachable through Cloudflare and your session header is being sent.

Now generate representative API traffic from the workstation. Send a distinct session-id per simulated user across the API's real endpoints (list orders, read one order, create an order):

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/v1/orders"
  curl -s -o /dev/null -H "$S" "https://$H/v1/orders/0"
  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"
done
Why: Cloudflare maps the API surface from live traffic attributed to your session identifier. The more representative the traffic, the more complete the picture Discovery builds of which operations exist and how they are used.
Shadow endpoints (concept): In production, Discovery's headline value is surfacing undocumented "shadow" endpoints: paths that appear in live traffic but in no schema or documentation (often forgotten debug, admin, or internal-logging routes). This shared lab origin implements only the three orders endpoints above, so there is no hidden endpoint to surface here, and Discovery needs up to 24h to populate regardless. On a self-hosted origin you would also hit an undocumented path (for example an internal logs route), then watch it appear under Add operations > Select from Discovery flagged as a shadow endpoint.
Timing: Discovery (and the rate-limit recommendations below) build from accumulated traffic and can take up to 24 hours to populate. Run the traffic now and revisit the results the next day.

12.2 Bring the first operation under management

Discovered endpoints appear under Security > Web assets > Operations (Endpoint Management) via Add operations > Select from Discovery. Until Discovery populates, you add operations manually. Do the first one together, step by step. On Add operations, choose Add custom operations, and enter:

Click Save operations.

Why: Endpoint Management is the inventory of operations Cloudflare actively governs. Only operations listed here can carry a schema, a rate-limit recommendation, or an authentication check.
Verify: the POST /v1/orders operation appears in Endpoint Management with its method, hostname, and path.

12.3 Add the remaining operations (your turn)

The orders API has two more real operations you just generated traffic for: listing orders and reading a single order by id. Bring them under management too.

Your turn (build it yourself): using the same Add operations > Add custom operations flow you just used, add these two operations on the public-api.<student-domain> host: a GET on /v1/orders, and a GET on the single-order path /v1/orders/{id} (use the {id} path-variable placeholder so it matches any order id). Save.

Check your work: Endpoint Management now lists three operations on the partner-API host: POST /v1/orders (from 12.2), GET /v1/orders, and GET /v1/orders/{id}. Each carries its method, hostname, and path.
Why manage all three: only operations in this inventory can carry a schema, a rate-limit recommendation, or an authentication check. In Lab 13 you attach an OpenAPI schema to POST /v1/orders, which only works because the operation is under management here.
Revisit later: after about 24 hours, confirm your /v1/orders operations also appear under Add operations > Select from Discovery, and optionally accept a rate-limiting recommendation to govern them.

Lab 12 Wrap-up

MilestoneHow you confirmed it
Configured a session identifierSession identifiers shows 1 / 10 with the session-id header; a header-carrying GET /v1/orders returned 200
Generated attributed API trafficthe loop sent distinct session-id values across all three endpoints
Managed the first operation, then added the rest yourselfEndpoint Management lists POST /v1/orders, GET /v1/orders, and GET /v1/orders/{id}

Lab 12 Troubleshooting

SymptomLikely causeFix
Identifier stays "not seen in requests"Generated traffic did not carry the session-id headerRe-run the loop; each request must send -H "session-id: user-N"
Nothing under Select from DiscoveryDiscovery can take up to 24 hours to populateAdd the operations manually now via Add custom operations
The single-order operation matches nothing laterThe path was entered literally instead of with a variableUse the {id} path-variable placeholder: /v1/orders/{id}
API requests fail before Discoverypublic-api not proxied, or zone not on FullConfirm the Lab 2 record is proxied and SSL is at least Full
Lab 12 to Lab 13: The API's operations are now visible and managed. Visibility is not enforcement: nothing yet stops a malformed or malicious order body. In Lab 13 you enforce the API's OpenAPI contract at the edge.

Lab 13 - API Schema Validation

A managed API still trusts its callers too much. You onboard the orders API and enforce its OpenAPI contract at the edge so malformed order requests are rejected before they ever reach the origin.
Estimated time: 30 to 40 minutes.
Why this lab exists: an API under management still trusts its callers. The OpenAPI schema is a strict contract (which methods, paths, and body fields are valid); enforcing it at the edge drops malformed or malicious order bodies before they reach the origin, and a fallthrough block rejects any operation the schema does not define.
What you'll build: a proxied api record, an uploaded OpenAPI schema, a Block action on POST /v1/orders violations (built step by step), and a fallthrough block for undefined operations that you enable and prove yourself.
How this lab works: the schema upload and the body-validation Block are spelled out, and you predict each test result before you run it. Closing the undefined-operation gap is your turn, including the probe that proves it.
Prerequisites: Lab 12 complete (operations under management) and Lab 3 at Full. Dashboard access to Security > Web assets > Schema validation. Workstation Terminal for curl.

13.1 Enforce the OpenAPI contract

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).

Why: The orders API runs on a separate origin from the website. Creating a proxied 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>.

Why: Cloudflare derives the hostname for each imported operation from the schema's 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.

Why: The schema is a strict contract: it defines exactly which methods, paths, and body fields (types, required fields, and whether extra fields are allowed) the API accepts. With Block, any request that violates it is dropped at the edge before it reaches the origin.

Predict, then test. You will send one compliant request and two violations. Before running, predict each result and the reason: which one reaches the origin, which are stopped at the edge, and whether a stopped request shows Cloudflare's 403 or the origin's own 400. The schema requires each entry in items to be a UUID, so a ready-to-use UUID is provided below (no need to find your own):

H=api.<student-domain>
CT="Content-Type: application/json"
UUID=5bd195af-f22a-4cf7-ae9b-116d47104fbc
curl -s -o /dev/null -w "compliant:      %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"$UUID\"]}" "https://$H/v1/orders"
curl -s -o /dev/null -w "bad item type:  %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"sku1\"]}" "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"
Verify: the compliant request (a valid UUID in items) returns 200 (it reaches the origin and creates the order); both 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. The bad item type request is blocked because the schema types each items entry as a UUID (format: uuid) and Cloudflare enforces it; the missing items request is blocked because items is a required field.
Why the compliant request even reaches the origin: that 200 depends on the zone running Full (set in Lab 3.2). Full lets Cloudflare accept the orders origin's self-signed certificate and forward the valid request to it. Had you selected Full (Strict) on this shared origin, Cloudflare would reject the self-signed certificate and return 526 before the request reached the API, so the compliant call would show 526 instead of 200. In the student-provisioned (self-provisioned) edition the origin has an installed Cloudflare Origin CA certificate, so the zone runs Full (Strict) and the compliant request still returns 200.
Timing: schema validation takes about a minute to propagate to the edge after you click Set action. If you run the test immediately the violations may slip through to the origin instead (returning the origin's own 400 rather than Cloudflare's 403). That means the rule has not propagated yet, not that you misconfigured it: wait about a minute and re-run, and the violations flip to 403.

13.2 Close the undefined-operation gap (your turn)

Body validation only checks operations the schema defines. An attacker can still probe operations that are not in the schema at all: an undefined path, or an unexpected method on a known path. A fallthrough block rejects any request to the API host that does not match a known operation.

Your turn (do it yourself): in Schema validation, use the Fallthrough template / setting to Block requests to the API host that do not match any known operation. Then prove it yourself. Predict what a request to an operation the schema does not define should return, then probe one undefined operation from the workstation:

H=api.<student-domain>
curl -s -o /dev/null -w "undefined op: %{http_code}\n" "https://$H/v1/refunds"
Check your work: the undefined operation returns 403: the fallthrough dropped it at the edge because /v1/refunds is not a defined operation. Your three real operations (GET and POST on /v1/orders, and GET /v1/orders/{id}) still work, because they match the schema. Give the setting about a minute to propagate if the probe still reaches the origin.
Why: Schema validation only checks operations it knows about. A fallthrough block rejects requests to undefined operations, closing the gap where an attacker probes paths that are not in the schema at all. Combined with the body-validation Block in 13.1, the API now enforces both "the fields must be right" and "the operation must exist".

Lab 13 Wrap-up

MilestoneHow you confirmed it
Onboarded and enforced the schemapredicted, then confirmed a compliant POST /v1/orders returned 200
Blocked contract violations at the edgebad item type and missing items both returned 403 (not the origin's 400)
Closed the undefined-operation gap yourselfyou enabled the fallthrough block and a probe to /v1/refunds returned 403

Lab 13 Troubleshooting

SymptomLikely causeFix
Violations return 400, not 403Schema action not yet propagated to the edgeWait about a minute after Set action and re-run
Compliant request returns 526Zone on Full (Strict) against a self-signed origin certConfirm SSL is Full (Lab 3.2), not Full (Strict), on this shared origin
Uploaded operations never match trafficSchema servers URL points at a placeholder hostGenerate the schema with servers = https://api.<student-domain>
No validation fires at allapi record not proxiedSet the api A record to Proxied (orange cloud)
Undefined-operation probe still reaches the originFallthrough not set to Block, or not yet propagatedSet the Fallthrough action to Block, wait about a minute, re-run
Lab 13 to Lab 14: The API now rejects anything off-contract. One blind spot remains that no server-side control can see: code running in the customer's own browser. In Lab 14 you close it with Page Shield.

Lab 14 - Page Shield: Client-Side Protection

The last blind spot is the customer's own browser. You turn on client-side monitoring to inventory every script and connection, spot a malicious script, then use a Content Security Policy allowlist to close the supply-chain gap.
Estimated time: 30 to 40 minutes.
Why this lab exists: the AcmeCorp site runs JavaScript in customers' browsers, where the WAF never sees it. A supply-chain attack (a compromised third-party script or an injected skimmer) executes entirely client-side. Page Shield gives visibility into every script, connection, and cookie in the browser and lets AcmeCorp enforce an allowlist with a Content Security Policy.
What you'll build: Continuous script monitoring, a review of the live client-side inventory (including a flagged malicious sample), a report-only CSP allowlist built step by step, and the promotion to enforcement that you perform and prove yourself.
How this lab works: the allowlist is spelled out and you predict the report-only header before you check it. Promoting from report-only to enforcing is your turn, including predicting how the response header changes.
Prerequisites: Lab 2 complete (ps host onboarded). Dashboard access to Security > Settings (Client side abuse) and Security > Web assets. Workstation browser and Terminal. This lab uses the demo page ps.<student-domain>/contact/received/, which loads several third-party resources including a malicious sample.

14.1 Client-side Resource Monitoring

Enable monitoring. Go to Security > Settings, open the Client side abuse category, and turn on Continuous script monitoring.

Why: This instructs Cloudflare to detect and track the JavaScript, connections, and cookies running on your site so you can spot malicious or unexpected changes. On this Enterprise zone it is available at no additional cost.

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.

Why: this is your live inventory of what actually runs in visitors' browsers. From here you can see each script's URL, where it loads from, and when it first and last appeared. Cloudflare flags known-malicious scripts.
Verify: after the page has been loaded and traffic accumulates, the Scripts tab lists the demo page's third-party scripts (for example 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).
Timing: the inventory is built from real browser traffic seen repeatedly over the previous days, so it is not instant. Load the demo page a few times and revisit the tab later.

14.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".

Why: a content security rule builds a Content Security Policy (CSP) allowlist. Cloudflare injects the CSP header on matching responses, and any resource the browser tries to load that is not on the allowlist is reported or blocked. This is the direct defense against a script that suddenly starts talking to an attacker's domain.

Scope the rule to your site host, and build the allowlist from the legitimate resources you saw in 14.1 (leaving the malicious ones off, so they become violations):

Set Action = Log to start in report-only mode, then Deploy. Report-only lets browsers report violations without blocking, so you can confirm the allowlist does not catch anything legitimate before you enforce it.

Why start in report-only: the allowlist permits the known-good first-party and third-party resources, while anything outside it violates the policy. With this allowlist the injected cryptomining.testcategory.com script and the hookb.in / cf-malicious-test.domain.example.com connections fall outside and are reported as violations. Report-only proves the allowlist is correct before you turn on blocking.

Predict, then confirm. Predict which CSP header a report-only policy emits, then check the response from the workstation:

curl -s -D - -o /dev/null https://ps.<student-domain>/contact/received/ | grep -i content-security
Verify: the response includes a content-security-policy-report-only header listing your allowed script-src and connect-src sources. Report-only means browsers report off-list resources as violations but still load them; nothing is blocked yet.

14.3 Enforce the allowlist (your turn)

Report-only has shown the allowlist is correct. Now enforce it so off-list resources are actually blocked in the browser, not just reported.

Your turn (do it yourself): edit "Page Shield - Script Allowlist" and change the action from Log to Allow, which switches the policy from report-only to enforcing, then Deploy. Predict how the response header should change, then re-run the same check:

curl -s -D - -o /dev/null https://ps.<student-domain>/contact/received/ | grep -i content-security
Check your work: the header name flips from content-security-policy-report-only to the enforcing content-security-policy, with the same allowed sources. In a real browser the off-list cryptomining.testcategory.com script and the hookb.in connection are now blocked, not just reported. Allow the edge about a minute to propagate if you still see the report-only header.
Why the two-step rollout matters: shipping an enforcing CSP straight to production can break a legitimate resource you forgot to allowlist and take down page functionality. Report-only first, then enforce, is the safe supply-chain rollout: observe, confirm clean, then block.

Lab 14 Wrap-up

MilestoneHow you confirmed it
Enabled client-side monitoringthe Scripts and Connections tabs inventory the demo page's resources
Surfaced a malicious scriptcryptomining.testcategory.com/1.js is listed and flagged
Shipped report-only, then enforced it yourselfthe header went from content-security-policy-report-only (Log) to content-security-policy (Allow)

Lab 14 Troubleshooting

SymptomLikely causeFix
Inventory is emptyThe demo page has not been loaded enough, or data is still accumulatingLoad ps.<student-domain>/contact/received/ a few times and revisit later
No CSP header in the responseRule not deployed, or scoped to the wrong hostConfirm the content security rule is deployed and scoped to the ps host
Legitimate script reported as a violationIts source is missing from the allowlistAdd the source to Script sources, keep the rule on Log until clean
Header stays report-only after enforcingAction still on Log, or the edge has not propagatedSet the action to Allow, wait about a minute, re-check
Lab 14 to Lab 15: Every layer of the perimeter is now in place, from DNS to the browser. But all of it can still be bypassed if an attacker talks to the origin IP directly. In Lab 15 you seal the origin and replay the Lab 1 attacks to prove the perimeter holds.

Final lab: seal the perimeter, then replay the opening attacks to prove it holds.

Lab 15 - Seal & Validate the Perimeter

A perimeter only matters if it cannot be bypassed. You seal the origin so it accepts traffic only from Cloudflare, then replay the attacks from Chapter 1 and watch each one fail. You cannot change the shared lab origin itself, so you simulate the seal at your workstation to experience the effect, then confirm the edge perimeter holds.
Estimated time: 30 to 40 minutes.
Why this lab exists: every control you built in Labs 2 to 14 runs at the Cloudflare edge. If a request can still reach the origin IP directly, as you proved it could in Lab 1, none of those controls apply and the whole perimeter is bypassable. Sealing the origin so it accepts only Cloudflare traffic is what turns a set of edge features into an enforceable perimeter.
What you'll build: a simulated origin seal on the web origin (step by step) and on the API origin (your turn), a predict-then-verify replay of the Lab 1 attacks that proves each gap is now closed, and an architecture recap of the whole guide.
How this lab works: you seal the first origin IP with the command spelled out, then seal the second one yourself using the same technique. In the replay you predict each attack's new outcome before you run it.
Prerequisites: Labs 1 to 14 complete. Workstation Terminal with sudo/iptables.
Real seal vs simulated seal: a true seal is an origin-side or network-side change (see 15.1), and in this managed lab the origin is a shared machine you do not control, so you cannot install a certificate on it or change its firewall. Instead you simulate the seal from your own workstation by blocking the origin IP locally. That reproduces exactly what an attacker at your workstation experiences once the origin is sealed. It is a teaching stand-in, not a real lock: the origin is still reachable from elsewhere, only your workstation is blocked. On a self-hosted origin you would enforce 15.1 at the origin so it is blocked for everyone.

15.1 Seal the Origin (Simulated)

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. On a real origin there are two standard ways:

Why: every control in Labs 7 to 14 runs at the Cloudflare edge. If a request can reach the origin without passing through the edge, none of those controls apply. Sealing the origin is what turns a set of edge features into an enforceable perimeter.

You cannot run either method against the shared lab origin, so simulate the seal from the workstation by dropping traffic to the origin IPs at your local firewall. This makes the origin unreachable from where you launch attacks, exactly as a real seal would. Start with the web origin, step by step:

sudo iptables -A OUTPUT -d 20.88.188.200 -j REJECT
Verify the seal: the direct-to-IP request that returned 200 in Lab 1 now fails with a connection error (curl exit code 7):
curl -s -o /dev/null -w "%{http_code}\n" --max-time 6 http://20.88.188.200/ || echo "blocked (curl exit $?)"
The output is 000 followed by blocked (curl exit 7): the connection was refused, so the web origin is now unreachable from your workstation.
Why this is a fair simulation: a real seal makes the origin refuse every non-Cloudflare client at the origin itself; blocking the origin IP at your workstation refuses it for you, the attacker, which is the same observable result from the attack machine. The only difference is scope: the real seal blocks everyone, this blocks only your workstation.

Your turn (do it yourself): the orders API runs on a second origin IP, 4.157.169.241, that is just as reachable directly and would let an attacker bypass every API Shield control you built. Seal it too, using the exact same technique you just used for the web origin, then prove it is refused.

Check your work: your command should be sudo iptables -A OUTPUT -d 4.157.169.241 -j REJECT, and a direct probe should now be refused:
curl -s -o /dev/null -w "%{http_code}\n" --max-time 6 https://4.157.169.241/ || echo "blocked (curl exit $?)"
You get 000 and blocked (curl exit 7), the same refusal you saw for the web origin. Both origin IPs are now sealed from your workstation.

15.2 Replay the Lab 1 Attacks

With both origin IPs sealed (simulated), rerun the opening attacks. The direct-to-IP path is gone, and the Cloudflare path is now defended by everything you built in Labs 7 to 14. Predict first: for each probe below, predict the new result and which layer produces it, then run them. Confirm the Cloudflare path still works and the sensitive file is virtually patched:

curl -s -o /dev/null -w "website via CF: %{http_code}\n" https://<student-domain>/
curl -s -o /dev/null -w "/.git via CF:   %{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify: the website through Cloudflare still returns 200 (blocking the origin IP does not affect the hostname, which resolves to Cloudflare's edge), while /.git/secrets.txt now returns 403: the Lab 7 managed ruleset virtually patches, through Cloudflare, the exact file that leaked its secrets in Lab 1.

Compared to your Lab 1 baseline:

Remove the simulated seal to restore the workstation to its normal state:

sudo iptables -D OUTPUT -d 20.88.188.200 -j REJECT
sudo iptables -D OUTPUT -d 4.157.169.241 -j REJECT
Verify: the direct-to-IP request returns 200 again, confirming the block was local and is now cleared:
curl -s -o /dev/null -w "%{http_code}\n" --max-time 6 http://20.88.188.200/
On a self-hosted origin: instead of this local simulation you would apply 15.1 at the origin (Authenticated Origin Pulls or a Cloudflare-IP firewall allowlist). The direct-to-IP request would then fail for everyone, not just your workstation, with no cleanup step, and the attack-replay results through Cloudflare would be identical.

15.3 Architecture Recap

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.

LayerChaptersWhat it defends
DNS onboarding & proxy2Hides the origin IP so attackers cannot target it directly.
SSL/TLS encryption3Ends plaintext and certificate warnings; Full (Strict) end-to-end encryption on a self-hosted origin.
Caching & performance4, 5Absorbs traffic spikes at the edge so the origin stays up.
Traffic & rules engine6Runs business and hardening logic at the edge, off the origin.
WAF (managed + custom)7, 8Virtually patches the exposed file and enforces access policy.
Bot management9Challenges scrapers while allowing trusted automation.
Threat intelligence10Preemptively blocks anonymizers and open proxies with Cloudflare-managed lists.
Rate limiting11Caps volumetric abuse and API fuzzing.
API Shield12, 13Discovers the API and enforces its OpenAPI schema at the edge.
Page Shield14Watches the browser for supply-chain and skimming attacks.
Sealed origin15Forces all traffic through Cloudflare so nothing above can be bypassed (simulated at the workstation in this lab).
Why this ordering matters: each layer assumes the ones before it. Encryption is meaningless if traffic never reaches Cloudflare (Lab 2), and the WAF is meaningless if the origin can be reached directly (Lab 15). Building outward from routing to encryption to performance to security, then sealing the origin last, is what makes the perimeter hold.

Lab 15 Troubleshooting

SymptomLikely causeFix
Direct-to-IP curl still returns 200The iptables REJECT rule was not added, or targets the wrong IPRe-run the iptables -A OUTPUT -d <origin-ip> -j REJECT commands exactly
The website through Cloudflare also failsYou blocked the hostname or a Cloudflare edge IP, not the origin IPOnly block the origin IPs; the hostname must keep resolving to Cloudflare
The API origin is still reachable directlyYou sealed only the web origin IPAlso run sudo iptables -A OUTPUT -d 4.157.169.241 -j REJECT for the orders API origin
Workstation still blocked after the labThe REJECT rules were not removedRun the two iptables -D OUTPUT ... delete commands to restore normal traffic
/.git/secrets.txt not 403 on replayThe Lab 7 managed ruleset is not deployedConfirm the Cloudflare Managed Ruleset from Lab 7 is active
You did it. AcmeCorp went from a wide-open origin (Lab 1) to a layered, validated edge perimeter (Lab 15): the IP is hidden, traffic is encrypted, spikes are absorbed, and every opening attack now fails. That is the full journey from exposed to defended.

End of Unified Lab Guide (Legacy Edition).