Raxx · internal docs

internal · gated

DET-BETA-007 — join geo-block bypass attempt

Rule ID: DET-BETA-007 Title: Repeated 403 blocked_region responses on /claim from the same tester — geo-block bypass probe Category: beta Last validated: 2026-06-18 (beta-phase2 catalog; grounded against as-built beta_join.py) State: liveFLAG_BETA_PHASE2_ACCESS is ON in prod; POST /api/beta/join/<token>/claim emits beta.join.geoblock_rejected audit events to audit_log when the geo-block triggers (written in backend_v2/api/routes/beta_join.py at step 3 of the claim processing order). Audit events are queryable from Raptor Postgres directly.

Why this detection exists

Geo-block invariant I-6 applies to the join-token path: even NDA-acknowledged invited testers in blocked regions cannot create accounts until compliance is resolved (OQ-2). The geo-block check in join_claim() runs at processing step 3 — after token re-verify and NDA check, before the atomic token consumption. It uses CF-IPCountry and CF-IPRegion headers set by the Cloudflare edge (stripped from client-supplied headers by CF Access, so they cannot be spoofed at the application layer).

When blocked, the endpoint: 1. Logs beta_join.claim geo_blocked email_hash=... country=... region=... at INFO level. 2. Writes a beta.join.geoblock_rejected audit event to audit_log with tester_email_hash, jti, ip_prefix, country, and region in the context JSON. 3. Returns 403 {"error": "blocked_region"}.

Repeated 403 responses from the same tester email hash within a 30-minute window indicate the tester is retrying from multiple IP addresses or attempting to appear from different country-of-origin — a VPN bypass attempt against the CF geo-signal. The detection operationalizes this pattern by counting beta.join.geoblock_rejected audit events grouped by tester_email_hash over time.

Telemetry source

Telemetry availability: audit_log is Raptor Postgres, queryable directly (no Heroku drain dependency). This detection has the strongest telemetry of the three DET-BETA-005/006/007 cluster.

Statistical method + baseline window

Event-count detection on the beta.join.geoblock_rejected audit stream:

Threshold + expected FP rate

Alert route

Escalation owner

CF geo-signal accuracy note

The geo-block reads CF-IPCountry and CF-IPRegion headers set by Cloudflare:

cf_country = (request.headers.get("CF-IPCountry") or "").strip().upper()
cf_region  = (request.headers.get("CF-IPRegion")  or "").strip().upper()

These headers are stripped and re-set by the CF edge on every request. They cannot be spoofed by the client when the application sits behind CF Access. The only bypass vectors are: (a) commercial VPN that routes through a non-blocked country's CF PoP, or (b) CF geo-DB miss for the tester's actual IP. Both produce the same observable: legitimate country header, not matching the tester's actual location. Detection distinguishes these by the ip_prefix change between events.

Test fixture / synthetic positive

See _fixtures/join_geoblock_bypass_positive.json — two synthetic beta.join.geoblock_rejected audit events for tester_email_hash=synth-tester-blocked-01 within 18 minutes: first event country=DE (Germany, EU) from ip_prefix=192.0.2.0/24, second event country=FR (France, EU) from ip_prefix=198.51.100.0/24 — representing a tester cycling VPN exit nodes to find a non-blocked EU country, unsuccessfully.

Postgres query (direct — no drain required)

-- Find tester email hashes with >= 2 geoblock_rejected events in any 30-minute window
SELECT
    context->>'tester_email_hash' AS email_hash,
    context->>'country' AS country,
    context->>'region' AS region,
    ip_prefix,
    created_at
FROM audit_log
WHERE action = 'beta.join.geoblock_rejected'
  AND created_at > now() - interval '24 hours'
ORDER BY context->>'tester_email_hash', created_at;

Group the results by email_hash. Any email_hash appearing 2+ times within a 30-minute span triggers the rule. Country changes between rows on the same email_hash confirm VPN cycling.

What NOT to do