Skip to content

Deploy the managed cloud to Cloudflare with Alchemy (Workers, Containers, host tunnels) - #25

Open
michaelshimeles wants to merge 2 commits into
mainfrom
worktree-alchemy-cloudflare-0919a
Open

michaelshimeles wants to merge 2 commits into
mainfrom
worktree-alchemy-cloudflare-0919a

Conversation

@michaelshimeles

Copy link
Copy Markdown
Collaborator

What

Everything that can run on Cloudflare now does, declared in apps/web/alchemy.run.ts:

  • Site: SvelteKit on Workers via Alchemy. Vercel adapter, analytics and deploy path removed. prod owns the custom domain (www redirect), Web Analytics, and the R2 artifact bucket.
  • Control plane + gateway: Cloudflare Containers, each owned by a Durable Object behind one edge Worker (apps/web/edge/worker.ts) that fronts api., 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 trusts CF-Connecting-IP from the edge.
  • Hosts (ADR 0005, 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 by NEHEMIAH_HOST_TRANSPORT / NEHEMIAH_GATEWAY_HOST_TRANSPORT; overlay remains the default so ADR 0002 deployments are unchanged. nehemiahd can present an Access token toward the control plane's /internal routes.
  • Dockerfiles for both services; .github/workflows/deploy-cloudflare.yml deploys prod on main and pr-<n> site previews.

Verified

  • go vet + go test green for gateway/ and nehemiahd/; repo-wide check, lint, test green (all workspaces).
  • Both container images build (linux/amd64) and start under the exact production env the stack sets; the control plane proceeds to the database connection, the gateway listens.
  • Deployed stage live (site only) to the Goshen Labs account: https://boring-computers-website-live-n4ughwik5hat5bf5.michaelwasihun96.workers.dev

Not yet deployable: prod

  • boringcomputers.com is not a zone in the Goshen Labs Cloudflare account.
  • The available OAuth login lacks DNS, R2, Access and Web Analytics scopes; the runbook lists the API token permissions needed.
  • Production secrets (Postgres with sslmode=verify-full, Clerk, OTel, gateway credentials) must be provided as CI secrets/variables.
  • A wildcard below the apex (*.hosts.<site>, *.<preview domain>) needs Advanced Certificate Manager or its own zone.

Made with Cursor

michaelshimeles and others added 2 commits September 19, 2026 17:33
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>
@greptile-apps

greptile-apps Bot commented Sep 19, 2026 •

Copy link
Copy Markdown

RetriggerConfidence Score: 2/5

Not safe to merge: production secrets can be exposed to pull-request code, and deployed host tunnels will fail to start.

Fix All in CursorFindings

  1. P1 Security Protect Production Secrets ▶
  2. P1 Grant Tunnel Credential Access ▶
Fix with agent prompt
### Issue 1
.github/workflows/deploy-cloudflare.yml:66-103
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.

### Issue 2
infra/cloudflare/cloudflared.service:36-40
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.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Summary

  • Pull-request deployment code can access production Cloudflare and application secrets.
  • The cloudflared service cannot access its tunnel configuration or credentials under its configured service identity.
  • These failures must be resolved before merging.

Reviews (1) · Last reviewed commit: "docs(cloudflare): runbook for containers..."

Comment on lines +66 to +103
- 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 }}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 security 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.

Artifacts

Evidence from the check

  • 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.

Command output from the check

  • Captured output from executing the supplied check against `HEAD^` (37ce7a8), exiting 0 and showing the same PR-controlled deployment path with injected secrets.

Command output from the check

  • Captured output from executing the supplied check against `HEAD` (5444898), exiting 0 and showing the same PR-controlled deployment path with injected secrets.

View artifacts

T-Rex 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.

Fix in Cursor Fix in Claude Code Fix in Codex

Comment on lines +36 to +40
ExecStart=/usr/local/bin/cloudflared --no-autoupdate --config /etc/cloudflared/config.yml tunnel run
Restart=on-failure
RestartSec=5s
TimeoutStopSec=30s
DynamicUser=yes

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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

Evidence from the check

  • The executed shell script creates the documented root-owned modes and compares root access with an unprivileged DynamicUser analogue, demonstrating the permission boundary.

Command output from the check

  • Captured numbered service lines show the root-only installation modes, configured credential path, ExecStart path, and DynamicUser setting under test.

Command output from the check

  • 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.

View artifacts

T-Rex 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.

Fix in Cursor Fix in Claude Code Fix in Codex

@greptile-apps

greptile-apps Bot commented Sep 19, 2026

Copy link
Copy Markdown

Comments Outside Diff

These 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.

  • P1 Same-repository PR code runs with Cloudflare and application secrets ▶

    • Bug
      • A same-repository PR that changes a permitted path can trigger the deploy job. The job admits that PR at lines 45-48, checks out its PR merge commit at line 51, runs npm ci at line 56, and runs the checked-out web deploy script at line 67. The same process environment contains CLOUDFLARE_API_TOKEN and CLOUDFLARE_ACCOUNT_ID at lines 69-70, plus the application/deployment secrets at lines 96-103. Thus a contributor who can create a branch in the repository can alter package lifecycle scripts, the web deploy script, or deployment/build inputs and exfiltrate credentials before or during deploy.
    • Cause
      • The pull_request workflow deliberately permits same-repository heads but combines their checked-out code with repository secrets. The fork-only restriction does not protect against untrusted or compromised same-repository contributors/branches.
    • Fix
      • Do not expose production/deployment secrets while executing PR-controlled code. Split preview deployment into an unprivileged PR workflow using a least-privilege preview-only Cloudflare credential, or require trusted post-merge/manual deployment from protected code. If secrets are indispensable, check out only a reviewed protected ref and do not execute any PR-provided scripts or dependencies.
  • P1 DynamicUser cannot traverse or read cloudflared configuration and credentials ▶

    • Bug
      • The installation instructions create /etc/cloudflared as root:root mode 0750 and credentials.json as root:root mode 0600, but the unit runs with DynamicUser=yes. A focused execution reproduced that an unprivileged service identity cannot open either the config path (directory traversal denied) or credential path (traversal/read denied), so cloudflared cannot load its configured tunnel credentials and will exit; Restart=on-failure then causes restart attempts.
    • Cause
      • The unit has no fixed service user or supplementary group matching the root-owned directory, and DynamicUser assigns a transient non-root identity. The directory grants execute only to root and root group, while the credential grants read only to root.
    • Fix
      • Provision the files for the service identity instead: use a persistent User=cloudflared/Group=cloudflared with the directory and credentials owned/readable by that account, or retain DynamicUser=yes and use systemd-managed credentials/state mechanisms that make the credential available to the dynamic identity with appropriately restricted permissions. Ensure the config directory is traversable by that identity.

This branch has not been deployed

No deployments
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.

1 participant