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

Now you start closing the holes from Chapter 1. You deploy Cloudflare's managed rulesets to virtually patch the exposed sensitive file, tune out a false positive on the contact form, and read Security Analytics to tell an attacker from a customer.
Estimated time: 30 to 35 minutes.
Why this lab exists: in Lab 1 you pulled /.git/secrets.txt straight off the origin, and you cannot fix the origin's exposed file 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.
What you'll build: the Cloudflare Managed Ruleset and the OWASP Core Ruleset (in Log to start), a surgical managed-rules exception so the contact form is not falsely blocked, 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 Deploy the WAF and understand OWASP

First, from the workstation, confirm the exposure is still live through Cloudflare (not just on the raw IP):

curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/.git/secrets.txt
Verify (still exposed): you get 200. With no WAF rules deployed, Cloudflare forwards the request and the origin serves the secrets file.

Go to Security > Security rules. In the Managed rules section, click Deploy managed ruleset and deploy the Cloudflare Managed Ruleset.

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.

Deploy the Cloudflare OWASP Core Ruleset as well. On its configuration page, leave Anomaly Score Threshold at Medium (40) and Paranoia Level at PL1, and deploy it in Log action to start.

Why: OWASP provides industry-standard protection against the Top 10 (SQLi, XSS, and more). It uses anomaly scoring: each matching rule adds points, and it only acts once the total crosses the threshold. Starting in Log lets you observe impact before you block legitimate traffic; PL1 keeps false positives low.

Now retest the exposed file through Cloudflare:

curl -s -o /dev/null -w "%{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 recognized the request for the exposed source-control path and blocked it at the edge, even though the origin would still happily serve it. Go to Security > Analytics > Events and confirm a matching Block event under Managed rules.

7.2 Dealing with False Positives

The contact form at /contact/ accepts free-text that can look like an injection payload, so legitimate submissions can trip the WAF's SQL-injection rules. First reproduce the false positive from the workstation:

curl -s -o /dev/null -w "%{http_code}\n" "https://<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Verify the false positive: you get 403 and the Cloudflare block page: a legitimate contact-form submission is hard-blocked. In Security > Analytics > Events a Block event appears for path /contact/ under Managed rules; expand it to see the matched rule (SQLi - Comment) in the Cloudflare Managed Ruleset. The Managed Ruleset blocks this pattern on its own default action, before the OWASP ruleset (running in Log) ever scores it.

