WholeTech Network · Infrastructure Runbook

Cloudflare in front,
Backblaze & R2 behind.

Keep GoDaddy as registrar. Put Cloudflare ahead of the droplet for free SSL, a global CDN, WAF/DDoS, and a hidden origin IP — and add Cloudflare R2 as a second offsite backup leg beside B2. About 190 sites, mail-safe, Let's-Encrypt-aware, and reversible at every step.

Adversarially reviewed — 6 outage risks caught & corrected in this plan

The nameserver change is the only cutover. Nothing inside your 190 webroots changes. Pointing NS back to GoDaddy is the heavy rollback — and grey-clouding a record inside Cloudflare is the instant one.

~190
zones, Free plan
$0
base monthly cost
~$3.50
/mo R2 (your call)
100%
reversible
⚠ Decision needed before Phase 4

Cloudflare R2 is metered, pay-per-use, with a card on file.

Everything else here is $0 and fits the subscription-only rule perfectly. R2 does not: it bills per GB stored, so it's the one piece that can produce a surprise invoice. It's small and worth it for a zero-egress second backup — but it's genuinely your call.

Add R2 (recommended) — second offsite leg at a different company; free restores. ~$3.50/mo + a billing alert + a hard cap
Skip R2 for now — keep Backblaze B2 as the sole offsite leg; do the DNS/edge work only. $0 net new
Why this move

One switch buys four protections

Free SSL, a global CDN for your portable static pages, an edge WAF with automatic DDoS mitigation, and — once the firewall is locked at the very end — a hidden origin. Today 143.198.182.180 is printed in every DNS record for anyone to see and hit directly.

Free, per zone, ×190 = $0
No per-site fee, no per-request billing. The only new spend in the whole plan is a few dollars of R2 storage.
GoDaddy stays your registrar
You don't move domains or ownership — you re-point each domain's nameservers. That's the single reversible switch.
Your pages don't change
Root-relative links, one canonical variable — nothing hard-codes the IP or a host. Turning on the proxy changes zero page content.
A real second backup leg
R2 is a different company from Backblaze, with zero egress fees — so restores and test-restores cost nothing.
DNS becomes code
One account, one API token, Terraform to create and audit all 190 zones from ccmidbee2 — instead of 190 GoDaddy panels.
Hidden, hardened origin
After cutover, the firewall accepts web traffic only from Cloudflare — the box stops being directly reachable or scannable.
How it fits together

The stack, top to bottom

Nothing here replaces the droplet. nginx keeps serving /var/www and reverse-proxying the /opt apps exactly as now — Cloudflare just sits in front, and mail deliberately steps around it.

Registrar · GoDaddy
Still owns and renews all ~190 domains. The only change per domain is the nameserver pair. Ownership, renewals, WHOIS untouched. DNSSEC must be turned off at GoDaddy before each NS flip, or the domain goes dark.
Cloudflare · edge
Authoritative DNS for every zone. For web hostnames the orange-cloud proxy terminates TLS, runs the WAF + DDoS protection, and serves cached static content globally. Public records now resolve to Cloudflare, not the droplet.
Droplet · origin
nginx unchanged. Two additions at the end: firewall 80/443 to Cloudflare IP ranges only, and restore the real visitor IP from the CF-Connecting-IP header so AWStats & the security dashboard don't just see Cloudflare.
Mail · grey-cloud
The proxy only speaks HTTP. Every mail record — MX, the mail-host A record, and SPF / DKIM / DMARC TXT — stays DNS-only so SMTP/IMAP reach the origin directly. Proxy a mail record and mail dies.
The one rule that keeps mail alive

Orange cloud vs. grey cloud

Every DNS record lands in one of two buckets. Get this split right per zone and the migration is safe; get it wrong on a mail record and that domain stops delivering. On import, default the proxy correctly and verify against GoDaddy before cutover.

Record
What it is
Setting
A / AAAA @
Apex website — the main site
Proxied
www / app
Web hostnames & /opt app front-ends (HTTP)
Proxied
MX
Mail exchangers — no proxy concept exists
DNS-only
mail / smtp / imap
Mail host A record — must reach the box directly
DNS-only
TXT · SPF
v=spf1 … sender policy
DNS-only
TXT · DKIM
selector._domainkey signing key
DNS-only
TXT · DMARC
_dmarc policy & reporting
DNS-only
CAA / SRV
Cert authority & service records
DNS-only

Cloudflare's auto-scan on import is best-effort and misses records it can't enumerate (especially DKIM selectors and app subdomains) — so diff every zone against GoDaddy before the NS flip.

