1 Reference Download Gateway
picklemustard edited this page 2026-09-07 15:49:27 -04:00

Private download gateway

cmd/gbb-gateway is an independently deployed process. It exposes only the artifact GET/HEAD route, /livez, and /readyz. No build runner, migration, Turnstile exchange, administrative route, or upload handler starts in this process. The API remains responsible for exchanging Turnstile tokens.

Route /api/v1/builds/{id}/artifacts/{name} to the gateway at the same HTTPS origin as the API. Existing frontend links and __Host- cookies keep working. Set GBB_DOWNLOAD_GATEWAY_ENABLED=true on the API at cutover: its old download handler then refuses delivery with 503 if a request reaches it accidentally. Do not send a download through both processes: the ingress routes it directly so the shared request allowance is spent once.

Authorization and delivery

Each request, including HEAD, checks shared request limits, a two-per-IP transfer lease, the 15-minute browser/IP-bound verification session, and PostgreSQL publication state. Only artifacts recorded on a succeeded build are served. Unknown or unpublished objects return 404; dependency failures return retryable 503; exhausted limits return 429. A URL alone grants no access. Losing the browser cookie or changing IP requires verification again. Existing transfers continue after verification expires; every resumed request verifies again.

The gateway streams private S3 objects, never redirects or exposes storage URLs. It accepts one byte range (closed, open-ended, or suffix), returns 206 and Content-Range, and rejects invalid/multiple ranges with 416. If-Range supports the SHA-256 ETag; a mismatch returns the whole object. Responses include Content-Length, Accept-Ranges, X-Artifact-SHA256 and an attachment filename. ?checksum=1 remains available after the same authorization checks. Historical artifacts without a valid recorded SHA-256/positive size fail closed; records are not rewritten to invent missing provenance.

S3 reads use HEAD followed by a conditional ranged GET against that object's ETag; a changed object, mismatched publication metadata, or a store that ignores the range fails closed. Storage policies must prohibit overwriting published objects. A transfer buffers 32 KiB and has 30-second storage/client idle bounds. Redis leases expire after 30 seconds and renew every ten seconds. Failed renewal or an exact-IP ban marker cancels storage reads and interrupts the client connection; partial transfers are never falsely completed.

All responses disable shared caching (private, no-store); ingress/CDN rules must preserve this and disable download caching/buffering. Never configure a public bucket or CDN storage redirect as a fallback.

Configuration and credentials

Build with go build ./cmd/gbb-gateway or podman build -f build/docker/gateway.Dockerfile -t gbb/gateway ..

The command takes -config PATH, defaulting to configs/gateway.yaml, and accepts the same database, redis and s3 blocks the API uses — including their discrete-field and *_file forms. See configs/gateway.example.yaml (configs/gateway.example.yaml in the repo) and Configuration for the precedence rules.

The file is optional. With none present the gateway is configured entirely from the environment, exactly as it was before the flag existed, so an existing deployment needs no manifest change. Every variable below is still read, and still wins over a value in the file:

  • GBB_DATABASE_DSN: dedicated PostgreSQL login with SELECT on jobs and narrowly scoped abuse-table writes (see grants (deploy/k8s/gateway/grants.example.sql in the repo)). Startup performs no migrations or token bootstrap.
  • GBB_HUMAN_REDIS_URL: the shared Redis endpoint with a dedicated ACL login. Equivalently the redis: block, whose password_file and tls.ca_bundle_path keep the ACL password out of the environment and trust a private CA.
  • GBB_HUMAN_TRUSTED_PROXIES: whitespace-separated immediate Traefik CIDRs, identical to API trust configuration. Untrusted peers cannot choose X-Real-IP.
  • GBB_GATEWAY_S3_BUCKET, GBB_GATEWAY_S3_ACCESS_KEY_ID, GBB_GATEWAY_S3_SECRET_ACCESS_KEY: required, dedicated read-only credentials.
  • GBB_GATEWAY_S3_ENDPOINT, GBB_GATEWAY_S3_REGION, GBB_GATEWAY_S3_PREFIX, GBB_GATEWAY_S3_PATH_STYLE=true: match the API's artifact storage layout.
  • GBB_GATEWAY_ADDR: defaults to :8080.
  • Startup now validates all of the above through config.LoadGateway, so a missing trusted-proxy range or an out-of-range limit is reported by name rather than as a bare "X is required".
  • Request-limit settings from anonymous verification must match all API/gateway replicas.

S3 permissions are GetObject for the artifact prefix and the bucket HEAD check (typically ListBucket). Explicitly deny writes/deletes and public access. The publisher retains its separate upload credential; do not reuse it here. The executable does not receive repository keys, a Turnstile secret, or a Kubernetes token. Redis needs the request-enforcement commands plus GET, PTTL, EXISTS, ZSCORE and ZREM, restricted to gbb:human:v2:* and gbb:enforcement:*. Use private/TLS dependency connections appropriate to the deployment.

Readiness checks PostgreSQL, Redis, and bucket access within three seconds. Liveness checks the process only. Dependency failures do not cause restart loops.

Deployment and remaining work

Deployment template (deploy/k8s/gateway/deployment.yaml in the repo) provides two replicas, non-root execution, read-only root filesystem, resource limits, and ingress-only network access from selected Traefik pods. Supply a dedicated ConfigMap and Secret named gbb-gateway, a digest-pinned image, and actual ingress selectors. Add egress restrictions for your DNS/PostgreSQL/Redis/object-store destinations. The template is deliberately separate from the existing Docker production base.

The routing example (deploy/k8s/gateway/ingressroute.example.yaml in the repo) uses Traefik PathRegexp and explicit priority. Set the real hostname and preserve the existing Cloudflare-only origin access, zone-specific authenticated origin TLS, forwarding trust, and ingress limits. Do not deploy the example as an independent unprotected public origin.

Transfer checks now use the durable abuse enforcement service and its shared atomic Redis snapshot. Ban escalation, session revocation, administrative unblock and index reconstruction are implemented. Production through-ingress large-download tests and read-only object-store credential policy verification remain required; no automatic deployment is performed.

Verification performed

The full Go suite passed. Race-enabled gateway and enforcement tests also passed against temporary PostgreSQL, Redis, and MinIO services. They exercised shared sessions across gateway instances, S3 range reads, publication checks, changed objects, IP binding, transfer concurrency, expiring leases, and ban markers. The gateway publication connection rejected database writes. Protocol tests covered HEAD, If-Range, malformed ranges, dependency failures, and interruption of a blocked transfer when renewal fails. Temporary services were removed.

These checks did not deploy the gateway or validate production ingress and storage credential policies.