GitNexus/render.yaml
Shifra Williams 356032e2f6
feat(render): private-service topology, docs, and a CI check on render.yaml (#10)
* feat(render): private-service topology, docs, and a CI check on render.yaml

Rewrites the Blueprint around the topology Abhigyan called for: `serve` on a
private service, the public web service reverse-proxying /api/* to it, and the
generated token gating that edge.

`gitnexus-server` becomes a `pserv`, so it has no public URL. `gitnexus-web`
gets `GITNEXUS_UPSTREAM_URL` from `fromService … property: hostport`,
`GITNEXUS_SERVE_AUTH_TOKEN` with `generateValue: true`, and
`GITNEXUS_PROXY_TRUST_XFF: 1` for Render's load balancer. `autoDeploy: true`
becomes `autoDeployTrigger: off` so a push cannot restart an indexing server
mid-analyze.

Four variables come out:

- `GITNEXUS_PUBLIC_ORIGIN`: under the proxy the browser is same-origin with the
  web service and Origin is stripped, so it has no job — and since #2820
  `assertServeAuthForPublicOrigin` refuses to start when it is set without
  serve-native auth. The old topology no longer boots.
- `GITNEXUS_BACKEND_URL`: docker-server.mjs falls back to RENDER_EXTERNAL_URL,
  which is this service's own origin.
- `AZURE_DEVOPS_URL` / `AZURE_DEVOPS_PAT`: per Abhigyan, "a stranger can POST
  /api/analyze an Azure URL, have the server spend the operator's credential,
  then read the private repo back through the ungated /api/file and /api/grep."
  The edge token closes the stranger path, but the knob still should not ship in
  a one-click prompt. Operators who want Azure DevOps set both in the dashboard,
  on a deploy they have already secured.

README drops "with no API keys required" — it described missing authentication
as a convenience, and the deploy now generates a token — and replaces it with
the token-retrieval flow. It also corrects `plan` versus `sizeGB`: `plan` sets
RAM, `sizeGB` only matters when the disk fills. Both the README and a new
SECURITY.md section state plainly that the proxy strips Origin, so the CSRF
guard does nothing for proxied traffic and the token is the only control — not a
second layer behind the guard. SECURITY.md enumerates the routes a token holder
reaches with no origin guard, and notes that `POST /api/mcp` has no `authToken`
of its own as a pre-existing gap in `serve`.

Nothing in CI parsed render.yaml, so the couplings the file documents were
unenforced. `gitnexus/test/unit/render-blueprint.test.ts` asserts them:
mountPath equals GITNEXUS_HOME, PORT matches Dockerfile.cli's CMD, the server
stays a `pserv`, GITNEXUS_PUBLIC_ORIGIN appears in no YAML, the token is
generated, no AZURE_DEVOPS_* key, and the schema pragma is intact. Each was
confirmed to fail when its invariant is deliberately broken.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(readme): state what the Render deploy costs

The Deploy to Render section described the topology and the token but not
the bill. At the Blueprint's defaults it is ~$35/month: $25 for the
server's `standard` instance, $7 for the web service's `starter`, $2.50
for the 10 GB disk. Neither service scales to zero, which matters more
than the number — an idle deploy costs the same as a busy one.

Also trims the section's em dashes and drops "access token" to "token"
where the antecedent is one sentence away.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* chore: update readme

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 15:08:20 -07:00

87 lines
3.9 KiB
YAML

# yaml-language-server: $schema=https://render.com/schema/render.yaml.json
#
# One-click GitNexus deploy: a private API server, plus the public web UI that
# proxies /api/* to it over Render's private network.
#
# SECURITY: the web service's URL is discoverable (onrender.com names appear in
# CT logs). The generated GITNEXUS_SERVE_AUTH_TOKEN is the only access control —
# the proxy strips Origin, so the server's CSRF guard sees no proxied traffic,
# and rate limits bound cost, not access. Anyone holding the token can read
# every indexed repo. See SECURITY.md § Hosted Deploys on Render.
previews:
generation: 'off'
projects:
- name: gitnexus
environments:
- name: production
services:
# Private: no public URL. `serve` has no authentication of its own.
- type: pserv
name: gitnexus-server
runtime: docker
# Private networking needs one shared region.
region: oregon
# plan sets RAM: standard 2GB, pro 4GB. Indexing is memory-bound.
plan: standard
dockerfilePath: ./Dockerfile.cli
dockerContext: .
# No dockerCommand — the image's CMD already binds $PORT, and Render
# re-wraps dockerCommand in a shell, so `sh -c` exits 127.
# No healthCheckPath either: Render rejects one on a pserv. The
# proxy's connect-retry covers the restart window instead.
#
# Don't restart an indexing server on a push.
autoDeployTrigger: 'off'
# mountPath must equal GITNEXUS_HOME in Dockerfile.cli.
disk:
name: gitnexus-data
mountPath: /data/gitnexus
sizeGB: 10
envVars:
# Dockerfile.cli's CMD binds this.
- key: PORT
value: 4747
# One hop: gitnexus-web's proxy, which writes X-Forwarded-For
# itself. A count rather than the default ranges (loopback,
# linklocal, uniquelocal), which assume Render's private network
# is RFC1918. `true` is rejected outright.
- key: GITNEXUS_TRUST_PROXY
value: 1
# Deliberately unset: GITNEXUS_PUBLIC_ORIGIN, which has no job
# behind the proxy and makes `serve` refuse to start
# (assertServeAuthForPublicOrigin); AZURE_DEVOPS_URL /
# AZURE_DEVOPS_PAT, since /api/analyze would spend the PAT and
# /api/file could read the private repo back. Set the Azure pair
# in the dashboard if you want it.
# Public: the UI, plus a token-gated same-origin proxy to the server.
- type: web
name: gitnexus-web
runtime: docker
region: oregon
plan: starter
dockerfilePath: ./Dockerfile.web
dockerContext: .
healthCheckPath: /
autoDeployTrigger: 'off'
envVars:
# host:port, scheme-less; the proxy prefixes http://. Set without
# the token below, docker-server.mjs refuses to start.
- key: GITNEXUS_UPSTREAM_URL
fromService:
type: pserv
name: gitnexus-server
property: hostport
# Bearer token for every /api/* request. Copy it from this
# service's Environment tab into the UI's settings panel; rotate by
# editing it here and redeploying.
- key: GITNEXUS_SERVE_AUTH_TOKEN
generateValue: true
# Render's load balancer is the one hop in front and appends the
# real peer. The proxy ignores inbound XFF by default.
- key: GITNEXUS_PROXY_TRUST_XFF
value: 1
# Deliberately unset: GITNEXUS_BACKEND_URL. docker-server.mjs falls
# back to RENDER_EXTERNAL_URL, this service's own origin.