Repository navigation
feat(platform): add project Zero Trust protection - #335
Conversation
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f5247f901
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
29d889c to
c0fd4c1
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: c0fd4c1678
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ea85aa9ec
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
8ea85aa to
cbf24e0
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cbf24e0359
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3396cf85b2
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
e0cf0c6 to
24def13
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 24def131f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
c98c47e to
2597cb5
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: cf6fdc47ba
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8a2e1f939b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
8a2e1f9 to
de2907c
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d01d693994
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 93344e9e46
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Route rewriters that derive Zero Trust metadata from PostgreSQL now reject a cached route that belongs to another project (a reused slug or moved hostname), so a stale KV read cannot strip protection from a previous project's still-live Worker. Suspension fan-out still updates every owned route before it reports the rejection. Access application recovery adopts Void's own replacement when a recorded application is missing and one application holds its nonce-bearing name, so a lost create response no longer blocks reconcile or disable. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ation Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Describe what visitors see, the Access applications Void manages and how to recognize them, what dashboard edits do and how to repair them, visitor experience during changes, and caching for signed-in requests. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The Zero Trust API client and the dispatch Access verifier stored the
global fetch as a default field and called it as a method. The Workers
runtime rejects fetch called with a foreign `this` ("Illegal
invocation"), so every Zero Trust configure failed and every request to
a protected project would have been denied after sign-in.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The project wildcard Access application covers every host under the application domain, including the host the dispatch health probe uses. With Zero Trust enabled the probe received the Access login redirect, so the admin status showed dispatch as unhealthy and install resume, upgrade, repair, and domain changes failed their health checks. Health probes now use a reserved host, void-platform-health.<domain>. Void creates an exact-host bypass application for it before the wildcard and deletes it after the wildcard; dispatch answers only the health probe on that host and never routes it to a project. The slug is reserved. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The health host guard answered every non-probe request with 404, so a project that already used the void-platform-health slug went offline after upgrading, even with Zero Trust disabled. Dispatch now answers the token health probe first and routes everything else normally; a protected project on that host still needs a valid Access token, so the bypass application cannot expose it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The health exception covered the whole reserved hostname, so a project that already used that slug relied only on dispatch's cached routing metadata while it became protected. The exception now covers only /health on that host; every other path stays under the project wildcard at the edge, and dispatch never routes a /health request there to a project. Reconcile narrows an exception that still covers the host. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…apps Uninstall disabled platform traffic even with project Zero Trust enabled. The API then could no longer disable Zero Trust, and the Void-managed wildcard Access application kept requiring sign-in for every hostname under the domain. Uninstall now reads the Zero Trust state from the platform database and refuses, before any change and in --plan, while Zero Trust is enabled, an operation is pending, Void-managed Access applications remain, or the state cannot be read. The message lists the applications and the command to disable first. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…raffic Uninstall checked project Zero Trust once, under the lifecycle lock. The API's Zero Trust writers never take that lock; they take the `zero-trust:configuration` advisory lock. A configure request or project protection change could therefore commit after the check and create Access applications while uninstall waited for confirmation or ownership checks, and uninstall then disabled traffic without noticing. After confirmation, uninstall now takes that lock exclusively as a session lock on its lifecycle connection, checks again, and holds it until the routing disable write. Writers that arrive in between wait and commit only after traffic is disabled. Uninstall refuses when no database session can hold the lock. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Project teardown marks a project deleting before several fallible cleanup steps and removes its Access applications near the end. When it stopped partway, the project's application handles stayed recorded, and neither Zero Trust disable nor hourly maintenance touches deleting projects. The uninstall refusal still said hourly maintenance would clear them, so an operator who deleted the applications in the dashboard was refused forever. The check now reads each project's slug and status. For a deleting project it asks the operator to rerun `void project delete`, whose Access cleanup treats an already removed application as done and whose cascade then drops the record. The hourly maintenance hint is shown only for the other projects. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…state Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…was skipped Adding the first application domain runs a Zero Trust reconcile operation that publishes the domain once no project is left to reconcile. Projects that hit a project-specific failure are marked skipped and excluded from that check, so the domain was published anyway. A skipped protected project's route then has no audience for its new hostname, so dispatch answers 403 there, and a skipped public project has no bypass for it, so Access asks visitors to sign in. Completion now refuses to publish an application target while any active project is skipped. It clears the skip markers so the next run retries those projects, records an error naming them, and releases the lease. The operation is then in error, so the two-minute tier leaves it alone and the hourly tier, `void platform zero-trust reconcile` or a rerun of the domain command retries it. workers.dev URLs keep working meanwhile. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… its deletion Removing a custom domain deletes the hostname, its route and its row in one transaction, then reconciles the project's Zero Trust state. The project's custom domain application in Access still lists the hostname until that reconcile rewrites it. If the reconcile failed before its checkpoint, or the request stopped after the deletion committed, the row stayed `ready`. Hourly maintenance only retries rows that are not ready, so the freed hostname could stay in the old project's Access application until something else reconciled that project. A new owner's visitors would then meet the installation's sign-in, and a protected new owner would answer 403, because dispatch checks the new owner's audience. The deletion transaction now takes the shared Zero Trust lock and marks a settled row that records a custom domain application as `reconciling`. The follow-up reconcile settles it as before; if it fails or never runs, hourly maintenance rewrites the application. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…l lock Uninstall held the Zero Trust configuration lock from its final check until it wrote the routing KV disable key, then released it. A Zero Trust request that passed the KV disable check before that write, or at a colo that had not seen it yet, could be waiting on the lock. After the release it got the lock and could create or record an Access application that the final check never saw, leaving it behind once the platform was removed. Uninstall now writes the `platform.uninstall` platform setting on its locked session right after the final check passes, before it disables traffic. `lockZeroTrustConfiguration` reads that setting in a new statement once it gets the lock, in both modes, and refuses while it exists. Every Zero Trust writer, deployment and route publication takes the lock through that function. If uninstall stops before it starts the disable write, it removes the setting before releasing the lock, so the platform keeps working. After that point, installing or enabling the platform removes it just before deleting the disable key, which also clears a setting left in a reused database. No migration is needed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Removing a custom domain deleted the Cloudflare custom hostname and the `domain:` routing entry inside its transaction, and only then took the shared Zero Trust lock. If uninstall had written its `platform.uninstall` record by then, the lock refused and rolled back the transaction, but the hostname and route were already gone. The domain row stayed active, so if the platform was later repaired or enabled, the hostname answered 404 until another deployment wrote the route again. The refusal also surfaced as a generic 500. The transaction now takes the shared lock right after the project row lock, before any cleanup, in the same order as the other project Zero Trust writers. Uninstall needs the lock exclusively to write its record, so it cannot appear while the removal holds it. A refusal now returns 409 with the reason and leaves the hostname, route and row untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…in its cleanup Activation reconciles a protected project's Zero Trust state before it publishes the route, so a pending custom domain can already be listed in the project's custom domain application. When the hostname never validates, the hourly cleanup of pending domains deletes the row and only then tries to reconcile. If that attempt failed, the project row stayed `ready`, hourly maintenance never picked it up, and the freed hostname stayed in the old project's Access application. The cleanup transaction now takes the shared Zero Trust lock right after the project row lock and marks a settled row that records a custom domain application as `reconciling` before it deletes the domain, the same as user domain removal. Taking the lock before the Cloudflare and routing cleanup also means a refusal during uninstall leaves the domain untouched. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
93344e9 to
02d88f3
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 02d88f3b86
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
Summary
Self-hosted Void can put Cloudflare Access in front of project hostnames. Platform administrators turn it on once. Project owners and project admins choose protected or public for each project. Readers can see the state.
settingsagain under the project row lock.availableuses the same check as the mutation gate.errorand skipped, so one project cannot block an installation operation. Failed operations retry hourly, not every 2 minutes. Deploys and rollbacks get an early 409 that explains the next step.status --check, or "Check with Cloudflare").void project zero-trust status|protect|public|reconcilevoid platform zero-trust status [--check]|configure|reconcile|disable|project-*--existing-projectsis needed only when you enable Zero Trust.0052_project_zero_trust, plus a rollback-safe entry for0.21.3.Effect on direct Cloudflare deploys
Direct deploys get no project protection. However, requests that carry
Cf-Access-Jwt-Assertionnow skip the shared ISR cache. The shared cache policy version also moved from 1 to 2, so old ISR entries render once after the upgrade.Open product decisions (not changed here)
void platform zero-trust disablefirst.Verification
vp test run: 607 files, 11,310 passed, 21 skipped.vp run build, andvp run build:platformpass.🤖 Generated with Claude Code