You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
|**Geo-blocking** (`server/middleware/geo-gate.ts`) | Blocks sanctioned countries via Cloudflare `CF-IPCountry`; fails closed (HTTP 451) if country is undetermined in prod |
329
329
|**RPC method whitelist** (`server/api/rpc/[chainId].ts`) | Only 15 safe read-only methods are proxied |
330
-
|**Rate limiting** (`server/utils/rate-limit.ts`) | Per-IP cost-based budgets (see below); fails closed (HTTP 403) if `CF-Connecting-IP` is absent in prod |
330
+
|**Rate limiting** (`server/utils/rate-limit.ts`) | Per-IP cost-based budgets (see below); fails closed (HTTP 403) if `CF-Connecting-IP`or the trusted-ingress marker is absent in prod |
331
331
|**Swap verifier validation** (`utils/swap-validation.ts`) | Validates swap verifier addresses against known config |
332
332
333
333
#### Rate Limiting
@@ -349,14 +349,17 @@ The app includes a built-in per-IP rate limiter as a defense-in-depth measure. D
349
349
350
350
**Production deployments must be behind Cloudflare.** This is a hard requirement, not a recommendation — two independent server features depend on it:
351
351
352
-
1.**Geo-gate** (`server/middleware/geo-gate.ts`) reads `CF-IPCountry` to enforce sanctioned-country blocks. Without Cloudflare, the country cannot be determined and all API requests are rejected with HTTP 451.
353
-
2.**Rate limiter** (`server/utils/rate-limit.ts`) uses `CF-Connecting-IP` as the trusted client IP. Without Cloudflare, `CF-Connecting-IP` is absent and all API requests are rejected with HTTP 403.
352
+
1.**Trusted ingress marker** (`server/utils/trusted-ingress.ts`) verifies that traffic reached the origin through the expected edge path before Cloudflare forwarding headers are trusted.
353
+
2.**Geo-gate** (`server/middleware/geo-gate.ts`) reads `CF-IPCountry` to enforce sanctioned-country blocks. Without Cloudflare, the country cannot be determined and all API requests are rejected with HTTP 451.
354
+
3.**Rate limiter** (`server/utils/rate-limit.ts`) uses `CF-Connecting-IP` as the trusted client IP. Without Cloudflare, `CF-Connecting-IP` is absent and all API requests are rejected with HTTP 403.
355
+
356
+
The trusted ingress must add `x-euler-edge-origin-secret` with `EDGE_ORIGIN_SECRET`, and strip or overwrite client-supplied `CF-*` forwarding headers plus `x-euler-internal-request`.
354
357
355
358
Bypass behaviour per environment:
356
359
357
360
| Environment | Geo-gate | Rate limiter |
358
361
|---|---|---|
359
-
|`prd`| CF required; fail-closed (HTTP 451) if absent. `DEV_GEO_COUNTRY`bypasses fail-closed if set. | CF required; fail-closed (HTTP 403) if absent. |
362
+
|`prd`|Trusted ingress and CF required; fail-closed (HTTP 403/451) if absent. `DEV_GEO_COUNTRY`is ignored. | Trusted ingress and CF required; fail-closed (HTTP 403) if absent. |
360
363
|`stg`| CF required; fail-closed (HTTP 451) if absent. `DEV_GEO_COUNTRY` bypasses fail-closed if set. | CF **not** required; falls back to `X-Forwarded-For`. |
361
364
|`dev`| CF not required; falls back to `DEV_GEO_COUNTRY`, then allows through if unset. | CF not required; falls back to `X-Forwarded-For`. |
Copy file name to clipboardExpand all lines: docs/geo-blocking.md
+6-2Lines changed: 6 additions & 2 deletions
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -51,7 +51,7 @@ When both collateral AND borrow vault in a pair are restricted, the pair is trea
51
51
52
52
The user's country is detected by sending a `HEAD` request to the application's origin and reading the `x-country-code` response header. The result is normalized to uppercase ISO 3166-1 alpha-2 (e.g. `US`, `DE`, `GB`).
53
53
54
-
The `x-country-code` response header is set by `server/middleware/cors.ts`, which reads Cloudflare's `CF-IPCountry` edge header (immutably set by Cloudflare's network). Any client-supplied `x-country-code` request header is stripped by `cors.ts` before processing, preventing bypass.
54
+
The `x-country-code` response header is set by `server/middleware/cors.ts`, which reads Cloudflare's `CF-IPCountry` edge header after the production trusted-ingress marker is verified. Any client-supplied `x-country-code` request header is stripped by `cors.ts` before processing, preventing bypass.
55
55
56
56
Detection is cached for 5 minutes to avoid repeated network calls.
A concurrency guard (`loadingCountry`) prevents duplicate in-flight requests if `loadCountry()` is called multiple times.
73
73
74
-
**Local development**: In development (`DOPPLER_ENVIRONMENT=dev`), Cloudflare is not in the request path so `CF-IPCountry`is never set. Set `DEV_GEO_COUNTRY=GB` (or any ISO country code) in `.env` to simulate a country for geo-block testing. Without it, the server allows requests through in dev rather than blocking.
74
+
**Local development and previews**: Outside production, Cloudflare is not always in the request path so `CF-IPCountry`may be absent. Set `DEV_GEO_COUNTRY=GB` (or any ISO country code) in `.env` to simulate a country for geo-block testing. Without it, the server allows requests through in dev rather than blocking. Production ignores `DEV_GEO_COUNTRY` and fails closed when Cloudflare country data is absent.
75
75
76
76
## Server-Side Geo-Gate
77
77
@@ -81,6 +81,10 @@ All API requests first pass through the server-side geo-gate, which applies the
81
81
82
82
The gate reads `CF-IPCountry` from the Cloudflare edge header. Special values `XX` (unknown IP) and `T1` (Tor exit node) are treated as an undetermined country. If the country cannot be determined **and** the environment is not `dev`, the request is rejected with HTTP 451 (fail-closed). In dev, unknown country is allowed through so local development is not blocked.
83
83
84
+
Production API traffic also depends on the trusted ingress boundary enforced by `server/middleware/cors.ts`, `server/middleware/geo-gate.ts`, and `server/utils/rate-limit.ts`. In `prd`, ingress must add `x-euler-edge-origin-secret` with the configured `EDGE_ORIGIN_SECRET` value and strip or overwrite client-supplied `CF-*` forwarding headers plus `x-euler-internal-request` before the request reaches the origin. Requests without this trusted-ingress marker are rejected before Cloudflare country or client-IP headers are trusted.
85
+
86
+
Server-internal `$fetch` calls use `INTERNAL_FETCH_HEADERS`, which carries a private `x-euler-internal-request` value generated by `server/utils/internal-headers.ts`. Those internal calls bypass the geo-gate and rate-limit edge checks without using public Cloudflare headers as a sentinel.
87
+
84
88
```text
85
89
Request → cors.ts (strip client x-country-code, set response x-country-code from CF-IPCountry)
0 commit comments