Cloudflare Application Services - Lab Guide

⬇ Course syllabus (PDF)

Unified Lab Guide & Technical Reference — Courseware Version 4.0 (AcmeCorp Edition). Prepared for AcmeCorp IT Security & Infrastructure. Scenario: Edge Security Modernization.

⚠ Read this first: you'll need it every time you change networks

The lab's AWS security groups allow inbound SSH (port 22) to all three VMs only from the public IP address you deployed from. The moment you move to a different network (home to classroom, classroom to hotel, or your ISP rotates your address), SSH to the workstation and origins stops working and you get connection timeouts.

Almost every command in this guide runs from the workstation over SSH (ssh cloudflare@<workstation-ip>), so refresh your IP before you start Lab 1.

There is one command for everything. Run the same lab installer for your operating system any time you need it: to deploy the lab the first time, to update your IP, or to tear the lab down at the end of class. You never need to know where the lab files live or type any other command.

Windows (PowerShell):

irm https://appsec-lab-deploy.cloudflareacademy.com/win | iex

macOS / Linux:

curl -fsSL https://appsec-lab-deploy.cloudflareacademy.com/mac | bash

The installer sets up your toolchain, finds your lab in AWS, then shows a short menu:

  • If your lab is already deployed: [1] Update my IP   [2] Destroy lab   [3] Quit. Choose 1 to unlock SSH from wherever you are now: it detects your current public IP, confirms the change, and rewrites the SSH rule on all three security groups.
  • If no lab exists yet: [1] Deploy new lab   [2] Quit.

Works from any computer. Because your lab's state lives in your own AWS account, running the same command on a second machine finds your deployed lab automatically and offers the same menu, so you can update your IP or destroy the lab without getting back to the machine you deployed from. The first time you run it on a new computer it will ask for your AWS access key, secret, and region.

Only SSH is IP-locked. The origins' HTTP :80 and HTTPS :443 stay open to the public internet, so the AcmeCorp website, the public API, and Lab 1's direct-to-origin attacks keep working across network changes.

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. Two origin servers sit on public IP addresses, each serving both the corporate website and a public orders API, with no CDN, no real encryption, and no security layer in front of them. It shows: browsers throw certificate warnings, traffic spikes knock the site offline, attackers openly probe the login page, scrape pricing, and fuzz the API, and because the origin IPs are public, anyone can reach them 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 code 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.

AcmeCorp's environment runs on three Ubuntu virtual machines in your AWS account. Each of the two origin servers serves both the AcmeCorp website and the AcmeCorp public API, reachable through Cloudflare as ps.<student-domain> and public-api.<student-domain>. Origin A is your primary; Origin B is an identical backup used in the Load Balancing lab.

ComponentPublic IPRole
Client Workstation<workstation-ip>Ubuntu machine you drive the labs from: run curl, dig, and openssl, and simulate attacks. Preloaded with the lab tooling.
Origin A (primary)<origin-a-ip>Serves the website (ps.<student-domain>) and the public API (public-api.<student-domain>). Identifies as origin-1; the website /status reports "origin":"primary".
Origin B (backup)<origin-b-ip>Identical to Origin A. Identifies as origin-2; the website /status reports "origin":"backup". The second pool member in the Load Balancing lab.

All three VMs use the cloudflare user with the lab password #Savetheinternet (for example, ssh cloudflare@<origin-a-ip>).

Fill in here — your lab values