The runbook · 7 phases, canary first

Order of operations

This is a real sequence, so it's numbered. The first five phases have zero live impact — creating a zone and loading records does nothing until that domain's nameservers flip. Only Phase 6 touches users. The green notes mark where the adversarial review changed the plan.

1

Prep & decisions no live change

  • Create one Cloudflare account; mint a scoped API token (Zone Read/Edit, DNS Edit) for Terraform; a separate R2 token if adding R2.
  • Build the ~190-domain inventory from /var/www + GoDaddy. Flag which domains actually carry mail on the droplet vs. use Gmail.
  • Decide origin-cert end state: recommended = a free Cloudflare Origin CA cert (15-yr) on the droplet; alternative = certbot on DNS-01.
  • Enumerate every proxied hostname's origin port — the Free proxy only carries standard web ports; any /opt app on an odd port must be grey-clouded or put behind a Tunnel.
  • Pick a low-traffic canary domain (a spare — not walhus.com / wholetech.com) to prove the whole flow first.
Fix applied — added the origin-port audit; a proxied app on a non-standard port would otherwise break silently.
2

Canary — one domain, end to end isolated

  • Add the canary zone; let Cloudflare auto-scan; verify every record against GoDaddy.
  • Mail records grey-cloud, web hosts orange-cloud. Install the Origin CA cert, then set SSL to Full (strict). Enable Always Use HTTPS. Never Flexible.
  • Confirm the presented origin cert covers this hostname with openssl s_client before flipping to strict.
  • Disable DNSSEC at GoDaddy, then change nameservers to the Cloudflare pair. Load the site; send + receive a test email; confirm SPF/DKIM/DMARC pass.
RiskA throwaway domain only — nothing important is exposed.
RollbackGrey-cloud instantly, or point NS back to GoDaddy.
3

Certificates only — no firewall yet no user impact

  • Install the Cloudflare Origin CA cert in nginx (or switch certbot to DNS-01) so Full (strict) holds without HTTP-01 renewals through the proxy.
  • Add a cert-expiry monitor (extend the existing security dashboard) that alerts under 20 days to expiry; after switching a host to Origin CA, remove its certbot cron so it can't silently error.
  • Add nginx real-IP config (set_real_ip_from CF ranges; real_ip_header CF-Connecting-IP) and a cache rule that bypasses the /opt app hosts.
Fix applied — the origin firewall was moved OUT of this phase (it would have blacked out every not-yet-migrated site) to Phase 7. This phase is cert work only.
4

Add the R2 backup leg if you chose R2

  • Create the R2 bucket + token; add the r2: rclone remote; smoke-test write + read-back.
  • Patch wholetech-backup.sh to copy each tarball to both b2: and r2: (R2 is purely additive — every B2 line stays exactly as-is).
  • Set a Cloudflare billing/storage alert + hard cap, and turn the R2 prune-failure WARN into a real ntfy alert (like the disk-space watchdog). Cap R2 retention at 30 days.
  • Let the 3am run fire once; next morning confirm the identical tarball is in both, and do one free test-restore from R2.
Fix applied — added billing alert, hard cap, and a real (non-silent) prune-failure alert so a metered service can't drift into a surprise bill.
RiskVery low — independent of DNS; a failed R2 copy logs WARN and never blocks B2.
RollbackDelete the r2 lines; B2 backups continue untouched.
5

Bulk zone creation — records loaded, NS not flipped no live change

  • With Terraform/API from ccmidbee2, create all remaining zones on Free. Import records; codify proxy on/off + Full (strict).
  • Programmatically audit every zone while NS still points at GoDaddy: mail grey-cloud, web orange-cloud, DKIM selectors + app subdomains present. Fix now.
  • Record each zone's unique Cloudflare nameserver pair for the cutover.
  • Pre-lower A/CNAME record TTLs in Cloudflare before each batch so the grey-cloud rollback lever is near-instant.
Fix applied — pre-lower record TTLs so the fast (CF-side) rollback is genuinely fast.
6

Batched nameserver cutover — the real switch user impact

  • Disable DNSSEC at GoDaddy for the batch, then change each domain's NS to its Cloudflare pair (GoDaddy API or console).
  • Go in batches of ~20, most-important domains last. After each batch: confirm web loads proxied, mail sends/receives, TLS valid — before the next batch.
  • Monitor 24h between the first batches. Do wholetech.com / walhus.com / ai.wholetech.com in a final, closely-watched batch.
