Protect a static site with Cloudflare Access

Use this when a static site (Antora, MkDocs, Docusaurus, a Pages Direct Upload) must be member-only. Why the lock belongs at the edge, and how GitHub org membership is checked: Access in front of static sites.

Worked shape (Dev-Centr)

custom domain team.docs.devcentr.org (orange-cloud CNAME) plus the Pages hostname *.pages.dev for the same project.

Figures on this page are simplified mocks (field labels from Cloudflare / GitHub docs as of 2026-08). They are not live dashboard captures. Secrets and team names are placeholders or bullets. Redacted API status lines are generic (403, missing_client_id_or_client_secret).

Prerequisites

  • Zone on Cloudflare; hostname proxied (orange cloud)

  • Static origin already serving HTTPS (Cloudflare Pages custom domain, or another origin behind the same zone)

  • A Zero Trust / Cloudflare One organization on the same account as the zone (dashboard: Zero Trust)

Free Zero Trust is enough for small orgs (on the order of 50 seats). A seat is consumed on first Access login, not when you type an email into a policy.

Zero Trust may already exist on the account (a team domain like https://<team-name>.cloudflareaccess.com, and often a default One-time PIN identity provider). PIN is not GitHub org membership. Add GitHub as its own IdP.

Order of operations

Access applications deny by default. Create the GitHub (or PIN) identity provider and the Allow policy before you attach applications to live hostnames. Otherwise the origin goes dark for everyone, including you, until a policy exists.

Setup order: GitHub UI OAuth App, Zero Trust IdP, Allow policy, then Access apps on both hostnames
Figure 1. IdP first, lock last
  1. Register a GitHub OAuth App in GitHub’s UI. GitHub has no REST route to create OAuth Apps. Agents (Composio, gh, GitHub Apps APIs) cannot mint the Client ID and secret.

  2. Add GitHub as a Zero Trust identity provider (paste those secrets). Cloudflare rejects POST /accounts/{account_id}/access/identity_providers with type: github if client_id or client_secret is missing.

  3. Finish setup in the dashboard (GitHub Authorize: orgs/teams + email). Do not rely on the API enterprise-setup URL — it can 500. Finish GitHub setup.

  4. Write the Allow policy (GitHub organization = your-org).

  5. Then create Access applications on the custom domain and *.pages.dev.

GitHub has no create-OAuth-App API; Cloudflare then answers missing_client_id_or_client_secret
Figure 2. GitHub OAuth Apps are a human step

Protect every public hostname

Access on the pretty domain is not enough. Also cover:

  • The custom domain (team.docs.example.org)

  • The Pages project hostname (project.pages.dev)

  • Preview / alias hosts (*.project.pages.dev) if those URLs exist

Create one Access application per hostname (or a wildcard application where Cloudflare allows it). Same Allow policy on all of them.

Find the Zero Trust team name

The GitHub OAuth Homepage URL and callback are the Zero Trust team domain, not the docs hostname.

  1. Open Zero TrustSettingsTeam name (wording may be Team name and domain).

  2. Copy <team-name>. Auth domain: https://<team-name>.cloudflareaccess.com.

Simplified mock of Zero Trust Team name; auth domain is team-name.cloudflareaccess.com, not the docs host
Figure 3. Mock: team name is the OAuth host

Choose how people prove who they are

You do not create a username in Cloudflare for each reader. They sign in with an identity provider (IdP). Pick one primary method so the login page is not a menu.

IdP Who already has an account Policy you write

GitHub (default for forge-centric orgs)

A GitHub user. You invite them to the GitHub organization (or a team).

Allow when login method is GitHub and GitHub organization is your-org (optionally require a team).

Google

A Google account, or a Google Workspace user you created in Admin.

Allow emails ending in @yourdomain or an allowlist of addresses.

One-time PIN

An inbox. No GitHub/Google app to register.

Allow specific email addresses. Cloudflare emails a 10-minute PIN; it only sends mail if the address is already allowed.

One-time PIN is the fastest spike. GitHub org membership is the durable default when the same people already have forge access.

GitHub as IdP

  1. In GitHub: organization SettingsDeveloper settingsOAuth AppsRegister a new application (prefer an organization-owned app so the org controls the secret). User-owned apps work; they are harder to hand off.

  2. Application name: something readers will see on GitHub’s consent screen (Team docs (Access)).

  3. Homepage URL: https://<team-name>.cloudflareaccess.com

  4. Authorization callback URL: https://<team-name>.cloudflareaccess.com/cdn-cgi/access/callback

  5. Copy Client ID. Generate a Client secret (shown once).

  6. In Zero Trust: IntegrationsIdentity providersAddGitHub. Paste the credentials and Save.

  7. Finish setup (dashboard) — GitHub consent. See Finish GitHub setup (human consent). Agents that POST the IdP skip this UI and get a fragile one-time URL instead.

  8. Test the login method (same browser order as Finish setup).

Simplified mock of GitHub Register a new OAuth application: Homepage URL and callback on the Zero Trust team domain
Figure 4. Mock: GitHub OAuth App fields Access needs
Simplified mock of GitHub Authorize: Organizations and teams read-only, email read-only
Figure 5. Mock: GitHub consent — orgs/teams and email, read-only
Simplified mock of Add GitHub IdP with redacted App ID and Client secret
Figure 6. Mock: Zero Trust GitHub identity provider (secrets redacted)

Finish GitHub setup (human consent)

Saving the IdP stores Client ID and secret. It does not grant Cloudflare read:org. Someone who can authorize the OAuth App must still click through GitHub.

Browser order (both accounts)
  1. Sign into the Cloudflare dashboard (Zero Trust lives there).

  2. In the same browser, unlock GitHub (complete 2FA if prompted) so Authorize does not interrupt the return to Cloudflare.

  3. Open Zero TrustIntegrationsIdentity providersGitHubFinish setup.

  4. On GitHub: Authorize organizations and teams (read-only) and email (read-only).

  5. Back in Zero Trust: Test.

"Sign into GitHub first" alone is not enough — you still open Zero Trust as a Cloudflare operator. GitHub unlocked in the same browser is the Cloudflare GitHub-IdP tip for 2FA.

Simplified mock of Identity providers list with Finish setup and Test on GitHub
Figure 7. Mock: Finish setup and Test on the GitHub login method

API create (POST /accounts/{account_id}/access/identity_providers) may also return a messages entry:

Finish enterprise enablement by going to
https://<team-name>.cloudflareaccess.com/cdn-cgi/access/enterprise-setup/<opaque>

Treat that URL as optional. It is one-shot (a later GET on the IdP does not return it). It can answer Cloudflare error 500 / Internal server error (a Ray ID, not a useful body). That is a broken setup token, not a wrong Client ID.

Redacted API message with enterprise-setup URL; 500 fallback to Finish setup
Figure 8. API enterprise-setup URL can 500 — use the dashboard

If the one-time URL 500s: ignore it. Use Finish setup in the dashboard, or open the protected hostname in a logged-out browser and complete GitHub Authorize there (with GitHub already unlocked if 2FA is on).

GitHub’s consent page lists every organization you belong to, not only the org in the Access Allow policy. Cloudflare asked for read:org; the screen is "which of your orgs may this OAuth App see?"

  • An organization-owned OAuth App is auto-granted for the owning org — expected.

  • Orgs that do not have OAuth App access restrictions may show as already granted when any member authorizes the app. That is GitHub’s default when restrictions are off (common on older orgs). Newer orgs often enable restrictions by default.

  • Extra orgs on that screen do not admit their members to the member docs. The Access policy still requires GitHub organization = your-org.

  • To stop auto-grant on an unrelated org: that org’s SettingsThird-party access → revoke the app, then enable OAuth App access restrictions. Deny / uncheck the org on Authorize when GitHub still offers a choice.

Simplified mock: owning org granted; unrelated org already granted when restrictions are off; Access policy still one org
Figure 9. Mock: consent lists every org you belong to

If the gate org restricts third-party OAuth apps, an org owner must approve this app or Access cannot see membership. Why that matters: How Cloudflare learns GitHub org membership.

One-time PIN as IdP

  1. IntegrationsIdentity providersAddOne-time PIN (skip if Zero Trust already created one).

  2. You still need an Allow policy that lists emails. Cloudflare will not send a PIN to an address the policy would reject.

Allow policy

  1. On the application (or under reusable policies), Add a policy.

  2. Action: Allow.

  3. Include (GitHub path): login method GitHub, and GitHub organization = your org slug.

  4. Include (PIN / Google path): Emails or Emails ending in.

  5. Optional Require: the same GitHub org, so a personal GitHub login that is not in the org cannot sneak through an overly broad Include.

Reuse the policy (or an Access Group of emails) on the pages.dev application so you do not maintain two allowlists.

Simplified mock of an Access Allow policy: login method GitHub and GitHub organization your-org
Figure 10. Mock: Allow when GitHub organization is your-org

Create the Access application

Do this after the GitHub IdP and Allow policy exist.

  1. Open Zero TrustAccessApplicationsAdd an application.

  2. Choose Self-hosted.

  3. Application name: something operators will recognize (Team docs).

  4. Public hostname: the custom domain. Repeat for pages.dev.

  5. Session duration: hours to a day is typical for docs; shorter if the content is sensitive.

  6. Attach the Allow policy from the previous section.

Until these applications exist, the origin is world-readable even if the git repo is private. Treat the hostname as unpublished.

Set up user accounts

This is the part that is easy to overbuild.

Readers (the member site)

You do You do not

Invite them to the GitHub org (Member is enough) — or add their email to the Access policy

Add them as a Cloudflare account member (that is for people who change DNS, Pages, and Workers)

Tell them the URL. First visit: Access login → GitHub / Google / PIN → the docs

Create an Antora, Pages, or "docs app" user table

After first successful login they appear under Zero Trust Team & ResourcesUsers and consume one seat

Hand them the Global API Key or a Pages deploy token

GitHub path, in order:

  1. They create a GitHub account if they do not have one (github.com signup — not Cloudflare).

  2. An org owner invites them to your-org (and to a team if the policy requires a team).

  3. They accept the invite.

  4. They open the member hostname and complete GitHub OAuth for the Access app (once).

  5. Later visits use the Access session cookie until it expires.

Simplified mock of the Access login page with Continue with GitHub
Figure 11. Mock: what a logged-out visitor should see

PIN path: add the address to the Allow policy, send them the URL, they request a code. No dashboard user is created for them unless they are also an operator.

Operators (Cloudflare dashboard)

Only people who must edit zones, Pages projects, or Access policies need a Cloudflare account invitation (Super Administrator / appropriate role). That list is much smaller than "everyone who reads team docs."

Removing someone

  1. GitHub path: remove them from the org (or the required team). Existing Access cookies last until the session expires; you can revoke the user in Zero Trust Users if you need them out immediately.

  2. PIN / email path: delete the address from the policy (and revoke the Zero Trust user).

  3. Do not leave them in the GitHub org if org membership is the only gate.

If they joined the org after a failed Access login, have them revoke the OAuth app under GitHub SettingsApplications and sign in again so GitHub sends a fresh org list.

Seats

  • First Access authentication creates an active Zero Trust user and takes a seat.

  • One person, many devices, still one seat.

  • Free plan: further logins are blocked when seats are full (not silently billed). Reclaim seats by removing users or enabling inactivity expiry (months, configurable).

  • CI that must fetch the protected site should use an Access service token, not a human seat.

Agents and API credentials

Composio (and wrangler) can list zones and deploy Pages with a Pages-scoped API token. That token is not enough to set up Access.

Pages API token: GET access/apps 200 empty, identity_providers and policies 403. Global API Key: Access reads and writes succeed
Figure 12. Pages-scoped token vs Global API Key (redacted statuses)

Typical split:

Secret What it can do

API token: Pages Write (+ Account Settings Read)

wrangler pages deploy. May GET /access/apps as an empty list. 403 Authentication error on identity providers, organizations, and policies.

Global API Key + account email, or an API token with Access: Apps and Policies and Access: Organizations / Identity Providers

List IdPs, create GitHub IdP (once you have OAuth secrets), create policies and applications.

GitHub personal access token / Composio GitHub

List org members. Cannot register an OAuth App.

In Composio, that is the Cloudflare toolkit (Global API Key + email), not Cloudflare API Key (token-only). Catalog names lie; match the connect form: Connect Cloudflare in Composio.

Do not create Access applications from an agent until the GitHub IdP exists. Deny-by-default will lock the hostname with no GitHub login button that can succeed.

Check that it is actually closed

  1. Incognito / logged-out browser: https://your.custom.domain/ → Access login (Continue with GitHub), not HTML.

  2. Same for https://your-project.pages.dev/.

  3. A member who matches the policy gets through and can use Lunr search (the index is behind Access too).

  4. A GitHub user not in the org (or an email not on the list) sees the Access deny page.