I moved my domain from the registrar's nameservers to Cloudflare Pro in a single afternoon. Multiple Cloud Run services across two GCP projects, Google Workspace email, and a domain that had never been behind a CDN or WAF. The Cloudflare documentation covers the happy path well. This piece covers everything else.

The migration ran twelve tasks across three phases: DNS setup, proxy activation, and hardening. Most of the difficulty lived between the tasks, in sequencing decisions and UI gotchas the documentation does not mention. Four stood out: gray-cloud sequencing for certificate provisioning, email as the highest-risk service, HSTS preload as a long-term commitment, and a double-authentication pattern I ended up keeping.

Gray cloud first, orange cloud later

Cloud Run domain mappings use Google-managed certificates. Google does not document the validation mechanism - the docs commit only to certificates being "automatically issued and renewed" - but they are explicit about the failure mode: a third-party CDN can inadvertently intercept validation requests, causing the domain mapping to fail or its certificate to fail to renew. The docs call out Cloudflare by name, with turning off the Always Use HTTPS edge setting as the suggested fix. The mechanism is consistent with ACME HTTP-01 (both CAs Google issues from, Google Trust Services and Let's Encrypt, are ACME CAs), but treat that as inference rather than documentation.

The fix is ordering: every Cloud Run subdomain starts with the proxy off (gray cloud in Cloudflare's UI), stays gray until Google provisions the cert, then flips to proxied (orange cloud) one subdomain at a time. If a flip breaks something, you know exactly which record caused it and can revert it without touching the others.

The harder problem is what happens after the flip. Google-managed certificates in the sibling products (load balancer certs, Certificate Manager) are valid for 90 days and renew automatically before expiry; Cloud Run's docs give no cadence, but the renewal-through-a-proxy risk they describe applies on whatever the real timer is. Google's documented fix is blunt: turn off the zone-wide Always Use HTTPS setting, because the validation request arrives over plain HTTP and an edge redirect kills it before it reaches Google. I added a WAF skip rule on /.well-known/acme-challenge/ so challenge requests clear WAF processing, but be clear about what that rule does not do: a WAF skip does not bypass the Always Use HTTPS redirect, so on its own it is a partial mitigation. I have not yet watched a renewal succeed through the proxy, so the fallback is scheduled rather than assumed: a 75-day calendar reminder per subdomain, and if the first proxied renewal fails, that record goes back to gray cloud (or Always Use HTTPS goes off) until the new cert lands.

Email is the highest-risk service in a DNS migration

Changing nameservers is a DNS migration, and the highest-impact service to break is email. The failure mode is quieter than people expect: if MX records are wrong or missing after cutover, sending servers queue and retry for a few days, then bounce the message back to the sender - RFC 5321 requires it. The domain owner never sees those bounces. Your inbox just goes quiet, and by the time someone tells you out-of-band, days of mail have already been returned to senders. The one genuinely silent case is an MX pointing at a host that accepts and discards - then nobody gets a signal.

The migration also tightened mail authentication: SPF with -all hard fail, DKIM with 2048-bit keys, DMARC with strict alignment (adkim=s; aspf=s) starting at p=quarantine and tightening to p=reject after clean aggregate reports. Two of those choices are stricter than Google's own guidance - Google recommends ~all and relaxed alignment - and they are viable here only because this domain has exactly one sending service and no subdomain senders. If your domain has forwarding, mailing lists, or third-party senders in play, strict alignment and -all will cost you legitimate mail: start at p=none and read the aggregate reports first.

The pre-flight check is critical: query Cloudflare's assigned nameservers directly before changing anything at the registrar. If the MX records resolve correctly against Cloudflare's NS, they will resolve correctly after cutover. Verify email immediately after propagation - do not wait until someone reports missing mail.

HSTS with preload is a one-way door

Cloudflare makes it easy to enable HSTS with subdomain inclusion and preload. Once a browser has seen the header, it upgrades every HTTP request for the domain and all subdomains to HTTPS before anything leaves the machine, for the max-age duration (12 months in my case - exactly the preload list's minimum). The preload flag is consent, not submission: getting into browser preload lists requires submitting the domain at hstspreload.org, and because the flag signals consent, anyone can submit it for you. Checking while writing this: rehagen.net is still not on the list, because I never submitted it. Once a domain is in, the policy outlives the header - removal is a separate request that takes six to twelve weeks to reach most Chrome users, longer for other browsers.

For a new Cloudflare zone where everything speaks HTTPS from day one, that commitment costs nothing. The constraint bites later: any future subdomain that needs plain HTTP - a legacy device, a delegated zone, an internal tool - will be blocked by a policy you set months earlier and forgot about. That is the profile of a hard-to-diagnose outage, and it is why preload deserves a deliberate decision rather than a checkbox.

Double authentication was not in the plan

The private app runs behind Google's Identity-Aware Proxy. Adding Cloudflare Access on top was originally a convenience: edge-level identity enforcement so unauthenticated requests never reach Cloud Run. What I did not plan for was keeping both.

With Cloudflare Access and IAP both active, the request passes through two identity checks. Cloudflare Access verifies at the edge via Google OAuth. IAP verifies again at the Cloud Run service.

In practice, when accessed from Chrome with a Google profile already signed in, the experience is one login: both checks reuse the active Google session and complete without a second prompt. That is observed behavior, not a guarantee - Access and IAP are separate OAuth clients with separate cookies, and account-selection or reauthentication prompts can resurface either one. The two-prompt experience shows up on cold sessions: a fresh browser, an incognito window, or a non-Chrome browser without a cached Google session.

The security argument for keeping both layers: if someone finds the .run.app URL - stable, guessable, and enumerated by scanners - the request bypasses Cloudflare entirely, and IAP is what blocks it (at Google's front end, before the request reaches the container). Removing IAP leaves protection resting on that URL staying unknown, which is obscurity standing in for access control. Removing Cloudflare Access pushes every probe through to Google's infrastructure before IAP turns it away, and gives up the WAF filtering, bot scoring, and edge analytics that come with rejecting junk at Cloudflare.

For a daily-use personal app accessed through a logged-in Chrome profile, the friction is essentially zero. The cleaner long-term design is to validate the Cloudflare Access JWT (the Cf-Access-Jwt-Assertion header) in the application layer and retire IAP: one identity check at the edge, with direct .run.app requests rejected because they carry no valid JWT. That collapses cold sessions to one prompt. It is a code change I have not made yet.