Self-hosting

Lock the dashboard with Access

Copy page
View as MarkdownThis page as plain text

Cloudflare Access puts a login in front of the editor and the API. Kestrel checks every request against that login as well, so a path left out of Access stays locked. The public pages readers see stay public.

In this guide, you set up Cloudflare Zero Trust, create one Access application that covers the editor and the API, and allow yourself in. Then you give the app two values from that application, and check that the login works.

1. Set up Zero Trust

Access is part of Cloudflare Zero Trust. Your account needs a Zero Trust organization before it can create an Access application. Skip this section if your account already has one.

  1. In the Cloudflare dashboard, select Zero Trust.

  2. Choose a team name. It becomes your team domain, YOUR_TEAM.cloudflareaccess.com, where your login page lives.

  3. Choose a plan. The Free plan covers a newsletter’s editors. Cloudflare asks for payment details even on the Free plan, and doesn’t charge them.

2. Create the Access application

The editor lives under /dashboard, and the API under /api. One application covers both, so they share one login.

  1. In Zero Trust, go to Access controls → Applications, and select Create new application.

  2. Select Self-hosted and private.

  3. Select Add public hostname, and enter your app’s hostname with the first path:

    • Subdomain: newsletter
    • Domain: example.com
    • Path: dashboard
  4. Add a second public hostname with the same subdomain and domain, and the path api:

    PathWhat it covers
    dashboardthe editor
    apieverything the editor and Claude read and change: posts, sends, subscribers, settings, the in-app docs, and the MCP server Claude Desktop and claude.ai connect to (/api/mcp)

    Each path covers everything under it. Leave every other path out. The home page, the archive, subscribing, unsubscribing, images, and your email provider’s webhooks must stay public.

  5. Add only these two public hostnames. If the form also shows an empty private hostname or IP row, remove it, or saving fails with an error naming use_clientless_isolation_app_launcher_url.

  6. Name the application after your app’s hostname, newsletter.example.com. Cloudflare suggests a name made from the subdomain, which won’t tell two newsletters apart.

  7. (Optional) Choose how long a sign-in lasts. The application’s session duration defaults to 24 hours, so everyone signs in again each day. You can set anything from immediately to one month, as Cloudflare’s session management describes. For example, a week may suit a publisher on their own computer.

3. Allow yourself in

Every Access application denies everyone until a policy allows them.

  1. Under Access policies, create a new policy:

    • Policy name: the publication’s name and publishers, such as Example publishers
    • Action: Allow
    • Include: the Emails selector, with your email address
  2. Select Create.

Cloudflare’s own login is on by default, and admits members of your Cloudflare account, so the email in the policy you made should be one. To let someone sign in another way, such as a one-time code by email, add it under Zero Trust → Integrations → Identity providers.

4. Give the app its two values

The app verifies each login against your team domain, and checks that it was issued for this application.

  1. Find your team domain under Zero Trust → Settings. It looks like YOUR_TEAM.cloudflareaccess.com.

  2. Find the application’s audience tag. Under Access controls → Applications, select Configure on your application. On the Additional settings tab, copy the Application Audience (AUD) Tag.

  3. Store the team domain as a secret. The command prompts you for the value. Enter the hostname alone, without https://:

    npx wrangler secret put ACCESS_TEAM_DOMAIN --env production
  4. Store the audience tag the same way:

    npx wrangler secret put ACCESS_AUD --env production

    A secret takes effect as soon as you store it, with no redeploy.

  5. (Optional, recommended) Limit the editor to named people. The Access policy decides who can sign in. This list keeps that true if the policy is widened later, for another app or by mistake. Store the list of addresses allowed in:

    npx wrangler secret put ACCESS_ALLOWED_EMAILS --env production

    At the prompt, enter the addresses separated by commas:

    you@example.com, publisher@example.com

    Spaces and capital letters don’t matter. Without the list, the app lets in anyone your Access policy allows.

To let someone else into the editor, such as the publisher or a co-editor, add their email address to the policy from Allow yourself in. If you set ACCESS_ALLOWED_EMAILS, run the command again and enter the whole list with their address added, since the new value replaces the old one.

Check it

  1. In a private browser window, open https://newsletter.example.com/dashboard/. Cloudflare asks you to sign in.

  2. Sign in. The editor loads, and its sidebar shows the email address you signed in with.

  3. In the same window, open https://newsletter.example.com/api/whoami. It shows you as a signed-in person, verified through Access:

    {"principal":{"kind":"human","email":"you@example.com","client":null},"auth":{"mode":"access"}}
  4. The public pages still need no login:

    curl https://newsletter.example.com/health
    {"status":"ok","service":"kestrel"}

If a check fails:

  • The editor says the configuration needs a fix. Store the secret it names. While only one of the two is stored, /health answers 500 naming the other, and the public pages say the newsletter is temporarily unavailable.
  • Cloudflare lets you in, but the editor answers 401. Check the two secrets for a typo, and that ACCESS_ALLOWED_EMAILS, if you set it, includes your address. The same goes for anyone else you let in.
  • The editor says Access let you in but didn’t say who you are. Your login carried no email, which happens through a kind of policy the app doesn’t accept, such as a device check. Sign out, and sign in through the policy you made above.

Next, create an AWS account.