SOPs
Cloudflare Zone Hardening
The Rule
Section titled “The Rule”Every Cloudflare zone hits the same baseline: SSL full-strict, Always HTTPS on, Min TLS 1.2, HSTS with preload, DNSSEC active, and a root SPF that matches the actual senders. That is the six-knob checklist. The daily audit at ~/apps/cloudflare-audit/ flags any zone that drifts from it.
The order matters. Never turn on SSL strict before confirming the origin serves a valid cert. Never write an SPF at the apex before checking whether Resend (or anything else) sends from the domain.
The Six Knobs
Section titled “The Six Knobs”Each knob has a default Cloudflare ships, a reason it matters, and a way it can bite if flipped blind. Explanations are for future-me reading this when the muscle memory is gone.
1. SSL mode — full (strict)
Section titled “1. SSL mode — full (strict)”Cloudflare’s SSL modes control how the edge talks to the origin.
- Flexible — edge→origin is plain HTTP. The browser padlock is a lie. Never use.
- Full — edge→origin is HTTPS but no cert validation. Self-signed or expired cert passes. Historical default.
- Full (strict) — edge→origin is HTTPS with a valid, trusted cert matching the hostname. Target state.
The risk of flipping to strict is that the origin cert is broken and the site starts throwing 525s. Pages origins, Vercel origins, and GHL origins all serve valid certs and are safe. Custom origins need a cert check first (curl -vI https://origin-hostname/ — look for “verify ok”).
API: PATCH /zones/{zid}/settings/ssl with {"value":"strict"}.
2. Always HTTPS — on
Section titled “2. Always HTTPS — on”Without this, Cloudflare serves HTTP requests over HTTP. The browser padlock only applies after the first HTTPS request, so the initial insecure hop is still open to a local attacker on hostile WiFi. Turning this on issues a 301 to HTTPS for any HTTP request.
Safe to flip as long as the site actually works over HTTPS. Verify with curl -sSI -L -o /dev/null -w "%{http_code} %{url_effective}\n" http://domain — expected: 200 landing on https://domain/.
API: PATCH /zones/{zid}/settings/always_use_https with {"value":"on"}.
3. Minimum TLS Version — 1.2
Section titled “3. Minimum TLS Version — 1.2”TLS 1.0 and 1.1 are deprecated (BEAST, POODLE). The floor should be 1.2. PCI, HIPAA, and most auditors require 1.2 as the minimum.
Minimum is not the same as only. With a 1.2 floor, modern clients still negotiate 1.3 automatically. Going to a 1.3 floor cuts off iOS 12.1 and older, Android 9 and older, some corporate proxy appliances, and a chunk of legitimate uptime monitors and crawlers. Stay at 1.2 unless compliance forces otherwise.
API: PATCH /zones/{zid}/settings/min_tls_version with {"value":"1.2"}.
4. HSTS — enabled, max-age 31536000, includeSubDomains, preload, nosniff
Section titled “4. HSTS — enabled, max-age 31536000, includeSubDomains, preload, nosniff”HSTS tells the browser “never use HTTP for this domain for the next year.” Removes the HTTPS downgrade window that Always HTTPS cannot fully close (the browser sends HTTP first even when Always HTTPS is on — a local attacker can intercept that first request before it reaches Cloudflare).
Four knobs on the setting:
- max-age — 31536000 (one year). Below 6 months is considered weak.
- includeSubDomains — every subdomain must serve valid HTTPS from now on.
- preload — ships the domain hardcoded inside Chrome and Firefox releases. Removal takes months to propagate through browser updates.
- nosniff — bonus X-Content-Type-Options header bundled in this setting. Blocks MIME-type sniffing attacks.
The gotcha: preload + includeSubDomains is semi-permanent. Before enabling, confirm every subdomain under the zone is on HTTPS. For Pages/Vercel/GHL-only zones this is automatic. For zones with custom subdomains on mixed infrastructure, audit first.
API: PATCH /zones/{zid}/settings/security_header with the strict_transport_security object enabled and all four flags set.
5. DNSSEC — active
Section titled “5. DNSSEC — active”DNS without DNSSEC is trust-based — a resolver accepts whatever answer comes back. Cache poisoning, hijacked responses on hostile networks, and ISP injection all exploit this. DNSSEC adds cryptographic signatures so resolvers can verify the record actually came from the authoritative source.
The chain of trust: Cloudflare signs the zone with a Zone Signing Key, signs the ZSK with a Key Signing Key, publishes the KSK public half as a DS record at the registrar, and the registrar’s parent zone (.com, .org) signs the DS. Each step chains to the root trust anchor.
The risk of enabling: if the DS record is missing or wrong at the registrar, the domain goes dark because resolvers see a broken chain of trust. James’s domains are on Cloudflare Registrar, which means the DS record gets published automatically when DNSSEC is enabled — one click, no registrar step. Non-Cloudflare-Registrar domains require manual DS record transfer.
API: PATCH /zones/{zid}/dnssec with {"status":"active"}. Status goes to “pending” then “active” within a few minutes.
6. Root SPF — Resend-aware
Section titled “6. Root SPF — Resend-aware”Every domain needs an SPF record at the apex. What the record says depends on whether the domain actually sends email and what it sends through.
- Domain sends nothing, ever:
v=spf1 -all(null SPF). Hardfail every IP. Without this record, receivers cannot distinguish real from spoofed — worse than a null policy. - Domain sends via Resend:
v=spf1 include:amazonses.com ~all. Mirrors the SPF Resend already set on thesend.subdomain. Null SPF would break any email using the bare apex FROM address. - Domain sends via multiple services: chain the includes. Example:
v=spf1 include:amazonses.com include:_spf.google.com ~all.
Before applying any SPF fix: ask what senders the domain has. The audit’s auto-detection handles Resend (see below), but other senders (Google Workspace, Mailgun, SendGrid, GHL, custom SMTP) are not yet auto-detected. Confirm each one.
To add: POST /zones/{zid}/dns_records with {"type":"TXT","name":"<zone>","content":"v=spf1 ..."}. To update an existing: PATCH /zones/{zid}/dns_records/{id}.
The Fix Order
Section titled “The Fix Order”When the daily audit flags a zone, work through these in sequence. Each step has a verification before moving to the next.
-
Probe the zone. Pull current DNS records (A, CNAME, MX, TXT), current security settings (SSL, Always HTTPS, Min TLS, HSTS, DNSSEC), and the origin that apex + www resolve to.
curl -sSI -L https://zoneandhttps://www.zoneshould both return 200. -
Identify the origin. Cloudflare Pages, Vercel, GHL, or something else. Pages/Vercel/GHL origins are cert-valid — SSL strict is safe. Anything else, verify the origin cert directly before proceeding.
-
Apply the four safe toggles. SSL strict, Always HTTPS on, Min TLS 1.2, HSTS with preload + includeSubDomains + nosniff. Fire the four PATCH calls.
-
Enable DNSSEC. One PATCH to
/dnssec. Status goes to “pending” — refresh and verify it moves to “active” within a few minutes. -
Sender check before SPF. Query the Resend API for verified domains (the audit does this automatically now). Ask James about Google Workspace, Mailgun, SendGrid, GHL, or any custom SMTP. Build the include chain from the list.
-
Apply or update SPF. If no SPF exists, add one. If an old SPF exists (common DreamHost/MailChannels relic), replace it with the current sender chain or null out if the domain is now dormant.
-
Clean up stale email records. If the domain is confirmed dormant, delete stale MX records (MailChannels, old DreamHost relays). Leaving them means incoming mail bounces further along the chain instead of at DNS — worse feedback to senders.
-
Verify. Final curl on apex and www (both HTTPS 200), plus
http://domain(should 301 to HTTPS). If anything is not 200, stop and diagnose before moving on.
The Resend Gotcha
Section titled “The Resend Gotcha”Resend configures email sending on the send. subdomain, not the apex. When you add a domain to Resend, the records Resend publishes are:
MX send.<domain>→feedback-smtp.us-east-1.amazonses.comTXT send.<domain>→v=spf1 include:amazonses.com ~allTXT resend._domainkey.<domain>→ DKIM public key
Notice the SPF is on send., not the apex. If the apex has no SPF record, the audit flags email_no_spf at the root. The intuitive fix — add v=spf1 -all at the apex — silently breaks Resend sends that use the bare apex FROM address (e.g., hello@domain.com instead of hello@send.domain.com). DMARC at p=none softens the break into “flagged as spam” instead of “rejected,” but it is still wrong.
The fix: at the apex, add v=spf1 include:amazonses.com ~all (softfail, matches what Resend put on send.). This authorizes Amazon SES (which Resend uses) to send from the apex too, while still rejecting spoofed mail from other IPs.
The audit now auto-detects Resend domains and emits the Resend-aware recommendation. See ~/apps/cloudflare-audit/audit.py (the SPF check block) and the Email Protection section of ~/apps/cloudflare-audit/CLAUDE.md.
Useful API Recipes
Section titled “Useful API Recipes”All calls use Global API Key auth with X-Auth-Email and X-Auth-Key headers. Base URL is https://api.cloudflare.com/client/v4/. Zone ID is pulled from /zones?name=<domain>.
Full audit for a zone:
eval "$(python3 ~/apps/cc/secrets/secrets_cli.py env)"ZONE="example.com"ZID=$(curl -s "https://api.cloudflare.com/client/v4/zones?name=$ZONE" \ -H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_API_KEY" \ | python3 -c "import sys,json; print(json.load(sys.stdin)['result'][0]['id'])")
# All records, security settings, DNSSEC status, TXT records# (See ~/apps/cloudflare-audit/audit.py for the full set of probes)Apply the four safe toggles in one pass:
for pair in "ssl:strict" "always_use_https:on" "min_tls_version:1.2"; do S="${pair%%:*}"; V="${pair##*:}" curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZID/settings/$S" \ -H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_API_KEY" \ -H "Content-Type: application/json" --data "{\"value\":\"$V\"}"done
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZID/settings/security_header" \ -H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_API_KEY" \ -H "Content-Type: application/json" \ --data '{"value":{"strict_transport_security":{"enabled":true,"max_age":31536000,"include_subdomains":true,"preload":true,"nosniff":true}}}'Enable DNSSEC:
curl -s -X PATCH "https://api.cloudflare.com/client/v4/zones/$ZID/dnssec" \ -H "X-Auth-Email: $CF_EMAIL" -H "X-Auth-Key: $CF_GLOBAL_API_KEY" \ -H "Content-Type: application/json" --data '{"status":"active"}'Precedent
Section titled “Precedent”2026-04-20 — Springville near-miss. During the April 20 audit fix pass, applied v=spf1 -all at the apex of springvillehousecleaning.com as the documented fix for email_no_spf. James flagged mid-session that the domain is a verified Resend sender. Caught before any email failures. Fix: replaced the record with v=spf1 include:amazonses.com ~all. Follow-up: audit.py now queries the Resend API and emits the Resend-aware recommendation automatically (Build 7). The “check every sender” principle is codified here so the next session does not rely on the auto-check for non-Resend senders.
2026-04-20 — Cascade dormant cleanup. cascadewindowcleaning.com had an old DreamHost/MailChannels email setup (stale MX records, apex SPF pointing to MailChannels and netblocks.dreamhost.com). Site is dormant and no longer sends email. Full cleanup: six-knob hardening + MX deletion + SPF replaced with null. Clean precedent for “what happens when a Pages-hosted site is dormant on email.”
Related
Section titled “Related”~/apps/cloudflare-audit/— the daily audit that flags findings. Runs on VPS cron at 02:00.- Local-Site Defaults — the “is this a local-only site?” question and the three WAF rules to apply when the answer is yes. Layered on top of the six-knob baseline.
- Cloudflare Pages Deploy — every Pages project needs GitHub Actions, not direct upload.
- Custom Domain Email — deeper into SPF, DKIM, DMARC for sending domains.
- Public writeup of the six-knob concept: published at
themarketingshow.com/tech/six-cloudflare-settings-not-on-by-default(externalized so non-infrastructure folks can follow along; the internal version keeps the real paths and the audit tooling).