Create a surgical exception. Go to Security > Security rules, click Create rule > Managed rules (the New managed rules exception page). Name it "Contact form exclusion". Under When incoming requests match set Field = URI Path, Operator = wildcard, Value = /contact/*. Keep Skip all remaining rules. Change Place at from Last to First, then Deploy.

Why the order is critical: a skip exception only skips rules that run after it. The Place at field defaults to Last, which puts the exception below the rulesets, so they run first and the skip appears to do nothing. Placing it First is what makes the skip effective. Reordering can take 15-30 seconds to apply at the edge.
Verify the exception works: re-send the contact-form request a few times (add a unique marker such as &n=1, &n=2 to avoid de-duplication), and send a control request to a different path. In Security > Analytics > Events the /contact/ requests now show Action = Skip while other paths still show managed-rule events. The exception surgically disabled managed rules on the contact path only.

7.3 Reading Analytics & Identifying Threats

Go to Security > Analytics. The Events tab groups firewall events by Action and by Service (for example, Managed rules) and lists the top source IPs, paths, and countries. The Traffic tab shows sampled request logs with Cloudflare's threat classifications.

Why: analyzing the dashboard helps you tell a distributed attack from a single malicious actor, so you can fine-tune your posture.

Use the filters to answer: which rules or services triggered most, and is a single source responsible? Drill into an event to view the Ray ID, User Agent, ASN, and matched rule.

Verify (Security > Analytics > Events): with Group view by: Action you see summary cards: Block (the .git virtual patch plus the blocked contact payload) and Skip (your exception rule). Under Events by service > Managed rules the matched rules are listed by name (for example Version Control - Information Disclosure, SQLi - Comment, and Contact form exclusion). Because all of your test traffic came from the workstation, a single entry under Source IP Addresses accounts for every event, Paths shows /.git/secrets.txt and /contact/, and User Agents shows curl. This "one IP, one user agent" pattern is exactly how you distinguish a single automated actor from a distributed attack.

Lab 7 Wrap-up

MilestoneHow you confirmed it
Virtually patched the Lab 1 file leak/.git/secrets.txt flipped from 200 to 403 through Cloudflare
Deployed the OWASP Core Rulesetruleset shows in Log at threshold Medium (40), PL1
Reproduced a false positivea legitimate /contact/ submission returned 403 (SQLi - Comment)
Fixed it surgicallya managed-rules exception placed First shows Action = Skip on /contact/ only
Read Security AnalyticsEvents grouped by Action/Service, single source IP and user agent identified

Lab 7 Troubleshooting

SymptomLikely causeFix
/.git/secrets.txt still returns 200Managed Ruleset not deployed, or change not propagatedConfirm the Cloudflare Managed Ruleset is deployed; 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. You write custom rules to block traffic by geography, challenge suspicious traffic on the contact form, lock the admin panel to a corporate IP list, and skip security for one trusted partner API.
Estimated time: 30 to 35 minutes.
Why this lab exists: managed rules stop known-bad patterns, but they do not enforce AcmeCorp's own policy. You need to block sanctioned regions, add friction to a frequently-abused form, lock the admin panel to corporate IPs, and let one trusted B2B partner API through untouched. Custom rules express all of that in Cloudflare's expression language.
What you'll build: a geo-block rule plus a log-only competitor-watch rule, a Managed Challenge on the contact form, an account-level IP List that gates /admin, and a Skip rule scoped to a trusted-partner header.
Prerequisites: Lab 7 complete (WAF deployed). Dashboard access to Security > Security rules and Manage account > Configurations > Lists. Workstation Terminal for curl.

8.1 Geo Blocking + Different Actions

Go to Security > Security rules. In the Custom rules card click Create rule (or use the Create rule menu at the top right and choose Custom rules). Name it "Geo Block".

Why: Custom rules let you build complex logic using Cloudflare's wirefilter expression language.

Click Edit expression (top-right of the "When incoming requests match" box) to switch to the raw expression editor, then paste an expression that matches everything outside the United States and Canada. Under "Then take action" choose Block, leave the response as the default Cloudflare WAF block page (403), and click Deploy:

not (ip.src.country in {"US" "CA"})
Why: This enforces strict regulatory compliance by preventing requests from unauthorized countries. The ip.src.country field is a two-letter ISO country code derived from the visitor's IP.

Create a second, separate custom rule named "Competitor Monitoring". Set the action to Log and use an expression that matches a single country you want to watch, such as China:

ip.src.country eq "CN"
Why: AcmeCorp wants threat intelligence on a competitor's region without actively blocking it yet. The Log action records matching requests in Security Analytics but takes no mitigating action, so evaluation continues to your other rules.
Testing geo rules from a single location: your workstation only has one source country, so you cannot directly send traffic that appears to come "from" another country. To prove the engine works, temporarily edit the rule's expression to match your own country, test, then change it back.

First, from the workstation, confirm that normal traffic is allowed by your Geo Block rule:

curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/
Verify (allowed): you should get 200. Because the workstation is not outside US/CA, the Geo Block expression does not match it, so the request passes.

Now open Security > Security rules > Custom rules, edit Geo Block, temporarily change its expression to match your own country (for example ip.src.country eq "US"), click Save, wait about 15 seconds for the edge to update, then re-run the same curl:

Verify (blocked): the same request now returns 403 and the Cloudflare block page. This proves the geo match and the Block action are working. Change the expression back to not (ip.src.country in {"US" "CA"}) and Save; the curl should return 200 again.

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

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.2 Challenge the Contact Form

Go to Security > Security rules. In the Custom rules card click Create rule. Name it "Challenge Contact Form", click Edit expression, and match the contact path:

http.request.uri.path eq "/contact/"

Under Then take action choose Managed Challenge, and Deploy.

Why: A Managed Challenge presents an interstitial that a real browser solves silently but an automated client cannot, so it filters bot form-spam and scripted abuse without blocking real customers. Cloudflare then issues a cf_clearance cookie to a client that passes, so subsequent requests in that session are not re-challenged.
Verify: from the workstation, request the contact path and check the mitigation header. An automated curl client cannot solve the challenge:
curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/contact/
curl -s -D - -o /dev/null https://<student-domain>/contact/ | grep -i "cf-mitigated"
You should get 403 and cf-mitigated: challenge. A real browser would be offered the challenge, solve it, and receive a cf_clearance cookie.

8.3 Lock Down the Admin Panel with an IP List

First, find the workstation's public IP by running this on the workstation:

curl -s https://api.ipify.org; echo

Lists are defined at the account level. Go to Manage account > Configurations > Lists, click Create IP list, set the Identifier to corporate_ips (lowercase letters, numbers, and underscores only; it cannot be changed later), and Create. On the next screen add the workstation's public IP and click Add to list.

Why: 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.

Return to Security > Security rules, create a custom rule named "Allow Corp Admin Only", click Edit expression, and block /admin for anyone not on the corporate list. Set the action to Block and Deploy:

(http.request.uri.path eq "/admin") and not (ip.src in $corporate_ips)
Why: This builds a perimeter around the backend admin panel: only source IPs on the corporate list may reach /admin; everyone else is blocked at the edge. "Block when NOT in the list" is the correct pattern, because the custom-rule Allow action only skips remaining custom rules; it does not block anyone by itself.
How to read the results: the origin protects /admin with HTTP Basic auth, so a request that reaches the origin returns 401. A request blocked by your rule returns 403 with the Cloudflare block page. So 401 means "the WAF let it through" and 403 means "the WAF blocked it".

From the workstation (whose IP is on the list), confirm you are allowed through:

curl -s -o /dev/null -w "%{http_code}\n" https://<student-domain>/admin
Verify (allowed): you get 401. Your IP is in corporate_ips, so the rule does not match and the request reaches the origin's Basic-auth challenge. To prove the block, remove your IP from the list, wait ~15 seconds, and re-run the curl: it now returns 403. Re-add your IP to restore access.

8.4 Skip Security for a Partner API

Go to Security > Security rules, create a custom rule named "Skip Partner API Security", and click Edit expression. Match requests to the partner API host that carry a trusted-partner header:

(http.host eq "public-api.<student-domain>") and any(http.request.headers["x-partner-api"][*] eq "true")
Why: A trusted B2B partner's automated calls trip the managed ruleset and generate false positives. The custom header x-partner-api: true identifies that trusted traffic. HTTP header names are lowercased in expressions, and http.request.headers[...] returns an array, so it is wrapped in any(...[*] eq "true").

Under Then take action choose Skip. In "WAF components to skip" tick All managed rules, leave Log matching requests enabled, and Deploy.

Why: The Skip action disables the selected security engines for matching requests only, while leaving DDoS protection and everything else active for the rest of your traffic.
Verify: from the workstation, send the same request with and without the header and compare in Security > Analytics > Events:
P="q=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -H "x-partner-api: true" "https://public-api.<student-domain>/v1/orders?$P"
curl -s -o /dev/null "https://public-api.<student-domain>/v1/orders?$P"
The request with the header appears as a Skip event and produces no Managed rules event; the request without the header still triggers the managed rules. This proves the bypass works only for trusted partner traffic.

Lab 8 Wrap-up

MilestoneHow you confirmed it
Geo block with a Log companion rulea temporary self-country expression flipped your request to 403, then back to 200
Challenged the contact form/contact/ returned 403 with cf-mitigated: challenge
Locked /admin to an IP Liston-list IP got 401 (reached origin auth); off-list got 403
Skipped security for the partner APIthe request with x-partner-api: true logged a Skip, without it hit managed rules

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
IP List cannot be referenced in a ruleIdentifier typo, or the list is still emptyReference it as $corporate_ips and confirm your IP was added
/admin returns 403 from your workstationYour public IP is not on corporate_ipsAdd your api.ipify.org IP to the list, wait ~15s, retry
Partner Skip rule never matchesHeader casing or array syntaxUse any(http.request.headers["x-partner-api"][*] eq "true") (lowercased name)
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: 25 to 30 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 baseline challenge rule scoped to the bot-demo path, precise exclusions for trusted automation, and a first-hand look at how an un-scoped bot rule causes zone-wide collateral damage (then how to fix it).
Prerequisites: Lab 7 complete (you can create Security rules). Enterprise Bot Management (Always active). Workstation Terminal for curl.

9.1 Bot Analytics & Baseline Rule

First confirm Bot Management is on. Go to Security > Settings and find the Bot traffic section: Bot management should read Always active on this Enterprise zone.

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.

Create the rule: Security > Security rules > Custom rules > Create rule. Name it "Bot Management - Baseline", click Edit expression, and paste the expression below. Scope it to the bot-demo path and start the action on Log, then Deploy. (A ready-made Templates option, "Mitigate definite bot traffic", is also available under Security rules if you prefer a guided starting point.)

(cf.bot_management.score lt 30) and not cf.bot_management.verified_bot and http.request.uri.path contains "/hellobots"
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.

Once the Log events look right, edit the rule, change the action to Managed Challenge, and Deploy. Test from the workstation (a single-egress curl client scores as automated, so it is your stand-in scraper):

curl -s -o /dev/null -w "%{http_code}\n" "https://<student-domain>/hellobots"
curl -s -D - -o /dev/null "https://<student-domain>/hellobots" | grep -i "cf-mitigated"
Verify: with the action on Managed Challenge, 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). You will also see the rule firing in Security > Analytics > Events.

9.2 Refine BM Rule to Exclude APIs

Legitimate automation also scores low: a nightly internal BI tool identifies itself with the user agent B1-Bot/1.11, and a trusted catalog-sync job hits /services/luggages/. Both would be challenged by the baseline rule. Edit "Bot Management - Baseline", click Edit expression, and replace the expression with:

(cf.bot_management.score lt 30) and not cf.bot_management.verified_bot and not (http.request.uri.path eq "/services/luggages/") and not (http.user_agent eq "B1-Bot/1.11")
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.

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

Before you move on, watch the side effect of that broadened expression. The rule is no longer scoped to /hellobots, so it now evaluates every request on the zone. Request an ordinary page you never intended to protect from bots, the site homepage, from the workstation:

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 12 to 17 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.

Lab 9 Wrap-up

MilestoneHow you confirmed it
Confirmed Bot Management is activeBot management: Always active in Security > Settings
Baseline challenge on the bot path/hellobots returned 403 with cf-mitigated: challenge
Excluded trusted automation/services/luggages/ and the B1-Bot/1.11 agent passed with 200
Saw and fixed the over-broad rulethe homepage was wrongly challenged, 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
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: 15 to 20 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 custom rule that blocks Cloudflare's Open Proxies and Anonymizers managed lists.
Prerequisites: Lab 7 complete (you can create Security rules). Enterprise plan (managed lists available). Dashboard access to Security > Security rules.

10.1 Using Managed Lists

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:

(ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)

Set the action to Block, set Status = Active, and click Deploy.

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.
Verify: the rule saves and appears in the Custom rules list with the expression (ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer), action Block, and status Active. 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.
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.

Lab 10 Wrap-up

MilestoneHow you confirmed it
Blocked known-bad networks by threat intelthe rule is Active with (ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)
Zero-maintenance enforcementCloudflare keeps the list contents current; no manual blocklist to maintain

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)
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: 25 to 30 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, and a response-code rate limit that throttles search fuzzing by counting 404s.
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.

Test from the workstation by bursting past the threshold:

D=<student-domain>
for i in $(seq 1 8); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/services/safes/"; done
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; what matters is that the burst flips from 200 to 429.

11.2 Rate Limit By Origin Response Code

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

http.request.uri.path eq "/services/search"
Why: Attackers fuzz search endpoints with junk queries to find hidden parameters. Those probes overwhelmingly generate 404 responses, which is the signal we count.

Tick Use custom counting expression. In the Increment counter when box click Edit expression and enter the origin-response condition:

http.response.code eq 404
Why: 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.

Set Requests = 10, Period = 1 minute. Under Then take action choose Block with duration 1 minute, then Deploy.

Why: Sustained 404-generating traffic is almost always malicious, so a hard Block is appropriate once the threshold is crossed.

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 IPa burst on /services/safes/ flipped from 200 to 429 around request 6
Throttled fuzzing by response codethe 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: 20 to 25 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, and operations brought under Endpoint Management.
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 Identifiers, Discovery & Endpoint Management

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.

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

Discovered endpoints appear under Security > Web assets > Operations (Endpoint Management) via Add operations > Select from Discovery. Until Discovery populates, add operations manually: on Add operations use Add custom operations and enter Method, Hostname (public-api.<student-domain>), and Path (for example POST /v1/orders), then Save operations.

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 operation appears in Endpoint Management with its method, hostname, and path. After ~24h, confirm your /v1/orders operations also show up 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
Generated attributed API trafficthe loop sent distinct session-id values across all three endpoints
Brought operations under managementthe /v1/orders operations appear in Endpoint Management

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
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: 25 to 30 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, and a fallthrough block for undefined operations.
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 Schema Validation

First, onboard the orders API. In Cloudflare DNS > Records, create an A record named api (the name must be exactly api) pointing to 4.157.169.241, with Proxy Status set to Proxied (orange cloud).

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.

Test from the workstation, one compliant request and two violations. 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.

Finally, add a fallthrough block. In Schema validation, use the Fallthrough template / setting to Block requests to the API host that do not match any known operation.

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.

Lab 13 Wrap-up

MilestoneHow you confirmed it
Onboarded and enforced the schemaa 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 gapa fallthrough block rejects requests to any operation not in the schema

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)
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: 25 to 30 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), and a CSP allowlist that reports or blocks anything off-list.
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. Once you confirm nothing legitimate is caught, switch the action to Allow to enforce the allowlist.

Why: 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. Log emits the report-only CSP header so browsers report violations without blocking; Allow emits the enforcing header.

Confirm the policy is live by checking the response header from the workstation:

curl -s -D - -o /dev/null https://ps.<student-domain>/contact/received/ | grep -i content-security
Verify: in Log mode the response includes a content-security-policy-report-only header listing your allowed script-src and connect-src sources; in Allow mode it is the enforcing content-security-policy header. A resource outside the allowlist is reported (or blocked) accordingly.

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
Enforced a CSP allowlista content-security-policy (or report-only) header appears with your allowed sources

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
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: 20 to 25 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 (a local firewall block that reproduces the attacker's experience), a replay of the Lab 1 attacks that proves each gap is now closed, and an architecture recap of the whole guide.
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:

sudo iptables -A OUTPUT -d 20.88.188.200 -j REJECT
sudo iptables -A OUTPUT -d 4.157.169.241 -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 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.

15.2 Replay the Lab 1 Attacks

With the origin 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. 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
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).