Your instructor assigns you a student domain; the three public IPs are printed by the lab installer when you deploy, as workstation_public_ip, origin_a_public_ip, and origin_b_public_ip. Type them below and every matching placeholder across this entire guide — including inside the copy blocks — is replaced with your real values. Values save 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 servers directly by IP, read a sensitive file they should never expose, scrape the product catalog, knock on the locked admin panel, then break the login with SQL injection and brute-force credentials. Nothing stops you. This is AcmeCorp's "before" picture and the baseline every later chapter is measured against.
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 two origin servers answer directly on their public IP addresses, on both :80 and :443, with a self-signed certificate. In this chapter you play the attacker: confirm the origins are exposed, leak a sensitive file, scrape the product catalog, probe the locked admin panel, then break the login with SQL injection and brute-force credentials, all unimpeded. Record every result. It is the baseline that later chapters must beat.
Run everything from the workstation. Open an SSH session to the workstation (ssh cloudflare@<workstation-ip>, password #Savetheinternet) and run the commands there. The workstation reaches the origins over the public internet, exactly as any attacker would.

1.1 Confirm the Origin Is Exposed

From the workstation, request the origin directly by IP and read the response headers:

ssh cloudflare@<workstation-ip>
curl -sI http://<origin-a-ip>/ | head -n 6
Why: With no proxy in front, the raw origin IP is publicly routable and the response advertises the exact web server software and version 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 <origin-a-ip>:443 -servername ps.<student-domain> 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. The second shows the certificate's issuer and subject are the same self-signed value, which is why a real browser would throw a certificate warning. The origin is reachable, fingerprinted, and not properly encrypted.

1.2 Leak Sensitive Files from the Origin

Developers accidentally left a source-control artifact on the web root. Request it directly:

curl -s http://<origin-a-ip>/.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 9.
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://<origin-a-ip>/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 11 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 Probe the Locked Admin Panel

Knock on the administrative panel and see how it responds:

curl -s -o /dev/null -w "%{http_code}\n" http://<origin-a-ip>/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 10).
Verify (locked, but reachable): the request returns 401. What matters for the baseline is that /admin is reachable directly on the raw IP at all.

1.5 Break the Login with SQL Injection

These origins also run a login application. Send a classic SQL injection payload straight at the login endpoint. The quotes and spaces are URL-encoded so the full payload survives:

curl -s -o /dev/null -w "%{http_code}\n" "http://<origin-a-ip>/login?username=admin%27%20OR%20%271%27%3D%271%27%20--%20&password=x"
Why: There is no Web Application Firewall inspecting Layer 7 traffic, so the injection string reaches the application untouched. This is the exact attack you will virtually patch in Lab 9.
Verify (attack lands): the request returns 200. Nothing inspected or blocked the payload; it went straight to the application.

1.6 Brute-force the Login

Now cycle a short password list against the login form, exactly as a credential-stuffing bot would:

for pw in 123456 password admin letmein qwerty hunter2 acmecorp; do
  curl -s -o /dev/null -w "%{http_code}\n" -X POST http://<origin-a-ip>/login -d "username=admin&password=$pw"
done
Why: The origin applies no rate limit or lockout, so an attacker can try thousands of passwords per minute. This is the brute force you will cap per IP in Lab 12.
Verify (no lockout): every attempt returns a normal status code (200 or a redirect); none are throttled or challenged. The login is wide open.
You just landed a run of attacks against a completely unprotected origin: exposure and fingerprinting, a leaked secrets file, catalog scraping, a directly reachable admin panel, SQL injection, and credential brute force. Keep these results. In Lab 18 you will seal the origin and replay this exact suite, and watch every one of them fail.

Lab 2 - DNS Onboarding & Proxy Control

Those attacks landed because the origin IP addresses are public. You put Cloudflare in front by onboarding authoritative DNS and proxying the web records so the real origin IPs disappear, while handling a legacy partner CNAME and an FTP record that must stay grey-clouded.
AcmeCorp Scenario: AcmeCorp's first priority is to place Cloudflare in front of their servers to hide the origin IPs (Origin A <origin-a-ip> and Origin B <origin-b-ip>). You will execute a full DNS migration for the primary domain. However, a legacy partner portal must be managed via a Partial (CNAME) setup. Finally, an old batch-processing FTP service needs to bypass the Cloudflare proxy entirely.

2.1 Cloudflare as Authoritative DNS

Log in to the Cloudflare dashboard and click Add a site.

Why: AcmeCorp needs to onboard their domain to leverage Cloudflare's global Anycast network for CDN caching and DDoS protection.

Enter your assigned domain name <student-domain> and select the Enterprise plan.

Why: The Enterprise plan is required to access the advanced WAF, Bot Management, and API Shield features dictated by AcmeCorp's security policies.

Because this is a fresh zone, the quick scan finds nothing to import. Create two A records by hand: ps and public-api, both pointing to Origin A (<origin-a-ip>), each with Proxy Status set to Proxied (orange cloud).

Why: Origin A runs the website (:80) and the API (:443) on the same box, so both hostnames resolve to the same IP. Proxying the A records ensures all traffic hits Cloudflare's security edge first, hiding the true IP of the AcmeCorp servers.

Log into your domain registrar (simulated via your instructor tool).

Why: To make Cloudflare authoritative, the internet's root servers must be told to look at Cloudflare's nameservers for this domain.

Change the nameservers to the ones provided by Cloudflare (e.g., howard.ns.cloudflare.com).

Why: This officially cuts over traffic from the legacy DNS provider to Cloudflare's edge.

Wait for DNS propagation.

Why: DNS changes take time to propagate across global ISP caches.

From the Terminal on your workstation, confirm the nameservers are pointing to Cloudflare:

dig +short NS <student-domain>
Expected output:
howard.ns.cloudflare.com.
mia.ns.cloudflare.com.
Why: Verifying the nameserver change confirms that the AcmeCorp migration to the Cloudflare control plane is successful.

2.2 Partial (CNAME) Setup for a Stubborn Partner (Concept)

Why this lab is read-only: Partial (CNAME) setup means Cloudflare is not your authoritative DNS: you keep your existing DNS provider and only proxy individual hostnames into Cloudflare. Your lab domain is a Cloudflare Registrar domain, where Cloudflare is both the registrar and the authoritative nameserver, and Cloudflare requires you to keep its nameservers. Partial setup is therefore not supported on Cloudflare Registrar domains. You cannot perform this on your lab domain, so this chapter walks the workflow conceptually. Do not attempt to convert your lab zone.
AcmeCorp Scenario: AcmeCorp integrated a partner whose portal lives at portal.example.com, hosted at a legacy DNS provider the partner flatly refuses to move off. AcmeCorp cannot take over the partner's nameservers, but still wants Cloudflare's WAF and CDN in front of that one hostname. Partial (CNAME) setup is exactly the tool: Cloudflare protects a specific subdomain without becoming authoritative for the domain.

Prerequisites you would confirm first. The domain must be on a Business or Enterprise plan, it must keep its current authoritative DNS provider (partial setup means Cloudflare is not authoritative), and it must not be a Cloudflare Registrar domain. Only proxyable record types (A, AAAA, CNAME) can be onboarded this way, and the root/apex (example.com) cannot be proxied in a partial setup, only subdomains.

Why: These constraints are what make partial setup a "surgical" option: it changes nothing about who runs the domain's DNS, so a partner keeps full control while AcmeCorp still gets Cloudflare in front of the one hostname that matters.

Convert the zone to CNAME setup. On the domain's Overview page, in the DNS card on the right, select Convert to CNAME DNS Setup, then select Convert to confirm.

Why: This switches the zone type from Full to Partial. Cloudflare stops expecting to be authoritative and instead waits for you to prove ownership and CNAME in the specific hostnames you want proxied.

Prove ownership with the verification TXT record. After converting, Cloudflare generates a Verification TXT Record (you can always find it again on the DNS > Records page under Verification TXT Record). It looks like this:

Type:    TXT
Name:    cloudflare-verify.example.com
Content: 966215192-518620144

Add that exact record at the partner's existing authoritative DNS provider. Cloudflare polls for it (this can take up to a few hours), marks the zone active, and emails a confirmation.

Why: Because Cloudflare is not authoritative here, it cannot simply read the zone. The TXT record is proof that whoever is asking for protection actually controls the domain, which prevents someone from hijacking a domain they do not own. The record must stay in place for as long as the domain remains on a partial setup.
Gotcha: if the partner's DNS provider automatically appends the domain to record names, enter only cloudflare-verify as the name, not the full cloudflare-verify.example.com, or you will end up with a broken cloudflare-verify.example.com.example.com.

Onboard the hostname. In Cloudflare, create a proxied (orange-cloud) record for the hostname you want to protect, for example portal. Then, at the partner's authoritative DNS provider, create a CNAME for that same hostname pointing to Cloudflare's target:

portal.example.com  CNAME  portal.example.com.cdn.cloudflare.net

Finally, remove any old A, AAAA, or CNAME record for that hostname at the partner's provider, leaving only the .cdn.cloudflare.net CNAME.

Why: The CNAME at the partner's provider is what actually reroutes visitor traffic for portal.example.com into Cloudflare's edge, where the WAF and CDN inspect it, while every other record on the domain keeps resolving straight from the partner's DNS untouched.

Verify. You would confirm with an external DNS lookup tool that portal.example.com now resolves through portal.example.com.cdn.cloudflare.net and shows Cloudflare in front, while the rest of the partner's domain is unaffected.

No hands-on step: do not convert your lab zone. It is a Cloudflare Registrar domain, so Cloudflare blocks the conversion, and even if it did not, converting is a zone-wide change that would break the Full-setup labs that follow. The flow above is exactly what you would do for a real partner domain hosted at another provider.

2.3 Controlling Traffic with Proxy Status

In Cloudflare DNS, create a new A record named ftp pointing to <origin-a-ip>.

Why: AcmeCorp uses FTP for nightly batch processing. We need a DNS record for the scripts to target.

Set the proxy status to DNS only (grey cloud).

Why: Cloudflare's standard proxy only intercepts HTTP/HTTPS (ports 80/443). FTP (port 21) would be dropped if it were proxied. Grey-clouding exposes the origin IP but allows the legacy protocol to function.

Use dig to query that record:

dig +short ftp.<student-domain>
Expected output:
<origin-a-ip>
Why: The output confirms that the raw IP of the server is exposed, verifying the bypass.

Set the proxy status back to Proxied for your main web records (ps and public-api).

Why: This ensures the primary web applications remain protected by the WAF.

Lab 3 - SSL/TLS & Encryption Standards

The browser padlock is missing and traffic to the origin is plaintext. You turn on encryption in stages: Flexible first, then Full (Strict) with a Cloudflare Origin CA certificate, and finally an advanced certificate for the deep payments subdomain.
AcmeCorp Scenario: The AcmeCorp site is currently throwing browser errors because there is no valid SSL certificate on the ps server. You need to fix this instantly using Flexible mode. However, security compliance requires strict end-to-end encryption, meaning you must eventually install an Origin CA Certificate on the server. Lastly, the payment portal uses a complex subdomain requiring an Advanced Certificate.

3.1 Universal SSL, Flexible Mode & Edge Encryption

Go to SSL/TLS > Overview and open Configure encryption mode.

Why: The AcmeCorp site currently serves over HTTP. We need to encrypt the "first mile" (client to Cloudflare) to remove browser security warnings.

Under Encryption mode, select Flexible, then click Save. (The manual modes appear as radio options beneath "Automatic SSL/TLS (recommended)"; there is no separate "Custom" toggle.)

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 requiring server-side changes.

Verify that your Universal SSL certificate has been issued and is active under Edge Certificates.

Why: Universal SSL provides the free, automated certificates that cover the root and wildcard subdomain.

Now harden the edge. On SSL/TLS > Edge Certificates, 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. Flexible mode gave the padlock; Always Use HTTPS and a modern minimum TLS now make the client-to-Cloudflare "first mile" redirected and modern, so students can see the difference between raw Flexible and a hardened edge.

Verify the redirect and the negotiated protocol from the workstation:

curl -sI http://ps.<student-domain>/ | head -n 4
curl -sI https://ps.<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.

Visit your site using https://ps.<student-domain> and inspect the certificate in the browser.

Why: This proves to the marketing team that the site now appears secure to end-users.

(Optional) Make a request using curl to Origin A directly (<origin-a-ip>) on port 80, bypassing Cloudflare.

Why: This demonstrates that despite the browser showing a padlock, the backend connection remains unencrypted, validating the security team's compliance concern.

3.2 Full (Strict) + Origin Certificate

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

Why: AcmeCorp compliance requires end-to-end encryption. Creating a Cloudflare Origin CA certificate avoids the cost and hassle of purchasing a third-party certificate for the backend server.

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.

Verify: the new certificate appears in the Origin Certificates list showing 2 hosts (*.<student-domain>, <student-domain>) with an expiry roughly 15 years out.

In the dashboard, copy the generated Origin Certificate and Private Key. You install the same pair on both origin servers: the whole zone runs under one SSL/TLS mode, the certificate is a wildcard (*.<student-domain>) that works on either machine, and the Load Balancing lab (Lab 7) fails traffic over to Origin B, which must also present a valid certificate or failover returns 526 Invalid SSL certificate.

From your workstation, SSH into Origin A:

ssh cloudflare@<origin-a-ip>

Open the certificate file in nano, paste the Origin Certificate you copied from the dashboard, then save and exit (Ctrl+O, Enter, Ctrl+X):

sudo nano /etc/ssl/acmecorp/cert.pem

Open the key file the same way and paste the Private Key, then save and exit:

sudo nano /etc/ssl/acmecorp/key.pem

Reload nginx so it serves the new certificate (it already listens on port 443 for both ps and public-api):

sudo systemctl reload nginx

Repeat the exact same install on Origin B: SSH in, recreate cert.pem and key.pem with the same certificate and key, then reload nginx.

ssh cloudflare@<origin-b-ip>
Why: Each Ubuntu origin needs the cryptographic material to terminate TLS. Both must present a valid certificate so Full (Strict) validation passes on the primary and on failover to Origin B. If Origin B keeps its boot-time self-signed certificate, Full (Strict) rejects it and failover returns 526 Invalid SSL certificate.

Go to SSL/TLS > Overview > Configure encryption mode, select Full (Strict) under Encryption mode, then click Save.

Why: This forces Cloudflare to encrypt the backend connection AND mathematically validate that the Ubuntu server's certificate is authentic, preventing Man-in-the-Middle attacks.

Use curl to send a request to the origin on port 443, expecting a secure connection:

curl -I https://ps.<student-domain>
Why: This verifies that the end-to-end encryption pipeline is fully functional and compliant.

3.3 Advanced Certificates for Subdomains

First, in Cloudflare DNS > Records, create an A record named payments.platform pointing to Origin A (<origin-a-ip>) 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 or browse to 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.

Why: The payment portal resides at payments.platform.<student-domain>. Universal SSL only covers one level of subdomains (e.g., *.domain.com), so it will not cover this deep subdomain.

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 this is a single-hostname certificate.

Why: Advanced Certificate Manager allows AcmeCorp to generate custom certificates for deep subdomain hierarchies. Universal SSL already covers the apex and wildcard, so a dedicated single-hostname certificate for the deep subdomain is all you need.

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.

Wait for the certificate to activate, then confirm from the workstation that it has taken precedence over the 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.

Go to the DNS tab and disable the proxy for the subdomain, then visit that subdomain and note what happens.

Why: You will see a certificate error. Bypassing Cloudflare exposes the origin directly, proving that the Advanced Certificate lives exclusively on the Cloudflare edge, not on the origin server.

Lab 4 - Caching Foundations

Traffic spikes are overwhelming the origin. You establish a caching baseline, offload a busy category page with a cache rule, honor the developers' cache-control headers, and purge stale pricing on demand.
AcmeCorp Scenario: Traffic spikes are overwhelming the AcmeCorp frontend server. 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 in Chrome DevTools: Several steps in this lab ask you to check response headers such as cf-cache-status, age, and cache-control. Whenever a step says "inspect the headers," do this:
  1. Open the URL in Chrome (for example https://ps.<student-domain>/services/luggages/).
  2. Press F12 (or right-click the page and choose Inspect) to open DevTools.
  3. Click the Network tab at the top of DevTools.
  4. Tick Disable cache (in the Network tab toolbar) so the browser's own cache does not hide Cloudflare's behavior, then reload with Ctrl+R (Windows/Linux) or Cmd+R (macOS).
  5. In the request list on the left, click the row for the resource you requested (for example the luggages/ document).
  6. In the panel that opens, select the Headers tab and scroll down to the Response Headers section.
  7. Read the values: cf-cache-status (MISS, HIT, BYPASS, EXPIRED, or DYNAMIC), age (seconds the object has been in cache; only present on a HIT), and cache-control.
Prefer the terminal? From the workstation you can run this and read the same headers in the output:
curl -sSI https://ps.<student-domain>/services/luggages/

4.1 Establishing Caching Baselines

From the workstation, inspect the cache status of a product category page:

curl -sSI https://ps.<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://ps.<student-domain>/services/luggages/ | grep -i cf-cache-status
curl -sSI https://ps.<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://ps.<student-domain>/services/luggages/ | grep -iE "cf-cache-status|age|cache-control"
Verify: control is back with the origin. If the origin sends a Cache-Control header (for example max-age=300), you will see that cache-control line and, after the first request, cf-cache-status: HIT with an age that grows up to the max-age. If the origin sends no Cache-Control for this HTML page, there is no cache-control line and cf-cache-status stays DYNAMIC, because Cloudflare does not cache HTML on its own. Either outcome proves the edge is now deferring to the origin, not to a dashboard rule.
Why: With the rule off, Cloudflare honors whatever Cache-Control the origin sends, so the cache behavior is driven by the application, exactly as the developers intended.

4.4 Troubleshooting an Aggressive Cache

A pricing page is stuck: the origin updated its prices during a live campaign, but visitors still see the old numbers because an aggressive Edge TTL is serving a cached copy. First reproduce the stale HIT. Re-enable the Cache Rule from 4.2 so the category page is cached again, then request it a few times until the edge is serving a cached copy:

curl -sSI https://ps.<student-domain>/services/luggages/ | grep -iE 'cf-cache-status|age'
Verify (stale): cf-cache-status: HIT with a non-zero, growing age. The edge is serving the cached copy, so a price change at the origin is not visible yet.

Fix it with a targeted purge. Go to Caching > Configuration, use Custom Purge, choose Purge by URL, enter https://ps.<student-domain>/services/luggages/, and confirm. Then re-request the page:

curl -sSI https://ps.<student-domain>/services/luggages/ | grep -iE 'cf-cache-status|age'
Verify (fresh): the first request after the purge returns cf-cache-status: MISS (fetched fresh from the origin), and the next request is a HIT again. The stale price is gone.
Why: Cloudflare serves the cached object until its Edge TTL expires. A Custom Purge by URL is surgical: it removes exactly that one object immediately, so you can fix stale pricing during a live campaign without dumping the entire cache and losing the performance benefit everywhere else.

Lab 5 - Advanced Caching

One cache rule is not enough to survive a real surge. You cache full HTML with a safe cookie bypass for logged-in users, normalize marketing URLs with a custom cache key, and turn on tiered caching so the origin sees a fraction of the load.
AcmeCorp Scenario: One cache rule for a single image will not carry AcmeCorp through a real surge. In this chapter you cache full HTML pages while safely bypassing the cache for logged-in sessions, normalize marketing URLs with a custom cache key, and enable tiered caching so upper-tier data centers shield the origin.

5.1 Advanced Strategies: Cache Everything & Cookie Bypass

Create a Cache Everything rule for a specific path (e.g., the marketing landing page).

Why: Cloudflare only caches static file extensions by default. "Cache Everything" forces Cloudflare to cache the HTML itself, allowing the site to survive massive traffic spikes.

Run the following and confirm the HTML is being cached (HIT):

curl -svo /dev/null https://ps.<student-domain>/static/test.html 2>&1 | grep cf-cache-status
Why: This verifies the HTML is being cached (HIT).

Create a second Cache Rule that bypasses the cache when a session cookie is present. Under When incoming requests match, set Field = Cookie, Operator = contains, Value = session_id (the expression reads http.cookie contains "session_id"). Under Then, set Cache eligibility to Bypass cache, and Deploy. Leave it below the Cache Everything rule so it takes precedence for logged-in requests.

Why: Caching HTML is dangerous if users are logged into a portal, as they might see other users' data. This rule ensures logged-in users bypass the cache.

Run the following two commands to verify:

curl -svo /dev/null https://ps.<student-domain>/static/test.html 2>&1 | grep "cf-cache-status"
curl -svo /dev/null -H "Cookie: session_id=12345" https://ps.<student-domain>/static/test.html 2>&1 | grep "cf-cache-status"
Why: The first request should return HIT (served from cache), while the second (containing the cookie) should return BYPASS or DYNAMIC (not served from cache), proving the security logic works. A "Bypass cache" cache rule commonly reports cf-cache-status: DYNAMIC for the matching request.

A broad Cache Everything rule also matches dynamic and health endpoints that must never be served from cache. Add one more Cache Rule to bypass the cache for the health endpoint /status: under When incoming requests match use the expression http.request.uri.path eq "/status", under Then set Cache eligibility to Bypass cache, and Deploy. Keep it below the Cache Everything rule so it takes precedence.

Why: /status reports live origin health and is the endpoint the Load Balancing lab (Lab 7) polls to watch failover. If Cache Everything caches it, the edge replays a stale response and never re-queries the origin or load balancer, so a real outage still looks healthy and failover appears broken. Health and status endpoints should always bypass cache.
Verify the bypass:
curl -sSI https://ps.<student-domain>/status | grep -i cf-cache-status
The response should read BYPASS (or DYNAMIC), confirming /status is fetched fresh from the origin on every request rather than served from cache.

5.2 Custom Cache Key

Create a custom cache key to Ignore query strings (e.g., ?utm_source=twitter, ?utm_source=email).

Why: Marketing tracking tags create unique URLs, breaking the cache and sending identical requests to the origin. Ignoring these strings consolidates them into a single cached object, drastically improving the Cache Hit Ratio.

Verify that two different query strings resolve to the same cached object:

curl -sSI "https://ps.<student-domain>/image.jpg?utm_source=twitter" | grep -i cf-cache-status
curl -sSI "https://ps.<student-domain>/image.jpg?utm_source=twitter" | grep -i cf-cache-status
curl -sSI "https://ps.<student-domain>/image.jpg?utm_source=email" | grep -i cf-cache-status
Why: The first request returns MISS, the second returns HIT. The third uses a different query string (utm_source=email) yet still returns HIT, proving Cloudflare ignored the query string and served the same cached object.
Note: a cache-key change takes a little longer to propagate than the rule itself. If the third request (the different query string) is still a MISS, wait 15 to 30 seconds and run it again: once the new cache key is live everywhere, every query-string variant collapses onto the same cached object and returns HIT.

5.3 Enable Tiered Caching

Enable Tiered Caching on your zone. Under Tiered Cache Topology, select Smart Tiered Cache (Recommended) and click Apply. If the topology already shows Active, confirm the topology is set to Smart Tiered Cache.

Why: Instead of every global Cloudflare PoP requesting the file from the origin, lower-tier PoPs will ask upper-tier PoPs, vastly reducing origin bandwidth.

5.4 Cache Negative Responses with a Status Code TTL

By default Cloudflare does not cache 404 responses, so a burst of requests to missing URLs (a broken campaign link, or an attacker fuzzing paths) reaches the origin every single time. You can absorb that at the edge by caching the 404 itself for a few seconds using a Cache Rule Status Code TTL.

Create a Cache Rule (Caching > Cache Rules > Create rule) that matches a path known to return 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://ps.<student-domain>/missing/nope | grep -iE 'HTTP|cf-cache-status'
curl -sSI https://ps.<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 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, hide the backend software fingerprint, and rewrite the Host header to reach the API vhost, all without touching origin code.
AcmeCorp Scenario: You need to manipulate requests before they reach the AcmeCorp server. You must redirect Portuguese users, build a campaign redirect, hide the backend server identity for security, and override the Host header so the legacy API gateway accepts the traffic.

6.1 Rules Engine: 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://ps.<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://ps.<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 Header Modification

First, observe a security win that Cloudflare gives you for free. The origin server advertises its exact software in the Server response header (for example Server: nginx/1.24.0 (Ubuntu)), which attackers use to fingerprint the backend and target version-specific vulnerabilities. Inspect the Server header on your proxied site and confirm it reads cloudflare, not nginx.

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, so the backend fingerprint is hidden with zero configuration.
Note - you cannot remove or overwrite the Server header yourself: Server is a Cloudflare-managed (reserved) header. If you try to add a Transform Rule that removes it, the deploy is rejected with the API error 'remove' is not a valid value for operation because it cannot be used on header 'Server'. You do not need to: Cloudflare already masks it for you (see above). Transform Rules are still the right tool for headers you do control, which you will use next.

Now create a Response Header Transform Rule that applies to All incoming requests and adds a custom response header: set PSBC-Labs to the value Are Great.

Why: Adding a custom header at the edge lets AcmeCorp's internal telemetry tag traffic that successfully passed through Cloudflare. The same technique is used to inject real security headers (for example X-Frame-Options or Strict-Transport-Security) without touching origin code.
Verify in Chrome DevTools:
  1. Open https://ps.<student-domain>/ in Chrome.
  2. Press F12 (or right-click and choose Inspect) to open DevTools, then click the Network tab.
  3. Tick Disable cache in the Network toolbar and reload with Ctrl+R (Windows/Linux) or Cmd+R (macOS).
  4. In the request list, click the top document row (the request to your hostname).
  5. Select the Headers tab and scroll to Response Headers.
  6. Confirm server: cloudflare (origin's nginx version is hidden) and psbc-labs: Are Great (your Transform Rule fired).
Prefer the terminal? From the workstation run this and confirm both server: cloudflare and psbc-labs: Are Great appear in the output:
curl -sSI https://ps.<student-domain>/

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. Turn on two of them now:

  1. Go to Rules > Settings > Managed Transforms (or, from Rules > Overview, click Go to Managed Transforms).
  2. Under HTTP response headers, enable Remove "X-Powered-By" headers. This strips another backend fingerprint, the same way Cloudflare already masks Server.
  3. Directly below it, enable 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. This is the fast path to the XSS and clickjacking protections you would otherwise hand-build with Transform Rules, and it never touches origin code.
Verify from the workstation: re-run the header check and confirm the security headers are now present and x-powered-by is gone:
curl -sSI https://ps.<student-domain>/ | grep -Ei '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 in the output, and no x-powered-by line.

6.4 Host Header Overrides for API Gateways

AcmeCorp's public API is served by a separate virtual host on the origin: public-api.<student-domain>. The origin routes by the Host header, so an API request that arrives with the public website's Host (ps.<student-domain>) lands on the website, not the API. First confirm the problem: request https://ps.<student-domain>/api/products and note it returns the website's 404 HTML page.

Why: This is the classic API-gateway routing mismatch. The backend expects a specific internal Host, but public traffic arrives with the front-door hostname, so the origin cannot route it to the right service.

Create an Origin Rule with a Host Header Override. Use a custom filter expression that matches the API path, and rewrite the Host the origin receives:

Why: Rewriting the Host header at the edge makes the origin route /api/* traffic to the API virtual host, without changing the public URL or touching origin code.
Verify in Chrome DevTools:
  1. Open https://ps.<student-domain>/api/products in Chrome.
  2. Press F12 to open DevTools, click the Network tab, tick Disable cache, and reload.
  3. Click the api/products request row, then the Response tab: you should now see a JSON product catalog instead of the 404 HTML page.
  4. On the Headers tab, under Response Headers, confirm content-type: application/json and x-origin: origin-1 (proof the request reached the API service on the public-api vhost).
  5. As a control, open https://ps.<student-domain>/ and confirm it still returns the website HTML with no x-origin header, since the override only applies to /api/ paths.
Prefer the terminal? From the workstation, run this before and after you deploy the rule (a GET that prints just the response headers):
curl -s -o /dev/null -D - https://ps.<student-domain>/api/products | grep -iE 'HTTP|content-type|x-origin'
Before the rule it returns 404 text/html; after the rule it returns 200 application/json with x-origin: origin-1. Use a GET here, not a HEAD (curl -I): this orders API only implements GET for /api/products and answers a HEAD with 404, so curl -sSI would show a misleading 404 even though the override is working.

6.5 URL Rewrites: Serve a Different Resource Without Redirecting

Marketing retired the Arrows product but still runs live links and ads pointing at /services/arrows/. Rather than send those visitors a redirect (which changes the address bar and can break campaign tracking), you will quietly serve the Luggages page from that URL using a URL Rewrite Rule. The visitor keeps seeing /services/arrows/; the origin receives a request for /services/luggages/.

First confirm both pages exist and differ: https://ps.<student-domain>/services/arrows/ and https://ps.<student-domain>/services/luggages/ each return 200 with their own product title.

Create the rule:

Why: A URL Rewrite changes the path the origin receives while the visitor's URL stays the same, which is fundamentally different from a Redirect Rule (which returns a 301/302 and changes the address bar). Rewrites are how you re-map moved content, front an object-storage bucket, or reshape API paths at the edge, invisibly to the client and without origin code.
Verify from the workstation: request the arrows URL and confirm you now receive the luggages page while the requested URL is unchanged and there is no redirect:
curl -sS https://ps.<student-domain>/services/arrows/ | grep -i '<title>'
curl -sSI https://ps.<student-domain>/services/arrows/ | head -n 1
Before the rule the title reads Arrows; after the rule the same /services/arrows/ URL returns the Luggages page title, and the status line stays HTTP/2 200 (not a 301/302).

Lab 7 - Load Balancing & Failover

A single origin is a single point of failure. You group both origins into pools behind a health monitor and prove that traffic automatically fails over to the backup when the primary goes down.
AcmeCorp Scenario: AcmeCorp wants to ensure high availability. You will use Origin A (<origin-a-ip>) as your Primary Origin and Origin B (<origin-b-ip>) as your Backup Origin. Both run the AcmeCorp website on port 80 with a /status health endpoint that reports which origin answered ("origin":"primary" from A, "origin":"backup" from B). You will configure Cloudflare to route traffic to the primary but automatically fail over to the backup if the primary goes offline, then harden the health check and experiment with weighted steering.
Build order matters: create the monitor and the two pools first, then the load balancer last. The load balancer wizard asks you to pick existing pools, and the pool form asks you to pick an existing monitor, so building bottom-up avoids empty dropdowns. All three live under Traffic > Load Balancing.

7.1 Configure Origin Pools & Basic Health Checks

Go to Traffic > Load Balancing, click Manage Monitors, then Create new monitor. Create a simple monitor named lab5-http-root-monitor: Type HTTP, Path /, Port 80, expecting the default 200 response code.

Why: Health monitors constantly probe your origin servers from Cloudflare's edge. If a server stops responding with a 200 OK, Cloudflare marks the pool unhealthy and stops sending it traffic.

Back on Traffic > Load Balancing, create two separate Origin Pools (use the pool Create button or the endpoints list). For each pool, add one endpoint and attach the monitor you just made:

Why: Pools logically group your infrastructure so the load balancer knows where to route traffic. Attaching the monitor to each pool is what lets Cloudflare evaluate its health.
Why the Host header matters: In Lab 3 you set the zone to Full (strict), so Cloudflare validates the origin certificate on every proxied request, including load-balanced traffic. Endpoints are addressed by raw IP, and without a Host header Cloudflare sends that IP as the TLS SNI. The origin certificate only covers <student-domain> and *.<student-domain>, not the IP, so the handshake fails and the load balancer returns error 525. Setting the endpoint Host header to a covered hostname (ps.<student-domain>) makes the SNI match the certificate, and Full (strict) validation succeeds. The weight field is required (0.00-1.00); leave it at 1 for now (you will change it in Lab 8).
Verify the pool health:
  1. On Traffic > Load Balancing, look at the endpoints (pools) list.
  2. Confirm both primary-pool and secondary-pool show a green Healthy status. (Health can take up to a minute to populate after you attach the monitor.)
Both pools must read Healthy before you build the load balancer, so you know the monitor is reaching the origins.
Troubleshooting: a pool reads Healthy but the load balancer returns 522. A 522 is an edge-to-origin connection timeout: Cloudflare opened the client TLS session but could not connect to the endpoint. It is confusing because the pool can stay green at the same time. The reason is that health and live traffic do not use the same port:
  • The health monitor probes each endpoint on the monitor's own Port (here 80), which is independent of the endpoint's Port. A green pool only proves the monitor's port is reachable, not that the endpoint Port is correct.
  • If an endpoint's Port does not match the port the origin actually serves (for example a typo of 79 instead of 80), the monitor keeps succeeding on 80 and reports Healthy, while live requests are sent to the wrong port and time out with 522. With Off (Failover) steering this pins every request to that broken primary, so you get a constant 522 under a green dashboard.
  • Fix: edit the pool endpoint and set Port to the port the origin serves (80 in this lab), matching the other endpoint. When you see a 522 while both pools show Healthy, check the endpoint Port first.

7.2 Implementing Failover

On Traffic > Load Balancing, click Create load balancer. Name it lb.<student-domain>. In the Pools step add primary-pool first and secondary-pool second (order is the failover order); set the fallback pool to secondary-pool. In the Traffic Steering step choose Off (Failover). Finish the wizard and save.

Why: The load balancer is the intelligent traffic cop sitting on the edge. Off (Failover) steering sends 100% of traffic to the first healthy pool in the list, only shifting to the next pool if the one above it goes unhealthy.
Verify the baseline (traffic on primary): from your workstation run this and confirm the response is {"status":"healthy","origin":"primary"}:
curl -s https://lb.<student-domain>/status

Now simulate a failure of Origin A. SSH to Origin A and stop nginx:

ssh cloudflare@<origin-a-ip>
sudo systemctl stop nginx
Why: Stopping the web server makes the primary pool's health check fail, which is exactly the outage the failover policy is designed to survive.
Verify the failover:
  1. From your workstation, poll the load balancer for a few cycles:
    for i in $(seq 1 6); do curl -s https://lb.<student-domain>/status; echo; sleep 6; done
  2. Within a health-check cycle the response flips to {"status":"healthy","origin":"backup"}: traffic is now served entirely by Origin B.
  3. Optionally, in the dashboard endpoints list confirm primary-pool now shows Unhealthy (Critical) while secondary-pool stays Healthy.
Troubleshooting: the poll still returns "origin":"primary" and never flips to "backup". If Origin A is stopped and primary-pool shows Critical in the dashboard, but /status keeps returning "primary", the request is being answered from the edge cache and never reaches the load balancer, so failover is invisible. Confirm it is a cache hit:
curl -sSI https://lb.<student-domain>/status | grep -i cf-cache-status
A cf-cache-status: HIT (with a climbing age) is the giveaway. The cause is the Lab 5 Cache Everything rule matching /status. Add the Bypass cache rule for http.request.uri.path eq "/status" from Lab 5 (kept below Cache Everything) and purge the cached /status object once, then re-run the failover poll: the response now flips to "origin":"backup" within a health-check cycle.

Restore the primary: SSH back to Origin A and start nginx again:

ssh cloudflare@<origin-a-ip>
sudo systemctl start nginx

Poll /status again and confirm the response returns to "origin":"primary" once the primary pool is healthy:

curl -s https://lb.<student-domain>/status
Why: This validates that end users experience no downtime during a server crash, and that traffic automatically returns to the primary once it recovers.

Lab 8 - Advanced Health Checks & Steering

A server that returns 200 but serves a broken page is worse than one that is clearly down. You build a content-aware health check that catches these "zombie" origins and steer traffic across pools by weight.
AcmeCorp Scenario: A server can answer with HTTP 200 while serving a completely broken page. You will build a content-aware health check that detects these "zombie" origins, and distribute traffic across pools using weighted steering.

8.1 Troubleshooting with Advanced Health Checks

A basic HTTP check only proves the web server answered with a 200. A "zombie" origin can return 200 OK while the application behind it is broken. To catch that, create a monitor that also inspects the response body. On Traffic > Load Balancing > Manage Monitors, click Create new monitor, name it lab5-status-body-monitor, set Type HTTP, Path /status, Port 80, then expand Advanced health check settings and set Response Body to the substring "status":"healthy".

Why: The Response Body field is a case-insensitive substring that must appear in the response, or the endpoint is marked unhealthy. This proves the underlying application is actually functioning, not just that the web server process is up.

Edit primary-pool and change its Monitor from lab5-http-root-monitor to lab5-status-body-monitor, then save. Confirm primary-pool stays Healthy (Origin A's /status still contains "status":"healthy").

Why: Swapping the monitor lets you compare a basic check against a content-aware check on the same origin.

Now simulate a zombie: keep nginx running (so a basic 200 check would still pass) but make /status return the wrong body. SSH to Origin A and flip the status body to degraded, then reload nginx:

ssh cloudflare@<origin-a-ip>
sudo cp /etc/nginx/sites-available/acmecorp /etc/nginx/sites-available/acmecorp.bak
sudo sed -i 's/"status":"healthy","origin":"primary"/"status":"degraded","origin":"primary"/' /etc/nginx/sites-available/acmecorp
sudo nginx -t && sudo systemctl reload nginx
Why: The origin still answers with HTTP 200, so the old basic monitor would keep the pool "healthy" and keep sending users to a broken app. The content-aware monitor sees that "status":"healthy" is missing and marks it down.
Verify the zombie is caught:
  1. Confirm the origin is a zombie (200 but wrong body). SSH on Origin A, or from the workstation check the status through the primary path.
  2. Poll the load balancer:
    for i in $(seq 1 8); do curl -s https://lb.<student-domain>/status; echo; sleep 8; done
  3. The body-content check fails, primary-pool goes Unhealthy, and the response flips to {"status":"healthy","origin":"backup"} even though Origin A never stopped serving 200s.

Revert the change so the primary recovers: SSH to Origin A, restore the backup, and reload nginx:

sudo mv /etc/nginx/sites-available/acmecorp.bak /etc/nginx/sites-available/acmecorp
sudo nginx -t && sudo systemctl reload nginx
Why: Advanced health checks prevent zombie servers (returning 200 OK but a broken body) from receiving traffic. Restoring the healthy body returns the primary pool to service.

8.2 Weighted steering and origin weights

Note on Geo-steering: True Geo-steering (mapping regions to specific pools) and dynamic latency (Proximity) steering require a higher Load Balancing subscription tier and are not selectable on this lab's plan. The concept is the same as weighted steering below: you influence which pool serves a request. Here you will demonstrate the mechanism you do have on this tier, weighted Random steering, which splits traffic across pools by a configured ratio.

Edit the load balancer and change the Traffic Steering policy from Off (Failover) to Random. Then set the pool weights so the primary receives roughly 60% and the secondary roughly 40% of traffic: weight primary-pool at 0.6 and secondary-pool at 0.4. Save.

Why: Weights let you send a specific percentage of traffic to each pool. This is critical when one origin is smaller and cannot handle an even share of global load. With Random steering the load balancer picks a pool per request in proportion to its weight.
Verify the weighted split:
  1. From your workstation, send a batch of requests and count how many each origin served:
    for i in $(seq 1 250); do curl -s https://lb.<student-domain>/status; echo; done | grep -o 'primary\|backup' | sort | uniq -c
  2. You should see both primary and backup counted, roughly in a 60/40 ratio. Use a large sample: random steering is per-request, so small batches swing widely (for example 40 requests, or even 120, can land near 50/50), but over ~250 requests the split converges on the configured weights.
When you are done experimenting, you can switch the steering policy back to Off (Failover) if you want the load balancer to end the lab in an active-passive posture.

8.3 Session Affinity (Sticky Sessions)

Weighted Random steering picks a pool per request, so the same visitor can bounce between Origin A and Origin B on consecutive clicks. For a stateful flow (a login session or a shopping cart held on one origin) that bouncing breaks the experience. Session Affinity fixes it by pinning each client to one endpoint for the life of a cookie.

With the load balancer still on Random steering, edit it and turn on affinity:

Why: Session Affinity generates a cookie that records which endpoint a client was assigned to, so every later request from that client returns to the same endpoint as long as it is healthy. This preserves per-user session state (cart contents, login) that would otherwise be lost when Random steering spreads requests across origins.
Verify the stickiness: from the workstation, make one request that saves the affinity cookie, then reuse that cookie for a batch and confirm every response comes from the same origin (no 60/40 split this time):
curl -s -c /tmp/aff.txt https://lb.<student-domain>/status; echo
for i in $(seq 1 10); do curl -s -b /tmp/aff.txt https://lb.<student-domain>/status; echo; done | grep -o 'primary\|backup' | sort | uniq -c
All 10 requests report a single origin (for example 10 primary), proving the client stuck to one endpoint. A fresh client with no cookie would again be split by weight.

Lab 9 - WAF Managed Rulesets

Now you start closing the holes from Chapter 1. You deploy Cloudflare's managed rulesets (the Cloudflare Managed Ruleset and OWASP) to virtually patch the leaked source-control file and the login SQL injection, tune out a false positive, and read Security Analytics to tell an attacker from a customer.
AcmeCorp Scenario: In Lab 1 you pulled /.git/secrets.txt straight off the origin and found the login page vulnerable to SQL injection. Because you cannot patch the code immediately, you will deploy Cloudflare's managed rulesets to "virtually patch" the server, and whitelist the internal security scanner to avoid false positives.

9.1 Deploy the WAF and understand OWASP

First, from the workstation, confirm an exposure from Chapter 1 is still live through Cloudflare: the origin still serves its leaked source-control file.

ssh cloudflare@<workstation-ip>
curl -s -o /dev/null -w "%{http_code}\n" https://ps.<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.

Retest the exposed file through Cloudflare:

curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/.git/secrets.txt
Verify (virtually patched): the request now returns 403 and the Cloudflare block page, even though the origin would still serve it. Confirm a matching Block event under Managed rules in Security > Analytics > Events.

Now add the OWASP ruleset to cover the login SQL injection. In the Managed rules section, click Deploy managed ruleset again and select the Cloudflare OWASP Core Ruleset.

Why: OWASP provides industry-standard protection against the Top 10 vulnerabilities, including SQLi and XSS. Together with the Cloudflare Managed Ruleset it gives AcmeCorp both curated known-bad blocking and score-based anomaly detection.

On the OWASP ruleset configuration page, leave Anomaly Score Threshold at Medium (40) and Paranoia Level at PL1, set the Action to Log, then click Deploy.

Why: OWASP uses anomaly scoring: every matching rule adds points, and the ruleset only acts once the total crosses the threshold. Starting in Log mode lets you observe impact before you block anything, so you do not break legitimate traffic. PL1 keeps false positives low.
How the two rulesets interact: The Cloudflare Managed Ruleset runs with its own default Block action, so a complete SQL-injection string like the one below is blocked outright (403) before the OWASP ruleset ever scores it. OWASP is score-based and you deployed it in Log: weak single-token payloads (for example ?username=admin') do not reach the Medium threshold and generate nothing, and full payloads that the curated Managed Ruleset already catches are blocked before OWASP can log them. OWASP's Log mode gives you visibility into anomaly scoring for the patterns the Managed Ruleset does not block.

From the workstation, SSH in and simulate a SQL injection against the login page. The quotes and spaces are URL-encoded so the full payload is preserved intact:

ssh cloudflare@<workstation-ip>
curl -s -o /dev/null -w "%{http_code}\n" "https://ps.<student-domain>/login?username=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Why: This mimics an attacker breaking the database query logic via the login parameter. Running it from the workstation over SSH matches how AcmeCorp's internal tooling reaches the site (never from your operator machine).
Verify the login SQLi is virtually patched: the command returns 403 and the Cloudflare block page. Go to Security > Analytics, open the Events tab, and within about 20 seconds a new event appears for path /login with Action = Block and Service = Managed rules. Click it to expand the full detail (Ray ID, matched rule, anomaly score): the match is in the Cloudflare Managed Ruleset, which blocks this SQL-injection pattern on its own default action before the OWASP ruleset (running in Log) scores it. The Lab 1 login injection now gets no further than the edge.

9.2 Dealing with False Positives

Part A - trust the scanner with an IP Access Rule. AcmeCorp's internal vulnerability scanner runs from the workstation and keeps tripping the WAF. Go to Security > Security rules, click Create rule > IP access rules. Set the Value to your workstation IP (<workstation-ip>), Action to Allow, scope to This website, add the note "Vulnerability Scanner", and deploy.

Why: An IP Access Rule with Allow bypasses WAF inspection entirely for trusted sources, silencing the scanner's false positives.

Re-run the same SQLi from the workstation (the same request from 9.1):

curl -s -o /dev/null -w "%{http_code}\n" "https://ps.<student-domain>/login?username=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Verify the bypass: the command now returns 200 instead of 403: the Allow rule skipped WAF inspection so the request reached the origin. In Security > Analytics > Events there is no new Block for /login. You may instead see an Allow event attributed to IP access rules, which is the allowlist matching, not a managed-rule block.

Important - remove this rule before continuing. Delete the "Vulnerability Scanner" IP Access Rule now. If you leave it in place, all workstation traffic bypasses the WAF, and the rest of Lab 9 (and the WAF labs that follow) will record no events. Go to Security > Security rules, set the Type filter to IP access rules, open the rule's Menu, and choose Delete.

Part B - a surgical path exception. The contact form at /contact/ accepts free-text that can look like an injection payload, so legitimate submissions trip the WAF's SQL-injection rules. Instead of allowlisting an IP, you will skip managed rules for just that one path. First reproduce the false positive from the workstation:

curl -s -o /dev/null -w "%{http_code}\n" "https://ps.<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.

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

Why the order is critical: A skip exception only skips rules that run after it, and managed rules execute top-to-bottom by order. The Place at field defaults to Last, which puts the exception below the managed rulesets: they run first and block the request before the skip ever takes effect, so the exception appears to do nothing. Placing it First (above the rulesets) is what makes the skip work. If you already created it at Last, you can drag the skip rule above the ruleset using the Reorder handle on the Managed rules list. Reordering can take 15-30 seconds to take effect at the edge.
Verify the exception works:
  1. From the workstation, re-send the contact-form attack several times (unique markers avoid edge de-duplication):
    for i in $(seq 1 10); do curl -s -o /dev/null "https://ps.<student-domain>/contact/?comment=admin%27%20OR%20%271%27%3D%271%27%20--%20&n=$i"; done
  2. Send a control attack against the login page:
    curl -s -o /dev/null "https://ps.<student-domain>/login?username=admin%27%20OR%20%271%27%3D%271%27%20--%20"
  3. In Security > Analytics > Events, the /contact/ requests now show Action = Skip (rule Contact form exclusion, no new Block), while the /login control still shows Action = Block (SQLi - Comment). The exception surgically disabled managed rules on the contact path only, leaving the rest of the site protected.

9.3 Reading Analytics & Identifying Real 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 (for example, Exploit or Automated).

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

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

Why: This detail is the actionable intelligence you will turn into Custom Rules in Lab 10.
Verify (Security > Analytics > Events): With Group view by: Action you should see summary cards (Total, plus Block events from the .git virtual patch and the /login and /contact/ SQL-injection attempts, and Skip events from your contact-form exception). You will not see a separate OWASP Log card: the Cloudflare Managed Ruleset blocked these payloads on its own default action before the OWASP ruleset (running in Log) could score them. Events by service shows Managed rules as the top source. Scroll to Top events by source: because all of your test traffic came from the workstation, a single entry under Source IP Addresses (your workstation IP) accounts for every event, and Paths shows /.git/secrets.txt, /login, and /contact/. User Agents shows curl/8.5.0 and Source ASNs shows the workstation's cloud provider. This "one IP, one user agent" pattern is exactly how you distinguish a single automated actor from a distributed attack.

Lab 10 - WAF Custom Rules

Managed rules are the floor, not the ceiling. You write custom rules to block traffic by geography, add a friction challenge to the abused contact form, lock the admin panel to corporate IPs, and skip security for one trusted partner API.
AcmeCorp Scenario: Standard rules aren't enough. You must enforce sanctions by blocking geographic regions, add a friction challenge to a frequently-abused form, lock down the administrator panel to corporate IPs, and bypass security for a trusted B2B partner API.

10.1 Geo Blocking + Setting 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.

AcmeCorp must refuse traffic from a sanctioned country. Click Edit expression (top-right of the "When incoming requests match" box) to switch from the visual builder to the raw expression editor, then paste an expression that matches a single sanctioned country, for example North Korea. Under "Then take action" choose Block, leave the response as the default Cloudflare WAF block page (403), and click Deploy:

ip.src.country eq "KP"
Why: This enforces regulatory compliance by refusing requests from an embargoed country. The ip.src.country field is a two-letter ISO country code derived from the visitor's IP. Deliberately blocking a country your workstation is not in keeps your own control requests working, so you can verify the rule from a single location.

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 send traffic that appears to come "from" the sanctioned country. To prove the Block action actually fires, temporarily point the rule at your own country, test, then change it back.

First, from the workstation, find the country the edge geolocates you to:

ssh cloudflare@<workstation-ip>
curl -s https://ps.<student-domain>/cdn-cgi/trace | grep '^loc='
Verify: the output is a single line such as loc=US. That two-letter code is your country as the edge sees it; call it <your-country>. It will not be KP, which is why the sanctioned-country rule does not block you.

Now confirm that your normal traffic is allowed by the Geo Block rule:

curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/
Verify (allowed): you get 200. Your workstation is not in the sanctioned country, so the Geo Block expression does not match it and the request passes.

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

curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/
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 ip.src.country eq "KP" and Save; the curl returns 200 again. Edge propagation can take up to a minute, so if you still see the old result, wait and re-run.

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 and Log events from Competitor Monitoring (if you flip its expression to US the same way to test it). Expand an event to confirm the matched rule name, the country, and the action.

10.2 Challenge the Contact Form

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

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

Under Then take action choose Managed Challenge, and Deploy.

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://ps.<student-domain>/contact/
curl -s -D - -o /dev/null https://ps.<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. When you are done, disable this rule so it does not interfere with the contact-form tests in later labs.

10.3 Use IP Lists to Manage Access

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

Why: Lists let you manage large sets of IP addresses centrally and reference them in any rule expression as $corporate_ips, without rewriting the rule when the membership changes.
Find your workstation's public IP by running this on the workstation:
curl -s https://api.ipify.org

Now return to Security > Security rules, create a custom rule named "Allow Corp Access", click Edit expression, and enter an expression that blocks the /admin path 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 administrative panel: only source IPs on the corporate list are allowed to reach /admin; everyone else is blocked at the edge. Using "Block when NOT in the list" is the correct pattern here, because the custom-rule Allow action only skips remaining custom rules; it does not block anyone by itself.
How to read the test results: the origin protects /admin with HTTP Basic authentication, so a request that reaches the origin returns 401 Unauthorized. A request blocked by your WAF rule returns 403 Forbidden 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:

ssh cloudflare@<workstation-ip>
curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/admin
Verify (allowed): you get 401. Your workstation IP is in corporate_ips, so the rule does not match and the request reaches the origin's Basic-auth challenge.

To prove the rule blocks everyone else, go back to Manage account > Configurations > Lists > corporate_ips, select your IP, click Remove, and confirm. Wait about 15 seconds, then re-run the curl:

Verify (blocked): the same request now returns 403, because your IP is no longer on the list so not (ip.src in $corporate_ips) is true and the Block action fires. Re-add your IP to the list to restore access; the request returns 401 again.

10.4 Skip Security Features 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 the 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 API 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 and All rate limiting rules, leave Log matching requests enabled so the skips still appear in analytics, and click 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.

From the workstation, send the same malicious payload twice: once with the partner header and once without it. The with-header request is allowed straight through to the origin (200), because the Skip action turns the managed rules off for it. The without-header request is caught and blocked by the Cloudflare Managed Ruleset (403). The difference shows up both in the status code and in the security events.

ssh cloudflare@<workstation-ip>
A=public-api.<student-domain>
P="q=admin%27%20OR%20%271%27%3D%271%27%20--%20"
curl -s -o /dev/null -w "with header:    %{http_code}\n" -H "x-partner-api: true" "https://$A/v1/partners?$P"
curl -s -o /dev/null -w "without header: %{http_code}\n" "https://$A/v1/partners?$P"
Verify (Security > Analytics > Events): the request with the header appears as a Skip event (service Custom rules) and produces no Managed rules event, because the WAF was skipped. The request without the header appears as a Block event under Managed rules (the Cloudflare Managed Ruleset's SQLi - Comment rule caught and blocked the injection). This side-by-side proves the bypass works only for the trusted partner traffic.

Lab 11 - Bot Management

Competitors are scraping AcmeCorp's pricing with automated bots. You challenge low-scoring automated traffic while explicitly allowing the mobile app and known-good internal tooling.
AcmeCorp Scenario: Competitors are running automated bots to scrape AcmeCorp's pricing data, skewing analytics and stealing intellectual property. You must deploy Bot Management to challenge scrapers while explicitly allowing your mobile app.

11.1 Evaluate Bot Traffic & Create 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 on the zone.

Now 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 top-N lists.

Why: Real scrapers cluster at a low bot score and often share a user agent or ASN. Identifying those patterns tells 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 raw expression below. Set the action to Log first and Deploy.

(cf.bot_management.score lt 30) and not cf.bot_management.verified_bot
Why: Starting in Log lets you confirm the rule matches the right traffic before it starts challenging anyone. not cf.bot_management.verified_bot excludes verified crawlers such as Googlebot, so your SEO ranking is never harmed.
Tip: after clicking Edit expression, click inside the expression box and type (or paste then add a space) so the on-screen character counter leaves 0. If the counter stays at 0, Deploy fails with "Enter a valid expression".

Once the Log events look correct, edit the rule and change the action to Managed Challenge, then Deploy. Test from the workstation. A single-egress workstation using curl scores as automated, so it is your stand-in scraper:

ssh cloudflare@<workstation-ip>
D=ps.<student-domain>
curl -s -o /dev/null -w "%{http_code}\n" "https://$D/"
curl -s -D - -o /dev/null "https://$D/" | grep -i "cf-mitigated"
Verify: with the action set to Managed Challenge, the curl 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 200). In Security > Analytics > Events you will also see the Bot Management - Baseline rule firing.

11.2 Refine the Rule to Exclude Trusted Automation

Legitimate automation also scores low: the AcmeCorp mobile app calls /api/products, and a nightly internal BI tool identifies itself with the user agent B1-Bot/1.11. Both are being challenged by the baseline rule. Edit the "Bot Management - Baseline" rule, 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 "/api/products") 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 tuning loop for Bot Management: start broad, then add precise exclusions for known-good automated clients.

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

ssh cloudflare@<workstation-ip>
D=ps.<student-domain>
curl -s -L -o /dev/null -w "normal:         %{http_code}\n" "https://$D/"
curl -s -L -o /dev/null -w "/api/products:  %{http_code}\n" "https://$D/api/products"
curl -s -L -o /dev/null -w "B1-Bot UA:      %{http_code}\n" -A "B1-Bot/1.11" "https://$D/"
Verify: the normal request is still challenged (403), while /api/products and the B1-Bot/1.11 request are now allowed through to the origin (200). The exclusions work without weakening the main bot defense. (The -L flag follows any trailing-slash redirect the origin issues on a passed-through request so it reports 200; a challenged request has no redirect to follow and stays 403.)

Look closely at that normal: 403 line, because it is the lesson of this lab. Your refined expression is no longer scoped to a single path, so it now evaluates every request on the zone. You never targeted the homepage, yet your single-egress curl scores as automated and the rule challenges it. Confirm it directly:

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. 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 15 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 12 - Rate Limiting

Attackers pivot to brute force and API fuzzing. You cap login attempts per IP and throttle search-API abuse by counting the origin's own error responses.
AcmeCorp Scenario: Attackers have realized they can't easily exploit the WAF, so they are pivoting to brute-force credential stuffing against the login page, and fuzzing the API to find open endpoints. You must configure rate limits to shut down abusive volumes of traffic.

12.1 Rate Limit By IP (Brute Force Protection)

Go to Security > Security rules, scroll to the Advanced rate limiting rules section, and click Create rule. Name it "Login Page Rate Limit", click Edit expression, and match the login path:

http.request.uri.path eq "/login"
Why: Login pages are prime targets for credential stuffing. A rate limit caps how many attempts a single client can make, so an attacker cannot cycle through thousands of stolen passwords.

Under With the same characteristics leave IP selected (count per source IP). Under When rate exceeds set Requests = 5 and Period = 1 minute. Under Then take action choose Managed Challenge, keep Action for the selected duration, and set the duration to 1 minute. Click Deploy.

Why: Counting per IP means one abusive client is limited without affecting everyone else. A legitimate user will not submit the login form 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; the form rejects Deploy otherwise. In production you would raise the duration (for example 10 minutes) for a stronger penalty; we use 1 minute so the lab resets quickly for re-testing.

Test from the workstation by bursting past the threshold:

ssh cloudflare@<workstation-ip>
D=ps.<student-domain>
for i in $(seq 1 8); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/login"; done
curl -s -D - -o /dev/null "https://$D/login" | grep -i "cf-mitigated"
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. The final header check prints cf-mitigated: challenge, confirming the action is a Managed Challenge (an automated client cannot solve it, so it stays blocked for the mitigation window; a real browser would be offered the challenge instead).

12.2 Rate Limit By Origin Response Code

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

http.request.uri.path eq "/api/v1/search"
Why: Attackers fuzz search APIs with junk queries to find hidden parameters or crash the backend. Those probes overwhelmingly generate 404 responses, which is the signal we will 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 (fuzzing or scanning) 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 (rather than a challenge) is appropriate once the threshold is crossed.

Test from the workstation. The origin returns 404 for /api/v1/search, so every request feeds the counter:

ssh cloudflare@<workstation-ip>
D=ps.<student-domain>
for i in $(seq 1 25); do curl -s -o /dev/null -w "req $i -> %{http_code}\n" "https://$D/api/v1/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 13 - API Discovery & Endpoint Management

You cannot protect an API you cannot see. You set a session identifier, let API Shield discover the real API surface (including an undocumented shadow endpoint), and bring those operations under management.
AcmeCorp Scenario: AcmeCorp's public API (public-api.<student-domain>, served from Origin A on :443) is an orders service. Its documented surface is GET /status, GET /internal/accessLogs, GET /v1/orders, POST /v1/orders, and POST /order. Developers also left an undocumented "shadow" endpoint (GET /v1/trying/hidden/endpoint) exposed. You will discover the true API footprint, enforce a strict OpenAPI schema on order creation to stop payload tampering, and use sequence analytics to spot abnormal call order.

13.1 Session Identifiers, Discovery & Endpoint Management

First set a session identifier. Go to Security > Settings, open the API abuse category, click into Session identifiers (/security/settings/api-abuse/session-identifiers), click Add identifiers, set Type = Header and Name = Authorization, and Save.

Why: API Shield uses a per-client identifier (a header, cookie, or JWT claim that is 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 Authorization value per simulated user across several endpoints, including the undocumented shadow endpoint:

ssh cloudflare@<workstation-ip>
H=public-api.<student-domain>
for u in $(seq 1 20); do
  A="Authorization: Bearer user-$u-tok"
  curl -s -o /dev/null -H "$A" "https://$H/status"
  curl -s -o /dev/null -H "$A" "https://$H/v1/orders"
  curl -s -o /dev/null -H "$A" -X POST -H "Content-Type: application/json" -d "{\"createdBy\":\"user$u\",\"items\":[\"sku-$u\"]}" "https://$H/v1/orders"
  curl -s -o /dev/null -H "$A" "https://$H/v1/trying/hidden/endpoint"
done
Why: Cloudflare maps the API surface from live traffic. Hitting the undocumented endpoint is what lets Discovery surface it as a "shadow API" that no schema or documentation mentions.
Timing: Discovery (and Sequence Analytics in 10.3, 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 Discovery results the next day.

Discovered endpoints appear under Security > Web assets > Operations (Endpoint Management) via Add operations > Select from Discovery. Until Discovery populates, add the endpoints you want to govern 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, rate-limit recommendation, or authentication check.
Verify: the operation appears in Endpoint Management with its method, hostname, and path. (After ~24h, confirm the shadow endpoint /v1/trying/hidden/endpoint shows up under Add operations > Select from Discovery.)

Lab 14 - API Schema Validation & Sequence Analytics

A managed API still trusts its callers too much. You enforce the OpenAPI contract at the edge so malformed order requests are rejected, and use sequence analytics to flag calls that arrive out of their normal order.
AcmeCorp Scenario: Discovering and managing the API is only half the job. You will enforce the OpenAPI schema at the edge so malformed order requests never reach the origin, and use Sequence Analytics to detect abnormal call ordering.

14.1 Schema Validation & Endpoint Rate Limiting

The API publishes its own OpenAPI schema at /openapi.json. Fetch it and point its servers URL at your zone's API hostname (this is required so Cloudflare maps the operations to your host, not the schema's placeholder host):

ssh cloudflare@<workstation-ip>
H=public-api.<student-domain>
curl -s "https://$H/openapi.json" \
  | sed -E "s#\"url\": *\"https://[^\"]*\"#\"url\": \"https://$H\"#" \
  > acmecorp-openapi.json
head -n 500 acmecorp-openapi.json
Why: Cloudflare derives the hostname for each imported operation from the schema's servers block. If it still points at the sample host, the uploaded operations will not match your live traffic and validation never fires.

Copy that file to the machine running your browser, then go to Security > Web assets > Schema validation, click Upload schema, select the file, review that the endpoints map to public-api.<student-domain>, and click Add schema and endpoints.

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.

Now enforce it. In the Schema validation list, tick the POST /v1/orders row, click Change action…, choose Block, and Set action.

Why: With Block, any request that violates the CreateOrderRequest schema (missing createdBy/items, wrong type, or an extra field since the schema sets additionalProperties: false) is dropped at the edge before it reaches the origin.

Test from the workstation, one compliant request and three violations:

ssh cloudflare@<workstation-ip>
H=public-api.<student-domain>
CT="Content-Type: application/json"
curl -s -o /dev/null -w "valid:            %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"sku1\"]}" "https://$H/v1/orders"
curl -s -o /dev/null -w "extra field:      %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\",\"items\":[\"sku1\"],\"evil\":\"x\"}" "https://$H/v1/orders"
curl -s -o /dev/null -w "missing items:    %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":\"alice\"}" "https://$H/v1/orders"
curl -s -o /dev/null -w "wrong type:       %{http_code}\n" -X POST -H "$CT" -d "{\"createdBy\":123,\"items\":[\"sku1\"]}" "https://$H/v1/orders"
Verify: the compliant request returns 201 (it reaches the origin and creates the order); all three violations return 403. The 403 is Cloudflare's schema-validation block at the edge, and it is distinct from the origin's own 400 for bad input, so seeing 403 (not 400) proves the request never reached the backend.
Timing: schema validation takes about a minute to propagate to the edge after you click Set action. If you run the test immediately you may see the violations slip through to the origin instead (extra field returns 201 because this origin accepts unknown fields, and missing items / wrong type return the origin's own 400). That pattern of 201/201/400/400 means the rule has not propagated yet, not that you misconfigured it: wait about a minute and re-run, and the three violations flip to 403.

Endpoint rate limiting. Open the operation from Endpoint Management (click the /v1/orders row) to reach Operation Details. Once session identifiers (13.1) and enough traffic exist, a Rate limiting recommendation is generated here that you can accept to create an endpoint-scoped rate-limiting rule. You can also build one manually with the Lab 12 flow, scoping the match to the operation, for example http.request.uri.path eq "/v1/orders" and http.request.method eq "POST".

Why: A per-endpoint limit is surgical: it protects the single heaviest or most sensitive operation without throttling the rest of the API.
Timing: the rate-limiting recommendation only appears after session identifiers plus accumulated traffic (up to ~24h). If it is not yet present, use the manual Lab 12 rate-limiting rule above.

14.2 Sequence Analytics & Mitigation

With session identifiers configured (13.1), generate traffic that follows the intended order, plus some out-of-order calls, from the workstation:

ssh cloudflare@<workstation-ip>
H=public-api.<student-domain>
CT="Content-Type: application/json"
# intended flow per user: check status, list orders, then create an order
for u in $(seq 1 10); do
  A="Authorization: Bearer seq-$u"
  curl -s -o /dev/null -H "$A" "https://$H/status"
  curl -s -o /dev/null -H "$A" "https://$H/v1/orders"
  curl -s -o /dev/null -H "$A" -X POST -H "$CT" -d "{\"createdBy\":\"seq$u\",\"items\":[\"a\"]}" "https://$H/v1/orders"
done
# abnormal: create an order with no preceding GETs
for u in $(seq 11 15); do
  curl -s -o /dev/null -H "Authorization: Bearer bad-$u" -X POST -H "$CT" -d "{\"createdBy\":\"bad$u\",\"items\":[\"a\"]}" "https://$H/v1/orders"
done
Why: Sequence Analytics groups requests by session identifier and highlights the common ordered "journeys" through the API. Attackers frequently jump straight to a sensitive action (creating an order) without the normal preceding steps.

Review the results under Security > Web assets > Sequences. When a risky sequence is identified, mitigate it with a custom rule (Security > Security rules > Custom rules) that references the API Shield sequence, or as a simpler stand-in, gate the sensitive operation behind a required precondition (such as a valid session token or a preceding step) so out-of-order requests are challenged or blocked.

Timing / scope: Sequence Analytics needs session identifiers plus accumulated traffic and can take up to 24 hours to populate, so it is reviewed on a later visit rather than immediately. The Sequences tab lists your API hostname and begins building journeys once traffic with identifiers is seen.

Lab 15 - 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.
AcmeCorp Scenario: As a final hardening step, you will preemptively block known bad actors from the internet using Cloudflare's threat intelligence, ensuring the AcmeCorp servers are entirely shielded from automated attack networks.

15.1 Using Managed Lists

Go to Security > Security rules, click Create rule > Custom rules (/security/security-rules/custom-rules/create), and name it "Block Malicious IPs by Managed List".

Why: Cloudflare curates and continuously updates lists of actively malicious and anonymizing IP addresses (Managed IP Lists) so you do not have to build or maintain blocklists by hand.

Build the match with the expression editor. Set Field = IP Source Address and Operator = is in list, then in Value choose the managed list Open Proxies. Click Or and add a second condition for the Anonymizers managed list. The Expression Preview should read:

(ip.src in $cf.open_proxies) or (ip.src in $cf.anonymizer)
Why: Attackers hide behind Tor and open proxies. AcmeCorp policy dictates that legitimate customers do not use these anonymizing networks. The Anonymizers list already includes Tor exit nodes, so there is no separate "Tor" list to select.
Available managed lists: Anonymizers, Botnets/Command and Control Servers, Malware, Open Proxies, and VPNs. Some managed lists are gated by plan or add-on; if a list is missing from the Value dropdown, it is not enabled for this zone.

Set Action to Block and Deploy.

Why: This provides a large, zero-maintenance reduction in background noise and scanning attacks hitting the origin servers.
Verify: the rule shows as Active in the Custom rules list. Legitimate traffic from the workstation still returns 200 (the workstation is not on any managed list), so there is no false positive:
ssh cloudflare@<workstation-ip>
curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/
Proving the block (optional): you cannot easily originate a request from a Tor or open-proxy IP, so to see the Block action fire, temporarily append or (ip.src eq <workstation-ip>) to the expression and re-test: the workstation now returns 403. Remove that clause afterward to restore the managed-lists-only rule. In production, real anonymizer traffic that is blocked appears over time under Security > Analytics.

Lab 16 - Spectrum for TCP/UDP (Concept)

Not everything AcmeCorp runs speaks HTTP. Conceptually, you extend Cloudflare's DDoS protection and origin masking to raw TCP and UDP services such as SSH. This chapter is a walkthrough only; nothing is provisioned.
AcmeCorp Scenario: AcmeCorp does not only run web applications. Their operations team relies on non-HTTP services (SSH to jump hosts, a game/telemetry backend, an SMTP relay) that sit on raw TCP and UDP ports and are therefore not protected by the WAF, which only understands HTTP. Spectrum extends Cloudflare's DDoS protection, IP masking, and edge acceleration to those arbitrary TCP/UDP applications.
Why this lab is read-only: Spectrum is a paid, contract-gated add-on. On a zone without a Spectrum entitlement the dashboard shows a trial banner ("Spectrum is a paid feature, and your Account Manager will follow up"), and creating an application provisions a billable resource. This lab therefore walks through the configuration conceptually so you understand the workflow, without provisioning an application in the shared lab environment.

16.1 Protecting TCP/UDP Applications

Spectrum lives at (domain) > Spectrum (/spectrum). The page lists any existing applications and offers Create an Application.

Why not the WAF: WAF, rate limiting, and API Shield operate at Layer 7 (HTTP). A service like SSH (TCP 22) or a UDP game server never speaks HTTP, so those tools cannot see or protect it. Spectrum proxies the raw TCP/UDP connection through Cloudflare's edge, hiding the origin IP and absorbing L3/L4 DDoS, while optionally terminating TLS.

The Create an Application dialog collects, in order:

Application Type: the protocol carried edge-to-origin (for example TCP or UDP).
Domain: a subdomain on your zone (for example ssh.<student-domain>) that becomes the public entry point; Cloudflare creates the matching DNS record.
Edge IP Connectivity: whether the edge provisions IPv4 (A record), IPv6 (AAAA), or both.
Edge Port: the port Cloudflare listens on at the edge (a single port, or a range such as 22-23; ranges must match between edge and origin).
Origin: where connections are proxied. This can be an Origin IP or DNS record, a Load Balancer, or a Virtual Network.
Edge TLS Termination, IP Access Rules, and Proxy Protocols: optional TLS offload at the edge, allow/block lists for who may connect, and PROXY-protocol headers so the origin still sees the real client IP.

A representative configuration for AcmeCorp would front their SSH jump host: Application Type TCP, Domain ssh.<student-domain>, Edge Port 22, Origin the jump host's IP on port 22. Users would then connect to ssh.<student-domain> and reach the origin through Cloudflare, with the origin IP never exposed.

No hands-on step: do not create an application in this lab environment. If your account carries a Spectrum entitlement, the flow above is exactly what you would follow; the result is a proxied hostname whose origin IP is hidden behind Cloudflare's anycast edge.

Lab 17 - 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, connection, and cookie, spot an injected malicious script, then use a Content Security Policy allowlist to close the supply-chain gap.
AcmeCorp Scenario: The AcmeCorp site loads JavaScript in customers' browsers. A supply-chain attack (a compromised third-party script or an injected skimmer) executes entirely client-side, where server-side controls like the WAF never see it. Page Shield gives AcmeCorp visibility into every script, connection, and cookie running in the browser, and lets them enforce an allowlist via Content Security Policy (CSP). This lab uses the demo page ps.<student-domain>/contact/received/, which loads several third-party resources including a malicious sample.

17.1 Client-side Resource Monitoring

Enable monitoring. Go to Security > Settings, open the Client side abuse category, and turn on Continuous script monitoring (/security/settings?tabs=client-side-abuse).

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 (injected malware or skimming scripts). On this Enterprise zone it is available at no additional cost.
Verify: the toggle stays on. The setting is now active for the zone (visible under the same category).

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 (/security/web-assets/monitor), 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 and shows resources seen repeatedly over the previous days, so it is not instant. Load the demo page a few times and revisit the tab later.

17.2 Content Security Rules (CSP)

Monitoring tells you what is running; a Content security rule lets you enforce what is allowed to run. Go to Security > Security rules, find Content security rules, and click Create rule (/security/security-rules/client-side-rules/create). 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 (Log) or blocked (Allow/enforce). 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 17.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, change the action to Allow to enforce the allowlist.

Why start with Log: The Action choices are Allow (enforce the allowlist, blocking anything not permitted) and Log (report-only: send violation reports without blocking). Starting in Log lets you observe what the policy would catch before you risk breaking a legitimate script by enforcing it. With this allowlist, the injected cryptomining.testcategory.com script and the hookb.in / cf-malicious-test.domain.example.com connections fall outside the policy and are reported as violations.

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

ssh cloudflare@<workstation-ip>
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; switching the action to Allow sends the enforcing content-security-policy header instead. A resource outside the allowlist (the cryptomining script, the hookb.in beacon) is reported or blocked accordingly.
Timing: like the inventory, CSP violation reports accumulate from real browser traffic, so the violations view populates over time rather than immediately.

End of Unified Lab Guide.

Lab 18 - Seal & Validate the Perimeter

The perimeter only matters if it cannot be bypassed. You seal the origin so it accepts traffic only from Cloudflare, then replay every attack from Chapter 1 and watch each one fail. AcmeCorp is finally defended.
AcmeCorp Scenario: You have built a full edge perimeter: DNS and proxy, encryption, caching, load balancing, WAF, bot management, rate limiting, API Shield, threat intelligence, and client-side protection. But a perimeter only matters if attackers cannot walk around it. The origin IPs are still reachable directly, which means every control you built can be bypassed by talking to the origin instead of Cloudflare. In this final chapter you seal the origin so it only accepts Cloudflare traffic, replay the Lab 1 attack suite to prove the "before" attacks no longer work, and recap the architecture you built.

18.1 Seal the Origin

Proxying through Cloudflare hides the origin IPs, but it does not by itself stop someone who already knows an origin IP (as you did in Lab 1) from connecting directly and skipping every edge control. Closing that gap requires the origin to reject any request that did not come from Cloudflare. There are two standard ways to do it.

Concept - this step changes the origin, not just the dashboard. Sealing the origin is an origin-side or network-side change, so it is presented here as the design you would apply. Two approaches:
  • Authenticated Origin Pulls (mTLS): Cloudflare presents a client certificate on every origin request, and the origin's web server is configured to require and verify that certificate. Requests without it (direct-to-IP attacks) are refused at the TLS layer. You enable Authenticated Origin Pulls in your zone's SSL/TLS settings, and configure the web server (for example nginx ssl_client_certificate + ssl_verify_client on) to enforce it.
  • Network IP allowlist: restrict the origin's firewall or cloud security group so that only Cloudflare's published IP ranges may reach ports 80 and 443. Everything else is dropped before it reaches the web server.
Either way, the result is the same: the only path to the origin is through Cloudflare, so the perimeter you built can no longer be bypassed.
Why: Every control in Labs 9 to 17 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.

18.2 Replay the Lab 1 Attacks

Now rerun the Lab 1 attacks, but this time through the Cloudflare hostname (ps.<student-domain>) instead of the raw IP, and compare the results to the baseline you recorded.

Brute force, now rate limited. Repeat the login brute force through Cloudflare:

ssh cloudflare@<workstation-ip>
for i in $(seq 1 8); do curl -s -o /dev/null -w "%{http_code}\n" https://ps.<student-domain>/login; done
Verify (throttled): the first few requests return 200, then the rate limit from Lab 12 kicks in and further requests return 429. In Lab 1 all attempts succeeded; now the brute force is capped.

SQL injection, now seen by the WAF. Replay the injection through Cloudflare:

curl -s -o /dev/null -w "%{http_code}\n" "https://ps.<student-domain>/login?username=admin%27%20OR%20%271%27%3D%271%27%20--%20"
Wait about a minute first. This payload also targets /login, which you just rate limited with the brute force above. If you run it immediately it returns 429 (the Lab 12 rate limit fires before the WAF is evaluated), which masks the WAF result. Give the per-IP counter about a minute to reset, then run the injection so you see the WAF's response rather than the rate limit's.
Verify (inspected): the OWASP ruleset from Lab 9 records the injection as a security event under Security > Analytics > Events. If you set that ruleset's action to Block, the same request returns 403 instead of reaching the origin. In Lab 1 the identical payload sailed through to the application unseen.

Direct-to-IP, now refused. Finally, try the Lab 1 direct-IP request again:

curl -sI --max-time 5 http://<origin-a-ip>/ | head -n 3
Verify (sealed): once the origin is sealed per 18.1, this direct request no longer succeeds (connection refused, timeout, or a TLS handshake failure) instead of the 200 you saw in Lab 1. The only way in is through Cloudflare. Note: if origin sealing has not been applied in your lab environment, this request may still return 200; that is exactly the gap 18.1 closes.

18.3 Architecture Recap

You took AcmeCorp from wide open to sealed and validated. 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 IPs so attackers cannot target them directly.
SSL/TLS encryption3Ends plaintext and certificate warnings with Full (Strict) end-to-end encryption.
Caching & performance4, 5Absorbs traffic spikes at the edge so the origin stays up.
Load balancing & health7, 8Removes the single point of failure and routes around broken origins.
Traffic & rules engine6Runs business and hardening logic at the edge, off the origin.
WAF (managed + custom)9, 10Virtually patches the SQL injection and enforces access policy.
Bot management11Challenges the scrapers while allowing trusted automation.
Rate limiting12Caps the login brute force and API fuzzing.
API Shield13, 14Discovers the API, enforces its schema, and flags abnormal sequences.
Threat intelligence15Preemptively blocks anonymizers and open proxies.
Page Shield17Watches the browser for supply-chain and skimming attacks.
Sealed origin18Forces all traffic through Cloudflare so nothing above can be bypassed.
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 18). Building outward from routing to encryption to performance to security, then sealing the origin last, is what makes the perimeter hold.