vinnymarquez.dev/tutorials/gating a self-hosted app with cloudflare access + google login
September 9, 20265 min readguide#homelab#cloudflare#security#self-hosted

Gating a Self-Hosted App With Cloudflare Access + Google Login

putting a real login in front of a self-hosted app that has weak or no auth of its own — Cloudflare Access as the gate, Google as the identity provider, and deciding which apps actually need it.

once a self-hosted app is reachable from the internet — via a cloudflare tunnel or anything else — its own login screen is the only thing between the world and your data. for some apps that’s fine. for others it’s a basic-auth box or nothing at all.

Cloudflare Access sits in front of the app and makes you authenticate to Cloudflare before a request ever reaches it. this walks through wiring Google in as the identity provider and gating one hostname with it.

first: does this app actually need a gate?

not every exposed service does. a rough split:

tier what it looks like gate?
never expose infra + credential-holding stuff — your CA, DNS admin, monitoring, container managers, anything holding API keys not exposed at all, LAN-only
tier 1 app has a real account system built for remote use — proper password + optional 2FA no gate — its own auth is enough
tier 2 app is worth reaching remotely but has weak auth, a shared password, or none Access gate — Cloudflare does the authenticating

the gate is for tier 2. putting one in front of a tier-1 app isn’t wrong, just redundant. the point of the exercise is that a compromised login on a document store or a finance app is a much worse day than on a bookmark manager — spend the gate where the blast radius justifies it.

step 1 — find your Zero Trust team domain

Cloudflare Zero Trust → Settings → Custom Pages (or Settings → General) → Team domain. it looks like <team-name>.cloudflareaccess.com. Zero Trust Free assigns a random one if you’ve never set it — that’s fine, just note it, you need it for the redirect URI next.

step 2 — create a Google OAuth client

in the Google Cloud Console:

  1. create or select a project
  2. APIs & Services → OAuth consent screen (first time only):
    • User type / Audience: External
    • App name: anything
    • User support email + developer contact: your own email
    • leave publishing status at Testing if you’re the only person who’ll ever log in — no verification needed
  3. APIs & Services → Credentials → Create Credentials → OAuth client ID:
    • Application type: Web application
    • Authorized redirect URI: https://<team-name>.cloudflareaccess.com/cdn-cgi/access/callback
  4. it hands you a Client ID and a Client Secret — the secret is shown once, copy it now
the redirect URI has to be exactOAuth error at login time, not at setup time
it's https://<your team domain>/cdn-cgi/access/callback — the /cdn-cgi/access/callback path is fixed, only the team-domain part changes. a trailing slash, http instead of https, or the wrong team domain all fail silently at setup and only surface as a redirect_uri_mismatch when you actually try to log in.

step 3 — add Google as a login method in Cloudflare

Zero Trust → Access controlsLogin methods (it’s under Access controls, not under Settings — this trips people up). Add newGoogle (plain Google, not “Google Workspace” — that one is for G Suite organisational domains).

field value
Name Google
App ID the Client ID from step 2
Client secret the Client Secret from step 2
Email claim name (leave blank)
the 'email claim' field wants a claim name, not your emailbrowser autofill will get this wrong for you
it's asking which OIDC claim carries the email address, and the default (empty) is correct for Google. browser autofill loves to stuff an actual email address into any field called 'email' — check it's blank before saving. a value here quietly breaks the mapping.

save, then hit Test on the login method — it should bounce you through Google and back with a green result.

step 4 — create the Access application

Zero Trust → Access controls → Applications → Add an application → Self-hosted.

Emails alone is enough — since login already goes through Google, matching the email address ties the policy to that Google account. you don’t also need a separate “login methods” rule unless you want to restrict which IdP, and you’ve only wired one.

step 5 — verify the gate actually gates

curl -sI https://app.yourdomain.com/

expect a 302 redirecting to https://<team-name>.cloudflareaccess.com/... — the Access login. if you get the app’s own response instead, the application domain doesn’t match, or the app is also reachable on a second hostname that isn’t listed.

then do it for real in an incognito window: you should hit Google, approve, and land on the app. a browser that’s already authenticated to Cloudflare Access skips straight through — that’s expected, not a bypass.

Access only protects the exact hostnames it's told toa second hostname on the same app is wide open
if the app is reachable on more than one hostname — a custom domain plus a platform default like <project>.pages.dev, or an old hostname you forgot to remove — Access only covers the ones listed as the application's domains. curl every hostname the app answers on; a 200 anywhere means that path is ungated. add each one as an additional domain on the same Access application so the one policy covers them all, then re-curl and confirm they all 302 now.
the app 400s behind the gateHost-header allowlist, unrelated to Access
some apps reject requests whose Host header isn't on an internal allowlist — the error page is the app's, not Cloudflare's, and it happens after Access has already let you through. add the gated hostname to whatever env var / config key the app uses for allowed hosts.

what this does and doesn’t buy you

it means no unauthenticated request reaches the app — good against credential-stuffing its own login, against an unauthenticated RCE in the app, against someone just finding the hostname. it does not replace keeping the app updated, and it does not help if the allowed Google account itself is compromised. it’s a layer, sized to the tier-2 case: worth reaching remotely, not worth trusting the app’s own front door alone.

▌ comments