Skip to content

Remove the legacy proxy token - #380

Merged
Brooooooklyn merged 8 commits into
security/1c-proxy-token-rotationfrom
security/1d-remove-legacy-proxy-token
Oct 7, 2026
Merged

Brooooooklyn merged 8 commits into
security/1c-proxy-token-rotationfrom
security/1d-remove-legacy-proxy-token

Conversation

@Brooooooklyn

@Brooooooklyn Brooooooklyn commented Oct 6, 2026 •

Copy link
Copy Markdown
Member

Summary

Every project now has rotatable v2.<gen>.<mac> proxy tokens and automatic rotation after upgrade (rotate-proxy-tokens --plan shows progress). This PR stops accepting and sending the original token, the plain HMAC of the project id under JWT_SECRET. That token could never be revoked, and it was signed with the same key that signs login JWTs and OAuth state.

Operators have no step to run before upgrading and no in-between release to install first: they upgrade, and the scheduled rotation moves any project still on the original token.

before:  tenant token = legacy (gen 0)  OR  v2.<gen>.<mac>   accept: gen >= floor
after:   tenant token = v2.<gen>.<mac>, gen >= 1             accept: gen >= max(floor, 1)

It also removes the fallback that let a tenant project token authenticate the proxy-only metadata routes. Those routes now take only the proxy's own x-void-proxy-internal key.

Changes

  • Token format and accept rule: platform/packages/api/src/proxy-token.ts
    • Legacy derive and verify code removed. Deriving generation 0 throws, and a 43-character legacy token is rejected before any MAC or KV work.
    • isAcceptedGeneration is now gen >= max(floor, 1), so a missing or zero floor still accepts every v2 token.
    • sendGenerations(0) is empty. LEGACY_PROXY_TOKEN_STATE is renamed to NO_PROXY_TOKEN_FLOOR.
  • Metadata routes take only the proxy key: platform/packages/api/src/internal-proxy-auth.ts
    • resolve-worker, resolve-resource and verify-prerender-build require x-void-proxy-internal and ignore x-void-internal.
    • /internal/revalidate still checks the forwarded project token against the floor.
  • Proxy stops sending legacy values: platform/packages/proxy/src/dispatch.ts, resolve.ts, proxy-token.ts
    • The x-void-internal header is no longer sent on metadata calls.
    • With no issued generation, dispatch logs a warning and returns null, so callers fall back as they do for a Worker without the /__void handler (REST where the route has one).
  • No re-derived tokens for Workers without a token binding: platform/packages/api/src/routes/projects.ts (rollback), deploy/recovery.ts (cutover replay), admin/backfill-queue-tokens.ts, operator/system-operations.ts
    • Such a Worker gets no token on its cron and queue copies, and a warning names the Worker and asks for a redeploy.
    • The backfill counts it as skipped and no longer takes a jwtSecret option.
  • Comment updates: platform/packages/api/src/id.ts, provisioning/cloudflare-api.ts, proxy-token-rotation.ts (generation 0 now means no token has been issued; the legacy token is never accepted).
  • Docs
    • docs/guide/platform/installation/maintenance.md: upgrade note, replacing the steps to rotate before upgrading. Rotation runs by itself after the upgrade, AI, email, ISR revalidation, prerendering and remote D1/KV/R2 can fail for a project until its turn, and --plan shows progress. Redeploying fixes a project right away.
    • docs/reference/cli/platform-operations.md: link to that note.
    • platform/meta/operations.md: next to the release order, the gate (0 legacy, 0 pending, 0 failed), why legacy: 0 alone is not enough, and the --all --yes step after a rollback below Rotate project proxy tokens when access changes #379; the note that the proxy and API no longer accept the legacy token; and the API-only rollback caveat. The __AI_TOKEN query now lives in Rotate project proxy tokens when access changes #379.
    • platform/packages/api/README.md, platform/packages/proxy/README.md: v2-only tokens, and an empty send when no generation has been issued.
  • Tests: API integration (api-internal-project-token, api-projects, api-revalidate, deployment-recovery, operator-operations, proxy-token-rotation), API unit (backfill-queue-tokens, proxy-token), and proxy unit (d1, dispatch, email, email-custom-domain, kv, proxy, r2, resolve).