RiskThe only step with real impact. A bad batch affects ~20 domains, not the network.
RollbackFast: grey-cloud the record in CF (seconds). Heavy: NS back to GoDaddy (up to 24–48h — registry TTL).
Fix applied — grey-cloud named as the PRIMARY rollback; the old plan leaned on NS-revert, which is slow and can't be pre-shortened.
7

Lock the origin & polish after all batches

  • Hard gate: only after an audit shows 0 zones still resolving to the droplet A record, apply the ufw rules allowing 80/443 only from Cloudflare's IPv4+IPv6 ranges (SSH stays open via admin IP / Tailscale). Add a monthly cron to refresh the CF range list.
  • Turn on a baseline WAF managed ruleset, Bot Fight Mode, and rate limiting (all Free-tier).
  • Confirm the origin IP no longer appears in any public web record. Honest exception: the mail-host A record still reveals the IP — accept it, or later route mail through a relay.
  • Verify AWStats logs real client IPs and /opt apps aren't HTML-cached. Update memory/runbooks.
Fix applied — the firewall now lives here, behind a hard "no zone still points at the droplet" gate, so hardening can't black out an un-migrated site.
Known traps

The eight ways this bites — and the fix

Proxy a mail record → mail dies
FixKeep MX, the mail-host A, and SPF/DKIM/DMARC TXT all DNS-only. Send + receive a live test after each cutover.
Wrong SSL mode → loops or 526
FixFull (strict) with an Origin CA cert installed first. Verify per host with openssl before flipping; a miss stays grey-cloud until fixed.
certbot HTTP-01 breaks behind the proxy
FixAdopt Origin CA (no ACME) or move certbot to DNS-01. Renewals fail silently ~30 days out — so add the expiry monitor, don't trust a cutover-day check.
Proxy on ≠ hidden origin
FixThe IP stays reachable until you firewall 80/443 to Cloudflare ranges (Phase 7). Optional stronger form: a Cloudflare Tunnel, no inbound port at all.
Mail host A still leaks the IP
FixHonest limit — mail can't be proxied, so mail.<domain> reveals the box. Accept it, or later route mail through a smarthost.
GoDaddy DNSSEC left on → domain offline
FixDisable DNSSEC at GoDaddy before each NS flip. Re-enable from Cloudflare's side later if wanted.
Cache serves stale /opt app output
FixCache rule bypasses app hosts + dynamic paths; static /var/www stays cacheable. Mind the 100MB Free request-body limit on upload apps.
Import misses subdomains; logs lose real IPs
FixDiff each zone vs GoDaddy and re-add missing records; configure nginx real-IP so stats keep true visitor IPs.
If you add the R2 leg

R2 setup — additive, reversible

S3-compatible object storage with zero egress. It becomes the second offsite copy of the daily tarball, at a different company than Backblaze — so an account or billing problem at one never orphans your backups. rclone is already on the droplet with b2: and gdrive:; you just add a third remote.

# ~/.config/rclone/rclone.conf on the droplet — alongside [b2] and [gdrive]
[r2]
type = s3
provider = Cloudflare
access_key_id = <R2_ACCESS_KEY_ID>
secret_access_key = <R2_SECRET_ACCESS_KEY>
endpoint = https://<ACCOUNT_ID>.r2.cloudflarestorage.com
region = auto
acl = private
no_check_bucket = true
# wholetech-backup.sh — add beside the B2 vars, then copy each tarball to BOTH
R2=r2:walhus-droplet-snapshots
R2_KEEP_DAYS=30            # shorter tail than B2's 90d to bound cost

rclone copy "$WWW" "$B2/"  # existing — unchanged
rclone copy "$WWW" "$R2/"  # new — additive
rclone delete --min-age ${R2_KEEP_DAYS}d "$R2/"
What it costs

The whole bill

Cloudflare Free plan × ~190 zones — DNS, CDN, SSL, WAF baseline, DDoS$0/mo
Universal SSL + Origin CA certificate (15-yr)$0
Cloudflare Tunnel (optional, instead of IP firewalling)$0
R2 storage — ~8GB/day, 30-day tail (~240GB) @ $0.015/GB-mo~$3.50/mo
R2 storage if matched to B2's 90-day tail (~720GB)~$11/mo
R2 egress — restores, test-restores, pull-backs$0 forever
GoDaddy — registrar renewals only (unchanged)no change
Net new spend for the whole migration~$3.50/mo

Everything except R2 is $0. R2 is the only metered line — hence the decision at the top.