Deploy the managed cloud to Cloudflare with Alchemy (Workers, Containers, host tunnels) - #25
michaelshimeles wants to merge 2 commits into
Conversation
Site: SvelteKit on Workers via Alchemy (replaces the Vercel adapter, analytics and deploy path); prod stage owns the custom domain, Web Analytics, R2 bucket. Control plane and gateway: Cloudflare Containers behind one edge Worker (api., gateway., preview wildcard). The Worker signs each caller's address for the control plane the way the gateway does; the gateway trusts CF-Connecting-IP from the edge. Hosts: keep their WireGuard address as identity but are reached through their own Cloudflare Tunnel at <address label>.<host tunnel domain> behind a Cloudflare Access service token (ADR 0005). Transport is selected by NEHEMIAH_HOST_TRANSPORT / NEHEMIAH_GATEWAY_HOST_TRANSPORT; overlay stays the default. nehemiahd can present an Access token toward the control plane. Dockerfiles for both services, a deploy workflow (prod on main, pr-<n> site previews), runbook and ADR.
Rewrite docs/cloudflare.md around the edge Worker, the two containers, and the Access boundary; make the cloudflared unit a per-host connector with its config; list the container settings in apps/web/.env.example. Co-authored-by: Cursor <cursoragent@cursor.com>
|
| - name: Deploy stage ${{ env.STAGE }} | ||
| run: npm run deploy -w web -- --stage "$STAGE" --yes | ||
| env: | ||
| CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }} | ||
| CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} | ||
| # Site settings, forwarded to the site Worker only when non-empty. | ||
| PUBLIC_NEHEMIAH_URL: ${{ vars.PUBLIC_NEHEMIAH_URL }} | ||
| PRIVATE_NEHEMIAH_URL: ${{ vars.PRIVATE_NEHEMIAH_URL }} | ||
| PUBLIC_CLERK_PUBLISHABLE_KEY: ${{ vars.PUBLIC_CLERK_PUBLISHABLE_KEY }} | ||
| PUBLIC_CLERK_FRONTEND_API: ${{ vars.PUBLIC_CLERK_FRONTEND_API }} | ||
| STATUS_CONTROL_PLANE_URL: ${{ vars.STATUS_CONTROL_PLANE_URL }} | ||
| STATUS_GATEWAY_URL: ${{ vars.STATUS_GATEWAY_URL }} | ||
| PUBLIC_SUPPORT_URL: ${{ vars.PUBLIC_SUPPORT_URL }} | ||
| PUBLIC_SUPPORT_EMAIL: ${{ vars.PUBLIC_SUPPORT_EMAIL }} | ||
| # Edge and container settings (prod stage only; defaults in apps/web/.env.example). | ||
| SITE_DOMAIN: ${{ vars.SITE_DOMAIN }} | ||
| API_HOSTNAME: ${{ vars.API_HOSTNAME }} | ||
| GATEWAY_HOSTNAME: ${{ vars.GATEWAY_HOSTNAME }} | ||
| PREVIEW_BASE_DOMAIN: ${{ vars.PREVIEW_BASE_DOMAIN }} | ||
| PREVIEW_ZONE_NAME: ${{ vars.PREVIEW_ZONE_NAME }} | ||
| HOST_TUNNEL_DOMAIN: ${{ vars.HOST_TUNNEL_DOMAIN }} | ||
| FLEET_ACCESS_TOKEN_ID: ${{ vars.FLEET_ACCESS_TOKEN_ID }} | ||
| R2_BUCKET: ${{ vars.R2_BUCKET }} | ||
| NEHEMIAH_SERVICE_VERSION: ${{ vars.NEHEMIAH_SERVICE_VERSION }} | ||
| NEHEMIAH_OTEL_ENDPOINT: ${{ vars.NEHEMIAH_OTEL_ENDPOINT }} | ||
| NEHEMIAH_DEFAULT_REGION: ${{ vars.NEHEMIAH_DEFAULT_REGION }} | ||
| NEHEMIAH_HOST_CIDRS: ${{ vars.NEHEMIAH_HOST_CIDRS }} | ||
| CLERK_ISSUER: ${{ vars.CLERK_ISSUER }} | ||
| CLERK_AUDIENCE: ${{ vars.CLERK_AUDIENCE }} | ||
| # Container secrets (prod stage only). | ||
| DATABASE_URL: ${{ secrets.DATABASE_URL }} | ||
| NEHEMIAH_HOST_CREDENTIAL_KEY: ${{ secrets.NEHEMIAH_HOST_CREDENTIAL_KEY }} | ||
| NEHEMIAH_GATEWAY_TOKEN: ${{ secrets.NEHEMIAH_GATEWAY_TOKEN }} | ||
| NEHEMIAH_GATEWAY_SECRET: ${{ secrets.NEHEMIAH_GATEWAY_SECRET }} | ||
| NEHEMIAH_DEVICE_CODE_PEPPER: ${{ secrets.NEHEMIAH_DEVICE_CODE_PEPPER }} | ||
| NEHEMIAH_OTEL_AUTHORIZATION: ${{ secrets.NEHEMIAH_OTEL_AUTHORIZATION }} | ||
| GATEWAY_OTEL_AUTHORIZATION: ${{ secrets.GATEWAY_OTEL_AUTHORIZATION }} | ||
| STRIPE_WEBHOOK_SECRET: ${{ secrets.STRIPE_WEBHOOK_SECRET }} |
There was a problem hiding this comment.
Same-repository pull requests can execute branch-controlled dependency and deployment code while production Cloudflare and application secrets are present in the job environment. The workflow permits same-repository PRs, checks out their merge commit, runs dependency lifecycle code, and invokes the checked-out deployment script with Cloudflare credentials and additional application secrets. A contributor or compromised branch could disclose those credentials or use them to alter production infrastructure. Use isolated preview credentials for pull requests, or require an approval-protected deployment that runs only reviewed, protected code.
How this was verified: The checked-out pull-request code path and its injected production secret environment were confirmed by an executed configuration check.
Artifacts
- The executed shell script reads the selected Git revision and asserts the PR trigger, same-repository gate, unchecked checkout ref, executable deploy path, and injected Cloudflare credentials; it shows the vulnerable source configuration.
- Captured output from executing the supplied check against `HEAD^` (37ce7a8), exiting 0 and showing the same PR-controlled deployment path with injected secrets.
- Captured output from executing the supplied check against `HEAD` (5444898), exiting 0 and showing the same PR-controlled deployment path with injected secrets.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: .github/workflows/deploy-cloudflare.yml
Line: 66-103
Comment:
**Protect Production Secrets**
Same-repository pull requests can execute branch-controlled dependency and deployment code while production Cloudflare and application secrets are present in the job environment. The workflow permits same-repository PRs, checks out their merge commit, runs dependency lifecycle code, and invokes the checked-out deployment script with Cloudflare credentials and additional application secrets. A contributor or compromised branch could disclose those credentials or use them to alter production infrastructure. Use isolated preview credentials for pull requests, or require an approval-protected deployment that runs only reviewed, protected code.
**How this was verified:** The checked-out pull-request code path and its injected production secret environment were confirmed by an executed configuration check.
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.| ExecStart=/usr/local/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run | ||
| Restart=on-failure | ||
| RestartSec=5s | ||
| TimeoutStopSec=30s | ||
| DynamicUser=yes |
There was a problem hiding this comment.
Grant Tunnel Credential Access
The installation commands create /etc/cloudflared as root:root with mode 0750 and the tunnel credential as root:root with mode 0600, but the service runs as an unrelated transient identity through DynamicUser=yes. That identity cannot traverse the configuration directory or read the credentials, so cloudflared cannot start the configured tunnel. The service will repeatedly restart and the control plane and gateway cannot reach the host.
Knowledge Base Used: Host runtime and virtual machine service
Artifacts
- The executed shell script creates the documented root-owned modes and compares root access with an unprivileged DynamicUser analogue, demonstrating the permission boundary.
- Captured numbered service lines show the root-only installation modes, configured credential path, ExecStart path, and DynamicUser setting under test.
- The executed script shows root can read both files while the unprivileged analogue receives Permission denied for config and credentials, confirming the service identity cannot access them.
Ran code and verified through T-Rex
Prompt To Fix With AI
This is a comment left during a code review.
Path: infra/cloudflare/cloudflared.service
Line: 36-40
Comment:
**Grant Tunnel Credential Access**
The installation commands create `/etc/cloudflared` as `root:root` with mode `0750` and the tunnel credential as `root:root` with mode `0600`, but the service runs as an unrelated transient identity through `DynamicUser=yes`. That identity cannot traverse the configuration directory or read the credentials, so cloudflared cannot start the configured tunnel. The service will repeatedly restart and the control plane and gateway cannot reach the host.
**Knowledge Base Used:** [Host runtime and virtual machine service](https://app.greptile.com/goshen-labs/-/custom-context/knowledge-base/boringcomputers/nehemiah/-/docs/host-runtime.md)
---
For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.
Comments Outside DiffThese findings sit on lines the diff does not cover, so they could not be posted inline. Each one leaves this list once its file changes.
|
What
Everything that can run on Cloudflare now does, declared in
apps/web/alchemy.run.ts:prodowns the custom domain (www redirect), Web Analytics, and the R2 artifact bucket.apps/web/edge/worker.ts) that frontsapi.,gateway.and the preview wildcard. The Worker signs each caller's address for the control plane (same HMAC the gateway uses) and passes through a valid gateway signature, so admission sees end users, not the Worker runtime. The gateway trustsCF-Connecting-IPfrom the edge.docs/nehemiah/adr/0005-cloudflare-containers-and-host-tunnels.md): keep their WireGuard address as identity; reached through a per-host Cloudflare Tunnel at<address label>.<host tunnel domain>(10.64.0.7→10-64-0-7.hosts.…) behind a Cloudflare Access service token. Selected byNEHEMIAH_HOST_TRANSPORT/NEHEMIAH_GATEWAY_HOST_TRANSPORT;overlayremains the default so ADR 0002 deployments are unchanged.nehemiahdcan present an Access token toward the control plane's/internalroutes..github/workflows/deploy-cloudflare.ymldeploysprodon main andpr-<n>site previews.Verified
go vet+go testgreen forgateway/andnehemiahd/; repo-widecheck,lint,testgreen (all workspaces).linux/amd64) and start under the exact production env the stack sets; the control plane proceeds to the database connection, the gateway listens.live(site only) to the Goshen Labs account: https://boring-computers-website-live-n4ughwik5hat5bf5.michaelwasihun96.workers.devNot yet deployable:
prodboringcomputers.comis not a zone in the Goshen Labs Cloudflare account.sslmode=verify-full, Clerk, OTel, gateway credentials) must be provided as CI secrets/variables.*.hosts.<site>,*.<preview domain>) needs Advanced Certificate Manager or its own zone.Made with Cursor