Decisions

  • A project that was not rotated degrades instead of failing hard. Its app keeps serving pages. Calls that need the tenant token (AI, email, ISR revalidation) fail until the project is rotated or redeployed. Before its rotation starts, the proxy sends nothing rather than an invalid token, so single D1/KV/R2 calls keep their REST fallback; D1 batches have none and return 503. Once the rotation starts and until its active Worker is patched, the Worker rejects the new token, so remote D1/KV/R2 and prerendering get 403. It fixes itself when the scheduled rotation reaches it; redeploying or running rotate-proxy-tokens --project <id> --yes fixes it sooner, and the docs give both steps.
  • No step before upgrading and no in-between release. This release carries the rotation code and the scheduled rotation, so an install upgrading from any earlier release moves its projects by itself. Asking operators to rotate first, or to upgrade twice, would add manual steps to avoid a window that already fixes itself.
  • No re-derived legacy token on rollback, cutover replay or backfill. A Worker without a __VOID_PROXY_TOKEN binding never accepted the derived value, so writing one only produced 401s later. These paths now leave the token empty, warn, and ask for a redeploy. We do not refuse the rollback.
  • Workers that still hold the credential under __AI_TOKEN (deployed before the binding rename) cannot be rotated. The runbook gives a query to list the active ones so each is redeployed before Rotate project proxy tokens when access changes #379 reaches staging and production. Self-hosted installs cannot have them. We added no tooling to patch or drop them.
  • Rollback caveat is documented, not guarded. Rolling back only the API, to a release that does not check x-void-proxy-internal, while the proxy is on this release breaks metadata calls until both match. A self-hosted upgrade from such a release sees the same failure for the few seconds before the new proxy is live. We documented this and added no version guard, because the window is short and a guard would add friction to every upgrade.
  • No new secret. All keys are still HKDF-derived from JWT_SECRET.
  • Known limitation: Durable Object alarms on retained deployments that still hold an old token can fail after rotation. This is documented. No new code handles it.

Left as is on purpose:

  • Rolling back to a Worker that has no token is not refused. The rollback goes through, its token stays empty, and a warning asks for a redeploy.
  • No overlap between the old and new internal API header. Hosted deploys the proxy before the API, so there is no gap. A self-hosted install that jumps straight from a release before the proxy key can see metadata calls fail for the seconds between API and proxy activation; keeping the old header would keep the old token valid on those routes.
  • No compatibility window for projects not rotated yet. Their old token stops working when this release activates, until the automatic rotation reaches them. Accepting it until rotation finishes is what the release before this one already does.

Rollout

Merge gate:

  • rotate-proxy-tokens --plan on production shows 0 legacy, 0 pending and 0 failed. Staging is the canary and gets there first; it deploys on merge, so confirm it still shows the same. legacy: 0 alone is not enough: a project leaves it when its rotation starts or a deploy moves it to generation 1, while its older Workers still hold the legacy token until the rotation finishes. If either environment was rolled back below Rotate project proxy tokens when access changes #379 after it shipped, run rotate-proxy-tokens --all --yes there first: --plan cannot see a Worker redeployed with the legacy token during the rollback.

Hosted release order:

#379 on prod ──> prod --plan 0/0/0 ──> merge (staging) ──> advance prod ──> push release tag (npm)

This does not wait for a published #379 release. The __AI_TOKEN redeploys are done before #379 reaches each environment, so they are not part of this gate.

Self-hosted operators upgrade straight to this release; rotation runs by itself, and --plan shows progress (see the upgrade note in docs/guide/platform/installation/maintenance.md). A project that errors before its turn can be redeployed to fix it right away.

Do not roll back the API alone below the release that checks x-void-proxy-internal while the proxy runs this release.

Testing

Command Result
vp run check pass (31 packages typecheck)
vp run test --base security/1c-proxy-token-rotation pass: 694 files passed, 1 skipped; 13409 tests passed, 30 skipped
vp test run pass: 694 files passed, 1 skipped; 13409 tests passed, 30 skipped
vp lint pass
vp fmt --check pass

