· 8 min read

Cloudflare Shipped One-Click Auth for Workers on August 14. If You Have Ever Shipped an Internal Tool to a workers.dev URL, Read the Part About Previews.

On August 14 Cloudflare shipped Access for Workers, which lets you attach a Cloudflare Access policy directly to a Worker instead of to a hostname. The policy then applies everywhere that Worker is reachable: routes, custom domains, workers.dev subdomains, and preview URLs. The blog post's title is about vibe-coded internal apps, which is honest marketing, because that is exactly the pile this is aimed at.

I have four internal Workers. Three of them had no auth at all, on the reasoning that nobody knows the URL. That reasoning is wrong and I knew it was wrong, and the reason I never fixed it is that wiring auth onto a 60-line admin tool used to cost more effort than the tool did.

The preview URL is the actual problem

Route-based Access has existed for years and it works. The failure mode is not that Access is bad, it is that Access was attached to a hostname while your Worker is reachable from more places than that hostname.

Every wrangler deploy with a version can produce a preview URL. Every workers.dev subdomain is live the moment you deploy unless you explicitly disabled it. If your Access policy covers admin.yoursite.com and your Worker is also answering on my-admin.yourname.workers.dev and on some preview alias you generated in March, you have protected one of three doors.

Attaching the policy to the Worker inverts that. The Worker is the thing being protected, so it stays protected when its routes change, when a new preview gets generated, and when you add a custom domain six months from now and forget the auth step. That default is the part I care about, more than the one-click framing.

What the code looks like

Once Access is on, every authenticated request carries identity on the context object. You call ctx.access.getIdentity() and get the user's email, name and groups back. No JWT parsing, no fetching Cloudflare's public keys, no signature verification you wrote at midnight and never tested.

export default {
  async fetch(request, env, ctx) {
    const identity = await ctx.access.getIdentity();
    if (!identity) return new Response("Unauthenticated", { status: 401 });
    return new Response(`Hello ${identity.email}`);
  },
};

That block is the whole thing. Compare it to the manual version, which is validating a JWT against a rotating JWKS endpoint, checking the audience tag, handling clock skew, and getting the failure case right so that a validation error does not accidentally read as a successful auth. I have written that code. I got the audience check wrong the first time.

Local testing is handled through a dev block in wrangler.jsonc:

{
  "access": {
    "dev": {
      "aud": "my-app",
      "identity": { "email": "admin@example.com" }
    }
  }
}

With that in place wrangler dev hands your Worker that identity through ctx.access, so you can develop the signed-in path without deploying. Delete the block and you get the unauthenticated path. That is a small detail and it is the one that decides whether people actually adopt this, because auth you cannot test locally is auth you skip.

The switch that matters more than the feature

Cloudflare also lets you make every Worker on the account private by default, so each existing and newly created Worker requires sign-in before anyone reaches it, with per-Worker bypasses for the ones that are supposed to be public.

That is the right default and almost nobody would have chosen it manually. The reason my three internal tools were open is not that I decided they should be open. It is that "open" was the default and securing them was an action I had to remember to take, per tool, forever. Flipping the polarity so that public is the exception you declare is worth more than any amount of one-click convenience.

The obvious caution: if you flip this on an account that also hosts public Workers, you will break them until you add the bypasses. Read your Worker list before you touch the switch, not after.

Where this does not apply

This is auth for you, your collaborators and anyone in your Access policy. It is not a customer-facing identity system. If you are building a product where users sign up, this is not that, and using it as that would be strange.

It is also Cloudflare-specific in a way the rest of the Workers platform mostly is not. A Worker with a fetch handler is portable enough. A Worker that reads identity off ctx.access is not, and if you ever move that code you are reimplementing auth from scratch. For an internal dashboard that will never leave Cloudflare, I do not care. For anything that might migrate, I would keep the identity read behind a small function of my own so there is one place to change.

The audit I would run today

This takes about ten minutes and does not require the new feature.

Open your Workers list in the Cloudflare dashboard. For each Worker, note whether the workers.dev subdomain is enabled. That is the one people forget. Then, for each Worker you consider internal, open its URL in a private browsing window with no session. If it renders, it is public, regardless of what you assumed about obscurity.

I did this and found one Worker I had genuinely forgotten existed, a link-shortener admin page from February that was serving fine on its workers.dev address to anyone who guessed the name. It contained nothing dangerous. It also did not need to be on the public internet for six months.

What I would actually do

If you are already on Cloudflare, turn on Access for the internal Workers this week. The reason to do it now rather than adding it to a list is that the cost has dropped to roughly zero and lists like that do not get worked through.

If you are not on Cloudflare, do not migrate for this. But do run the private-window audit against whatever you do host on, because the underlying mistake is platform-independent. The specific thing I would check on Vercel or Netlify is preview deployment URLs, which are the same failure with different branding.

Where I could be wrong: I am treating "attached to the Worker" as strictly better than "attached to the route," and there are setups where route-level policy is the point, for example when one Worker serves several tenants that need genuinely different access rules. If that is you, this feature is a simplification you do not want. For the single-operator case where every internal tool should be visible to exactly one person, it is the correct shape.

Author

Sources

Stay in the Loop

Get new posts delivered to your inbox. No spam, unsubscribe anytime.

Newsletter coming soon. Set PUBLIC_CONVERTKIT_FORM_ID in .env to activate.

Related Posts