Lab 04 — Domain Verification
Duration: 30 minutes
This lab has no bench component. The Mango is not involved. Everything runs
on the operator console (the Debian shell inside your Dev Container or
Codespace) using the wrangler CLI that was pre-installed there.
The goal is to confirm that three things are ready before Day 2:
- Your Cloudflare account is authenticated from the operator console (
wrangler login). - Your workshop subdomain under
eplabs.cloud(or your own domain) has the DNS records the later labs expect: a wildcard proxiedArecord for Worker routing and a non-proxiedtest.record for DNS-control verification. - The Cloudflare services that Day 2 deploys (D1, KV, R2, Access) are available under your account.
Labs 06 (Cloudflare Tunnel) and 07 (First Worker) deploy services bound to your subdomain. Lab 08 (Cloudflare Access) gates those services with OTP-enforced access. Getting the domain baseline right here prevents half a dozen hard-to-diagnose failures later.
Subdomain assignment. Each student receives a per-student subdomain under
eplabs.cloud, provisioned by the instructor before the workshop perinstructor/dns_delegation_setup_guide.md. Your subdomain is printed on your student assignment card. If you are using your own domain that is already active in Cloudflare, substitute it wherevereplabs.cloudappears.
TLS coverage for nested subdomains. This workshop runs against a shared parent zone (
eplabs.cloud) with Cloudflare Advanced Certificate Manager + Total TLS enabled, so per-hostname certificates are auto-issued for every depth —<slot>.eplabs.cloud,api.<slot>.eplabs.cloud, anything else under your slot. You do not need a delegated zone; the records on your slot inherit the parent zone’s ACM coverage. (For the take-home BYO-domain track inhandouts/workshop_domain_prerequisites.md, you provision your own root zone and rely on its own free Universal SSL.)
Where each step runs
This lab is pure cloud. Every command runs inside the operator console.
| Step | Operator console |
|---|---|
| 1. Authenticate wrangler | yes |
| 2. Verify subdomain resolves | yes |
| 3. Create required DNS records | yes (via dashboard or wrangler API) |
| 4. Confirm entitlements | yes |
| 5. Smoke-test CF proxy | yes |
| Validation | yes |
Learning objectives
- Authenticate
wranglerfrom the operator console and verify account identity. - Confirm DNS delegation for the workshop subdomain is active.
- Create the wildcard proxied
Arecord and thetest.DNS-only record thatvalidate.shchecks. - Verify that D1, KV, R2, and Cloudflare Access are accessible in the account.
- Confirm the CF proxy is live by checking the HTTP response code from the subdomain.
Pre-state
Before starting, confirm all of the following in the operator console:
# wrangler is present (pre-installed in the Dev Container / Codespace):
wrangler --version
# dig is present (pre-installed; used in Steps 2 and 3):
command -v dig
# curl is present:
command -v curl
If wrangler is missing, it means the Dev Container image did not build correctly.
Rebuild the container (Dev Containers: Rebuild Container in VS Code or delete
and re-create the Codespace) before continuing.
You also need:
- Your workshop subdomain (from your student assignment card), for example
a00f3f13.eplabs.cloud, OR your own domain already added to Cloudflare. - Access to the Cloudflare dashboard at dash.cloudflare.com from a browser (for the DNS record creation in Step 3).
Walkthrough
wrangler login opens a browser OAuth flow. In the Dev Container, the browser
does not open automatically. Copy the URL that wrangler prints to the terminal,
open it in the browser on your laptop, and complete the OAuth grant. The CLI will
detect the callback.
# Run this in the operator console terminal:
wrangler login
Expected output after successful authentication:
Successfully logged in.
Confirm the account is visible:
wrangler whoami
# Expected: prints your Cloudflare account name and email address.
Codespace users: port forwarding handles the OAuth callback automatically in most cases. If the browser does not redirect back, look for a “Open in browser” prompt in the VS Code port-forward notification, or copy the printed URL manually.
API token alternative. If OAuth is blocked (corporate proxy, headless environment), use a scoped API token instead:
# Generate a token at https://dash.cloudflare.com/profile/api-tokens
# with "Edit Cloudflare Workers" template plus D1, KV, R2 read permissions.
export CLOUDFLARE_API_TOKEN="your-token-here"
wrangler whoami # should succeed without running wrangler login
Your workshop subdomain is delegated to Cloudflare’s nameservers by the instructor-side zone configuration. This step confirms the delegation is active before you add records to it.
# Set your domain once; used in all subsequent commands:
export DOMAIN="a00f3f13.eplabs.cloud" # substitute your actual subdomain
# Query the NS records for your subdomain via Cloudflare's resolver:
dig NS "$DOMAIN" @1.1.1.1
# Expected: two NS records in the ANSWER section pointing to
# NS1.CLOUDFLARE.COM and NS2.CLOUDFLARE.COM (or the delegated sub-zone NS).
# If the ANSWER section is empty, the delegation is not yet active.
# Contact the instructor.
# Confirm the zone is active in the CF dashboard:
wrangler zones list 2>/dev/null || true
# If wrangler zones list isn't available in the installed version, check
# the dashboard directly: the zone should show "Active" status.
Own domain users. If your domain was already active in Cloudflare before the workshop, the NS check will return your registrar’s NS records pointing to Cloudflare. This is correct. Proceed.
Three records are needed. Create all three in the Cloudflare dashboard
(dash.cloudflare.com) under your domain’s DNS
settings. The first two cover anything beneath the zone (test.<domain>,
api.<domain>, etc.); the third covers the bare apex <domain> itself,
because RFC 1034 wildcards do not match the zone apex.
Record 1: wildcard proxied A record (for Worker routing)
| Field | Value |
|---|---|
| Type | A |
| Name | * |
| Content | 192.0.2.1 |
| Proxy status | Proxied (orange cloud) |
| TTL | Auto |
The 192.0.2.1 address is from the RFC 5737 documentation range. Cloudflare’s
proxy layer intercepts all traffic destined for *.yourdomain before it ever
reaches that IP. The address is a placeholder; the actual origin is your Worker
or Tunnel, configured in later labs.
Record 2: test. DNS-only A record (for DNS control verification)
| Field | Value |
|---|---|
| Type | A |
| Name | test |
| Content | 1.2.3.4 |
| Proxy status | DNS only (gray cloud) |
| TTL | Auto |
This record is a no-op for traffic; it exists so validate.sh can confirm you
have write access to the zone and that DNS resolution is working end-to-end.
Record 3: apex proxied A record (so <domain> itself resolves)
| Field | Value |
|---|---|
| Type | A |
| Name | @ (apex — your dashboard may show this as the bare zone name) |
| Content | 192.0.2.1 |
| Proxy status | Proxied (orange cloud) |
| TTL | Auto |
The wildcard from Record 1 covers anything.<domain>, but DNS wildcards do
not match the bare apex. Lab 06 (Tunnel) and Lab 07 (Worker) will replace
the placeholder content with the real origin; for now the apex needs to
exist so curl https://<domain>/ reaches the Cloudflare proxy at all.
Verify all three records from the operator console:
# Test record (DNS only — the configured IP is what clients see):
dig +short A "test.${DOMAIN}" @1.1.1.1
# Expected: 1.2.3.4
# Wildcard record (proxied — Cloudflare answers with its own anycast IPs):
dig +short A "validate-lab04-check.${DOMAIN}" @1.1.1.1
# Expected: an IP in 104.16.0.0/13 or 172.64.0.0/13
# (e.g. 104.21.94.82 or 172.67.221.71)
# Cloudflare always intercepts proxied records and returns its anycast IPs
# regardless of what you set as Content. The 192.0.2.1 placeholder is a
# routing target CF only contacts when the record is DNS-only or the proxy
# is in passthrough mode — neither applies to a proxied wildcard.
Proxied wildcard behavior. A Proxied (orange cloud) record always resolves to one of Cloudflare’s public anycast IPs at the resolver level. The configured Content (here
192.0.2.1) is the placeholder origin CF would forward to once the proxy decides traffic is legitimate; clients never see it directly.validate.shtherefore checks that the wildcard answer is in the published CF anycast ranges (104.16/13, 172.64/13). Seeing192.0.2.1indigoutput means the record is DNS-only (gray cloud) and needs to be flipped to Proxied.
Day 2 deploys the Worker (Lab 07), binds it to D1 and KV (same lab), stores artifacts in R2 (Lab 10), and gates everything with Access (Lab 08). Verify each service is reachable now so you are not troubleshooting entitlements under time pressure on Day 2.
Wrangler CLI checks:
# D1: list databases (empty is fine; just confirms the binding is available)
wrangler d1 list
# KV: list namespaces (empty is fine)
wrangler kv namespace list
# R2: list buckets (empty is fine)
wrangler r2 bucket list
Each command should exit 0 and return either a JSON array or an empty list. A
non-zero exit or an “unauthorized” error means the API token is missing the
required permission scope; re-issue the token with D1/KV/R2 read permissions
(or re-run wrangler login to use the full OAuth scope).
Dashboard check for Zero Trust / Access:
Open dash.cloudflare.com, select your account, and confirm “Zero Trust” appears in the left sidebar. Click it and verify the Access application list is reachable. Lab 08 creates an Access application; no existing applications are needed here.
Free plan note. D1, KV, R2, and Access are all available on the Cloudflare free tier within the workshop’s usage envelope. The workshop does not require a paid plan.
With the wildcard proxied record in place, HTTPS to any subdomain should reach Cloudflare’s edge, even though no Worker is deployed yet. The edge will return a 5xx error code in the 521-524 range (origin unreachable) because the actual origin has not been configured. That is the expected result at this stage.
# The root domain:
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 15 "https://${DOMAIN}/"
# Expected: 2xx, 3xx, or 521-524.
# 000 means curl could not connect at all; check that the record is proxied.
# A wildcard subdomain (should hit the wildcard record):
curl -sS -o /dev/null -w '%{http_code}\n' --max-time 15 "https://api.${DOMAIN}/"
# Expected: same range.
A result in the 521-524 range is a CF origin error: the proxy is active and
forwarding to an origin that has not been configured. This confirms:
- The zone is active in Cloudflare.
- The wildcard record is proxied.
- TLS is terminating at the CF edge (the certificate is issued by Cloudflare).
Lab 06 (Cloudflare Tunnel) and Lab 07 (First Worker) attach real origins to
these routes. The smoke-test result will change to 200 once those labs complete.
Post-state
When this lab is complete, all of the following should be true:
-
wrangler whoamiprints your Cloudflare account name. -
dig +short A "test.${DOMAIN}" @1.1.1.1returns1.2.3.4. -
dig +short A "validate-lab04-check.${DOMAIN}" @1.1.1.1returns192.0.2.1. -
wrangler d1 list,wrangler kv namespace list, andwrangler r2 bucket listall exit 0. -
curl -sS -o /dev/null -w '%{http_code}\n' "https://${DOMAIN}/"returns2xx,3xx, or521-524. - You can name the four Cloudflare services that Day 2 uses and confirm each is visible in the dashboard or accessible via CLI.
Validation
Three validation paths. All three call the same validate.sh and post the result
to the same backend; only the delivery channel differs.
Path 1: epl CLI (recommended from the operator console).
DOMAIN=a00f3f13.eplabs.cloud epl validate lab04-domain-verification
Substitute your actual subdomain. The script exits 0 on full pass. Re-run as many times as needed; only the most recent result is recorded.
Path 2: direct script.
DOMAIN=a00f3f13.eplabs.cloud \
bash courses/engagement-platform-labs/labs/lab04-domain-verification/validate.sh
Or via the Makefile:
cd courses/engagement-platform-labs/labs
DOMAIN=a00f3f13.eplabs.cloud make validate-lab04-domain-verification
Path 3: paste output into the widget.
Run validate.sh anywhere, copy the full terminal output, and paste it into
the widget below.
What the script checks:
test.<DOMAIN>resolves to1.2.3.4viadigagainst1.1.1.1.validate-lab04-check.<DOMAIN>resolves to192.0.2.1(wildcard record).curl https://<DOMAIN>/returns2xx,3xx, or521-524(CF proxy active).wrangler whoamiexits 0 and prints a non-empty account name. SetSKIP_WRANGLER=1to skip this check if wrangler is not installed.
Primitives and drop-in substitutes
Lab 04 verifies two primitives that happen to share a vendor here. The first is authoritative DNS: who answers queries for eplabs.cloud (Cloudflare in this course; could just as well be Bind9 on a VPS). The second is cloud-platform identity: who recognizes your API token and lets you provision D1, KV, R2 (Cloudflare here; could be AWS, GCP, Fly, or self-hosted). Day 2’s labs assume both are reachable; if a client constraint moves you off Cloudflare, you swap each half independently.
DNS authority substitutes
| Name | Model | License | When to consider it |
|---|---|---|---|
| Bind9 | classic authoritative DNS server | MPL-2.0 | self-hosted on a VPS; widest feature set |
| NSD (NLnet Labs) | authoritative-only, no recursion | BSD-3-Clause | minimal attack surface; pairs with Unbound for recursion |
| PowerDNS Authoritative | SQL-backed authoritative server | GPL-2.0 | when you want zone data in Postgres or MySQL |
| Knot DNS (CZ.NIC) | authoritative-only, modern | GPL-3.0 | high throughput; widely used at TLD operators |
| Route 53 (AWS) | managed, anycast | proprietary | when the rest of your platform is on AWS |
| Google Cloud DNS | managed, anycast | proprietary | GCP-aligned shops |
| deSEC | free public managed DNS, REST API | non-profit / free tier | when you want managed DNS without a large vendor |
| Tailscale MagicDNS | private DNS for tailnet only | proprietary | when “DNS” in your stack means tailnet hostnames, not public records |
Cloud-platform and API entitlement substitutes
| Name | Model | When to consider it |
|---|---|---|
| AWS (DynamoDB / S3 / Lambda / API Gateway) | mature, broad, expensive | enterprise alignment |
| GCP (Firestore / GCS / Cloud Functions / Cloud Run) | similar shape to AWS, different pricing model | GCP-aligned organizations |
| Fly.io (Postgres, S3-compatible storage, Machines) | container-first, region-pinning | when you want a closer-to-the-metal VM model |
| Supabase (Postgres / Storage / Edge Functions) | Postgres-first, opinionated | when relational data is the core |
| PocketBase (single-binary BaaS, self-hosted) | SQLite plus auth plus realtime | edge or embedded; small deployments |
| Self-hosted: Postgres + MinIO + nginx + a Go or Rust HTTP server | the “no platform” baseline | when you must own every layer |
Take-home prompt
Take one half of the stack and re-host it. Easy mode: move DNS authority to deSEC for <your-handle>.eplabs.cloud and observe the propagation delay. Harder: stand up a small self-hosted Worker-equivalent (workerd or a Go HTTP handler on a VPS) and re-run the Lab 07 health-endpoint exercise against it.
Further reading
Cloudflare DNS and zones:
- Cloudflare DNS overview. The full zone and record management reference.
- DNS proxy (orange cloud). What proxied vs. DNS-only means for each record type, and which records can be proxied.
Wrangler CLI:
- Wrangler CLI reference.
Complete command reference. The
login,whoami,d1,kv, andr2subcommands used in this lab are documented here.
Services used in Day 2:
- D1 (SQLite at the edge). The database the Workshop Worker uses to record device enrollment.
- KV (key-value store). Used for rate-limit state and shared config.
- R2 (object storage). Used in Lab 10 to store captured packet artifacts.
- Cloudflare Access. OTP-gated access configured in Lab 08.
Workshop-internal:
- Lab 06: Cloudflare Tunnel.
Attaches the Mango to your subdomain via a
cloudflaredtunnel running on the operator console. - Lab 07: First Worker. Deploys the Workshop Worker and binds it to D1 and KV under your subdomain.
instructor/dns_delegation_setup_guide.mddocuments how per-student subdomains are provisioned undereplabs.cloud.
View this page’s source:
labs/lab04-domain-verification/README.mdx
Troubleshooting
wrangler login: browser does not open in the Dev Container
wrangler login prints a URL to the terminal when it cannot open a browser
automatically. Copy the URL and open it in the browser on your laptop. After
you complete the OAuth grant, the CLI detects the callback and prints
“Successfully logged in.” If the callback never fires, check that port 8976
(wrangler’s default OAuth callback port) is forwarded in VS Code.
As an alternative, generate an API token at
dash.cloudflare.com/profile/api-tokens
using the “Edit Cloudflare Workers” template (add D1, KV, and R2 read
permissions), then set CLOUDFLARE_API_TOKEN in the operator console shell.
dig returns empty or NXDOMAIN for the test record
- Wait two minutes after creating the record in the dashboard. CF DNS propagates quickly but not instantly.
- Try querying a specific resolver:
dig +short A "test.${DOMAIN}" @8.8.8.8 - Confirm the record was saved with gray cloud (DNS only). A proxied
test.record resolves to CF’s anycast IP, not1.2.3.4. - If using your own domain: confirm the zone status is “Active” in the Cloudflare dashboard. A “Pending” zone means nameserver delegation at the registrar has not completed.
Wildcard record resolves to wrong IP
The wildcard check queries validate-lab04-check.<DOMAIN> and expects 192.0.2.1.
Common reasons it fails:
- The wildcard record (
*) was created as DNS only (gray cloud) rather than proxied (orange cloud). Proxied records with RFC 5737 content return the configured IP via CF’s resolver. - The wildcard record content is not exactly
192.0.2.1. - A more specific record for
validate-lab04-checkexists and overrides the wildcard. Check the DNS records list in the dashboard.
curl returns 000 (connection failure)
000 means curl could not establish a TCP connection, not that the server
returned an error code. Check:
- The wildcard
Arecord exists and is set to proxied (orange cloud). - The zone status in the CF dashboard is “Active,” not “Pending.”
- Your operator console has outbound HTTPS access. Test with
curl -sS -o /dev/null -w '%{http_code}\n' https://cloudflare.com/.
wrangler d1 list / kv namespace list / r2 bucket list: unauthorized
The OAuth token issued by wrangler login covers Workers and basic account
access. D1, KV, and R2 scopes are included by default for full-access tokens,
but a restricted API token may be missing them. Either:
- Re-run
wrangler loginto refresh with a full-access token, or - Generate a new API token at
dash.cloudflare.com/profile/api-tokens
with the “Edit Cloudflare Workers” template and add the D1, KV, and R2
read-write permissions manually. Set
CLOUDFLARE_API_TOKENin the shell.
Workshop subdomain not found or “Add a Site” fails
The per-student subdomain is delegated at the eplabs.cloud zone level before
the workshop. Students do not add it to Cloudflare via “Add a Site.” The
subdomain should already appear in your account’s zone list when you open the
dashboard. If it is missing, check with the instructor: the delegation record
may not have been created for your slot, or the wrong email was associated with
your assignment card.
ValidateOutputPaster lab="lab04")