🤖 Generated with Claude Code

@Brooooooklyn
Brooooooklyn added this pull request to stack #381 October 6, 2026 21:33
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

🚀 Deploying Preview to Cloudflare 🚀

Preview URL: https://security-1d-remove-legacy-proxy-token-void-cloud.voidzero-docs.workers.dev (commit 2d57273)

This URL reflects your latest Preview deployment

Preview Deployments by commit

Status Deployment URL Commit Updated (UTC) See this deployment's details
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://02ddbc02-void-cloud.voidzero-docs.workers.dev 2d57273 2026-10-07T05:14:35.801Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://87386435-void-cloud.voidzero-docs.workers.dev 44a452f 2026-10-07T02:41:55.456Z Visit the dashboard ↗
  • Build: Success ✅
  • Deployment: Success ✅

View logs ↗
https://6d471e7e-void-cloud.voidzero-docs.workers.dev 903cbc3 2026-10-06T21:34:37.304Z Visit the dashboard ↗

@Brooooooklyn Brooooooklyn changed the title security/1d remove legacy proxy token Remove the legacy proxy token Oct 6, 2026

@cpojer cpojer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. I would expect the removal to remove code, not add 2x of the removals.
  2. Is it relevant to add this to the docs at all or can we remove the additions to the docs?

@Brooooooklyn
Brooooooklyn force-pushed the security/1d-remove-legacy-proxy-token branch from 903cbc3 to 44a452f Compare October 7, 2026 02:40
Brooooooklyn and others added 8 commits October 7, 2026 12:46
Rollback, cutover replay and the queue token backfill no longer derive a
token from JWT_SECRET when the target Worker has no __VOID_PROXY_TOKEN
binding. Such a Worker never accepted that value, so its cron and queue
copies now carry no token, a warning names the Worker and asks for a
redeploy, and the backfill counts the entry as skipped. The backfill no
longer takes a jwtSecret option.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The resolve-worker, resolve-resource and verify-prerender-build routes now
require x-void-proxy-internal and ignore x-void-internal, so a tenant project
token no longer authenticates them and they never read the token floor. The
proxy stops sending the project token on those calls. /internal/revalidate
still checks the forwarded project token.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Tenant tokens are now only v2.<gen>.<mac> with gen >= 1. The accept rule
is a valid MAC and a generation of at least max(floor, 1), so a missing or
zero floor still accepts every v2 token. Deriving generation 0 throws, and
a token in the original 43-character form is rejected before any MAC or
KV work.

The proxy sends nothing for a project with no issued generation: dispatch
logs a warning and returns null, so authorized callers fall back to the
REST path as for an old Worker.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upgrade guide now tells operators to rotate every project's original
credentials before upgrading, rotate once more right after, and how to
recover when they upgraded first. The CLI reference links to it.

The maintainer runbook covers the hosted release steps and why the API
must not be rolled back alone below the release that checks the
proxy-only metadata key. The proxy README describes only v2 tokens and
the empty send for a project with no issued generation.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ments

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The upgrade guide no longer asks operators to rotate before and after
upgrading. It says the platform moves each project off its original
credentials by itself, what fails until a project's turn comes, and that
--plan shows progress. The hosted runbook adds the gate for the release
that removes the legacy token: --plan must show 0 legacy, 0 pending and
0 failed, since a project leaves legacy as soon as its rotation starts.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Static projects can keep earlier Worker deployments for rollback, so the
scheduled run rotates them like any other deployed project.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@Brooooooklyn
Brooooooklyn force-pushed the security/1d-remove-legacy-proxy-token branch from 44a452f to 2d57273 Compare October 7, 2026 05:13
@Brooooooklyn
Brooooooklyn merged commit 2d57273 into main Oct 7, 2026
31 of 34 checks passed
@Brooooooklyn
Brooooooklyn deleted the security/1d-remove-legacy-proxy-token branch October 7, 2026 07:22
@Brooooooklyn

Copy link
Copy Markdown
Member Author

Folded into #379, which now ships rotation and legacy token removal in one release. GitHub marked this PR merged because its commits are now in its base branch security/1c-proxy-token-rotation; main did not change.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants