Raxx · internal docs

internal · gated

WAF Threat Model — Raxx Platform

Per-Surface Analysis + Rule Recommendations

Date: 2026-05-11 UTC Author: security-agent (nightly ops run) Status: Research output — NOT implementation. Architect agent consumes Section 2 for CF WAF + Heroku origin guard + app-layer rate-limit design. sre-agent implements. Paired with: software-architect integration design (coordinate via PR comment once both PRs open) Sources: - docs/security/web-surface-posture.md — surface inventory baseline - docs/security/auth-posture.md + ADR-0031 — surface class framework - docs/architecture/adr/0042-auth-unification-hybrid-model.md — CF Access / Google IDP posture - docs/architecture/durable-email-delivery.md — Postmark/API-Gateway inbound path - docs/architecture/raxx-app-track-b.md — origin guard + FLAG_ENFORCE_CF_ORIGIN status - docs/architecture/queue/api-contract.md — Queue service auth/rate-limit contract - backend_v2/api/middleware/cloudflare_origin_guard.py — existing origin guard impl - backend_v2/api/middleware/rate_limiter.py — existing app-layer rate limiter - OWASP Top 10 (2021): https://owasp.org/www-project-top-ten/ - Cloudflare WAF Managed Rulesets: https://developers.cloudflare.com/waf/managed-rules/ - OWASP CRS 4.x: https://coreruleset.org/ - CISA Advisory AA22-137A (web application threats): https://www.cisa.gov/news-events/cybersecurity-advisories/aa22-137a


Executive Summary

Three surfaces stand out as highest-risk before launch:

  1. api.raxx.app (Raptor)FLAG_ENFORCE_CF_ORIGIN is currently OFF on raxx-api-prod, meaning the .herokuapp.com origin URL is a live bypass of all CF WAF rules, CF Access, and rate limits. Auth endpoints (registration, login, backup-code redemption) and trading endpoints are both reachable without transiting Cloudflare.

  2. API Gateway (tzkznyft9c.execute-api.us-east-1.amazonaws.com/v1/email/inbound) — Postmark webhook receiver has no Cloudflare WAF layer. Signature validation exists in design (SSM /raxx/email/postmark_inbound_webhook_token) but the Lambda shim to validate it (SC-E10) has not shipped as of 2026-05-11. Until SC-E10 lands, any party that can reach the API Gateway URL can inject arbitrary payloads into the email SNS topic.

  3. raxx-queue-{prod,staging}.herokuapp.com (Queue C++ service) — Queue owns customer identity, sessions, RBAC, and audit. The .herokuapp.com origin URL bypasses Cloudflare. No CF Access gate is documented for Queue. If Queue serves any unauthenticated public endpoints (registration, login/begin), they are reachable directly.


Section 1 — Surface Inventory + Threat Exposure Table

# Surface Hostname Exposure Auth Tier Sensitive Data Throughput Top 3 Threats
S1 Marketing site getraxx.com CF-Access-gated pre-launch; public at launch (2026-05-23 UTC) anon (public post-launch) None Low (a) Pre-launch content scraping / competitive intel, (b) defacement via CF Pages deploy pipeline, (c) DDoS volumetric (low ROI pre-launch)
S2 Antlers app app.raxx.app Public (Class 1) customer (WebAuthn passkey) Customer trading data (portfolio, positions, orders), PII (email) Medium (a) Account enumeration via signup/login error timing, (b) credential stuffing via auth endpoints, (c) session token theft via XSS if CSP is misconfigured
S3 Raptor API api.raxx.app Public (Class 1); origin URL currently bypasses CF customer (session JWT) + service-to-service (Queue tokens) Customer trading data, Alpaca paper credentials (indirect), PII High (API hot path) (a) Brute-force / credential stuffing on /api/v1/auth/*, (b) API scraping (market data, positions, portfolio), (c) Heroku origin bypass (direct to raxx-api-prod.herokuapp.com skips all CF controls)
S4 Operator console console.raxx.app CF-Access-gated (Class 2 transitional) operator (CF Access + passkey/Google OAuth) All customer PII, all secrets via Velvet, RBAC grants, audit log Low (a) CF Access bypass via .herokuapp.com origin URL (FLAG_ENFORCE_CF_ORIGIN off), (b) Credential stuffing on Google OAuth fallback path, (c) Admin action replay / CSRF on privileged mutations
S5 FreeScout tickets tickets.raxx.app CF-Access-gated (Class 3, strong MFA) operator (CF Access strong MFA + FreeScout native) Customer support PII, ticket content, potential Alpaca/service discussion Low (a) CF Access bypass (FreeScout on Lightsail — origin URL reachable directly?), (b) FreeScout auth brute-force if CF gate bypassed, (c) Ticket content injection via email webhook forgery
S6 Infisical vault vault.raxx.app CF-Access-gated (Class 3, strong MFA) operator + machine identities (CF Access + Infisical tokens) All platform secrets (Alpaca keys, Postmark token, DB URLs, service tokens) Low (a) Machine-identity token exfiltration via CF Access bypass, (b) Stale/over-privileged service tokens (>90 day rotation), (c) Infisical API brute-force if CF gate bypassed
S7 Status page status.raxx.app Public (Class 1) anon (read-only) None (operational status only) Low (a) Defacement of status page to spread false incident info, (b) DDoS to prevent customers seeing real status, (c) Status API scraping for uptime SLA evidence
S8 Customer docs docs.raxx.app Public (Class 1) anon None Low (a) SEO manipulation / content injection via CF Pages deploy, (b) Phishing via lookalike subdomain, (c) Link injection if docs accept user-submitted content
S9 Internal docs internal-docs.raxx.app CF-Access-gated (Class 4) operator (CF Access, sole auth) Internal architecture, runbooks, credentials references in docs Low (a) CF Access policy misconfiguration exposes all docs publicly, (b) Static content exfiltration if CF Access gate dropped, (c) Credential or key references in doc content
S10 API Gateway (email inbound) tzkznyft9c.execute-api.us-east-1.amazonaws.com/v1/email/inbound Public (AWS-hosted, no CF proxy) service-to-service (Postmark HMAC-SHA256 webhook signature) Inbound email content (PII: sender address, subject, body) Low (a) Webhook spoofing (inject arbitrary email events into SNS topic), (b) Replay attacks on valid webhook payloads, (c) DDoS-via-flood to exhaust Lambda concurrency / SQS capacity
S11 Queue service raxx-queue-{prod,staging}.herokuapp.com Heroku origin URL, no CF proxy customer (JWT) + operator (Console JWT) + service-to-service Customer identity, sessions, RBAC grants, audit chain Medium (a) Direct origin access bypasses all CF controls, (b) JWT forgery / weak signing key, (c) RBAC privilege escalation via grant endpoint without step-up

Critical gap flags:


Section 2 — WAF Rule Recommendations Per Surface

General notes on CF WAF tiers

Cloudflare WAF operates in three deployment modes: - Block (HTTP 403): High-confidence rules; attacker gets no useful signal. - Challenge (Managed Challenge / JS Challenge / CAPTCHA): Medium-confidence; legitimate users pass, bots fail. Use for ambiguous traffic. - Log: Observation mode. Run for 7 days before promoting to Challenge or Block on any rule set with broad scope.

All rule references below use Cloudflare's published ruleset nomenclature (https://developers.cloudflare.com/waf/managed-rules/reference/).


S1 — getraxx.com (marketing, CF Pages)

Current state: CF-Access-gated until 2026-05-23 UTC. CF Pages with full Cloudflare proxy.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | Broad OWASP coverage; low false-positive rate on static/marketing content | | OWASP Core Ruleset | Log (7 days pre-launch), then Challenge | CRS can be overly broad; observe first for static marketing site | | Exposed Credentials Check | OFF | No login form on getraxx.com |

Custom Rules (priority order):

  1. Rate limit — page scraping: 100 req/10 min per IP → Managed Challenge. Rationale: competitive intel scraping (pre-launch waitlist content, pricing) is the primary risk.
  2. Bot Fight Mode: ON. Allow known bots (Googlebot, Bingbot — already in CF known bot list). Block headless browser fingerprints via Cloudflare Bot Score < 30 → Challenge.
  3. Geo restriction: No blanket block needed for marketing. Exception: if operator decides to block Quebec signup at this layer post-launch, add CA-QC → Block on /signup path specifically (signup geo-block is already in Queue's registration endpoint; WAF is a coarser second layer).
  4. Header validation: Reject User-Agent: * missing header → Block. Reject UAs on CF's known bad-bot list.

Block thresholds: Conservative — marketing site losing legitimate users to WAF false-positives is costly. Start all custom rate-limit rules in Challenge mode, not Block, for the first 30 days post-launch.

Caveats: CF Pages handles static content; no origin server to protect. WAF layer is about bot management and rate limiting, not injection protection.


S2 — app.raxx.app (Antlers customer SPA, CF Pages)

Current state: CF Pages project raxx-app. CF Access gated pre-launch (remove at launch per B5). Post-launch: Class 1 public.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | Standard protection | | OWASP Core Ruleset | Log initially, then Block after 7-day observation | Antlers is a React SPA — most content is static JS. CRS can flag false-positives on WebAuthn attestation payloads (base64url-encoded binary blobs) — must verify before enabling Block mode. | | Exposed Credentials Check | OFF | Antlers does not handle passwords; credentials are WebAuthn public keys |

Custom Rules: 1. Rate limit — global per IP: 200 req/min per IP → Managed Challenge. React SPA makes many small asset requests; 200/min is generous but deters floods. 2. Bot Fight Mode: ON. Allow: no server-side bots need to access Antlers. CF Bot Score < 20 → Block. 3. Static asset caching: CF already caches *.js, *.css, *.png etc. via Cache Rules — WAF should pass-through on cache hits (CF handles this automatically for Pages). 4. Security headers via Transform Rules: Add Content-Security-Policy, X-Frame-Options: DENY, X-Content-Type-Options: nosniff, Strict-Transport-Security: max-age=31536000; includeSubDomains via CF Transform Rules (Header Modification). This supplements the app-layer security headers already in security_headers.py.

Caveats: WebAuthn attestation objects in registration/login requests are large binary-encoded payloads. CRS rules for large body sizes (e.g., CRS 920170 — GET/HEAD with body, CRS 920420 — content type not allowed) may false-positive. Test with a live WebAuthn flow before enabling CRS Block mode. Monitor CRS rule 920200 (path traversal in URL) — should not apply to SPA but test first.


S3 — api.raxx.app (Raptor backend API, Heroku)

This is the highest-risk surface. Three distinct rule groups required.

Current state: CF proxied. FLAG_ENFORCE_CF_ORIGIN currently OFF on raxx-api-prod — Heroku origin URL raxx-api-prod-*.herokuapp.com is a live bypass. Auth layer: session JWT (post-launch).

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | Full OWASP injection coverage | | OWASP Core Ruleset | Log (7 days), then Block | API surface; test all endpoint payloads before Block mode. Key CRS rules: SQLi (942xxx), XSS (941xxx), LFI (930xxx), Remote File Inclusion (931xxx). Exclusions needed: WebAuthn attestation payloads (binary-encoded, large) — create CRS exclusion for /api/v1/auth/webauthn/* paths to avoid false-positives on attestation blobs. | | Exposed Credentials Check | ON (Log mode initially) | Raptor has login/register endpoints; CF's Exposed Credentials Check scans for known-breached credentials appearing in request bodies. Log mode only — do not auto-block on this surface as it could block legitimate registrations. |

Custom Rules (priority order):

  1. Auth endpoint rate limits (tightest): - /api/v1/auth/webauthn/login/begin and /login/complete: 10 req/min per IP → Block (not Challenge — bot traffic hitting login at >10/min has no legitimate use) - /api/v1/auth/webauthn/register/begin: 5 req/min per IP → Block - /api/v1/auth/backup-codes/redeem: 5 req/min per IP → Block (mirrors Queue app-layer rate limit; WAF is a second line of defense) - /api/v1/auth/email/send-verification: 3 req/5 min per IP → Managed Challenge (email send rate — matches Queue contract) - /api/auth/login/* (legacy Raptor routes): 15 req/min per IP → Block

  2. Trading endpoint rate limits: - POST /api/trading/orders: 30 req/min per IP → Block (order submission — DoS and Alpaca quota exhaustion prevention) - GET /api/trading/*, GET /api/market-data/*, GET /api/historical-data/*: 120 req/min per IP → Managed Challenge (data endpoints — scraping mitigation) - POST /api/backtest/*: 10 req/min per IP → Block (compute-intensive; Alpaca quota exhaustion vector per 2026-04-24 review finding C2)

  3. Global API rate limit: 300 req/min per IP across all /api/* paths → Managed Challenge. (App-layer global rate limit is 100 req/min per GLOBAL_RATE_LIMIT_REQUESTS default; WAF provides a coarser first gate.)

  4. Heroku origin bypass block: Until FLAG_ENFORCE_CF_ORIGIN=true is set on raxx-api-prod, this WAF rule is the only layer blocking direct origin access. Custom Firewall Rule: (http.host contains "herokuapp.com") → Block. Note: this requires that CF is proxying requests to the custom domain — requests to the Heroku URL directly never transit CF. This WAF rule is NOT a substitute for enabling FLAG_ENFORCE_CF_ORIGIN; it is belt-and-suspenders.

  5. CORS pre-flight validation: Only allow Origin headers matching https://raxx.app, https://app.raxx.app, or https://raxx-app.pages.dev (staging). Block requests with other Origin headers on non-OPTIONS methods. (App-layer CORS is already enforced via FRONTEND_ORIGIN env var; WAF is coarser upstream filter.)

  6. Bot management: Bot Fight Mode OFF on api.raxx.app (would break legitimate API clients including mobile apps and automated trading workflows). Instead: CF Bot Score < 10 → Block (these are definitively-automated with no WebAuthn capability). CF Bot Score 10–30 → Log (investigate before blocking — could be legitimate API clients).

  7. Geographic restrictions: Post-launch, match Quebec geo-block at WAF layer for signup paths: - (ip.geoip.subdivision_1_iso_code == "QC" and http.request.uri.path contains "/register") → Block - Rationale: Queue's registration endpoint already returns 422 geo_block_qc — WAF block at the edge is a second layer that prevents the request from reaching the origin at all.

Block thresholds for auth endpoints: These are deliberately tight. A legitimate user attempting login 10 times in 60 seconds indicates either a script or a UI bug. Block (not challenge) because a CAPTCHA on a WebAuthn-only flow is a poor UX and an attacker with a bot can solve CAPTCHAs anyway. The correct response to an auth flood is a hard block with a short cooldown period.

Caveats: WebAuthn attestation payloads are binary-encoded JSON blobs of variable size. CRS rule 920160 (Content-Type header not defined) and 920420 (Content-Type not in allowed list) must be tuned to allow application/json explicitly. Additionally, CRS 949110 (anomaly score threshold) can trip on multi-value JSON bodies — tune anomaly threshold to 15+ before enabling Block mode.


S4 — console.raxx.app (Operator Console, Heroku + CF Access)

Current state: CF Access gated (Class 2 transitional). Heroku origin URL exposed (FLAG_ENFORCE_CF_ORIGIN off per docs). CF proxied.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | Operator surface; aggressive blocking acceptable | | OWASP Core Ruleset | Block (after 7-day Log) | Operator-only; low traffic; lower false-positive risk than customer surfaces | | Exposed Credentials Check | OFF | Console uses passkey + Google OAuth; no credential bodies |

Custom Rules: 1. CF Access bypass — strict mode: (not cf.access.jwt.verified) → Block. This rule fires before any other — any request not carrying a valid CF Access JWT is blocked at the WAF layer before reaching Heroku. This provides defense-in-depth independent of FLAG_ENFORCE_CF_ORIGIN. 2. IP allowlist (operator IPs): If operator uses a known egress IP (office, home — noting Oracle Dyn/home network context), add to IP allowlist. Not required, but reduces CF challenge friction for operator sessions. 3. Rate limit — privileged mutations: Paths like /api/rbac/grants, /api/sessions/revoke-all, /api/customers/*/erase: 10 req/min per IP → Block. These are step-up-gated at the app layer but rate-limit at WAF prevents enumeration abuse. 4. Bot Fight Mode: ON. No non-human clients should access the Console. 5. Geographic restriction: Operator-only surfaces may not need geo restrictions, but if the operator is US-based, adding a country allowlist (US, plus any other countries where the operator travels) reduces attack surface from opportunistic global probing.

Caveats: CF Access JWT validation (rule 1) depends on CF Access being correctly configured and the JWT being properly issued. This rule should be tested before deployment — a misconfiguration would lock out all operators. Test path: verify that a valid CF Access session passes the check, and that a request without CF Access cookie/header is blocked.


S5 — tickets.raxx.app (FreeScout, Lightsail + CF Access)

Current state: CF-Access-gated (Class 3, strong MFA). FreeScout on Lightsail. CF proxied.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | | | OWASP Core Ruleset | Log → Block | FreeScout is PHP; PHP surfaces have higher OWASP rule hit rates — observe for 14 days before Block | | Exposed Credentials Check | OFF | FreeScout auth is behind CF Access |

Custom Rules: 1. CF Access enforcement: Same as S4 — (not cf.access.jwt.verified) → Block. 2. Rate limit: 60 req/min per IP → Challenge. FreeScout is admin-access only; even operators don't hammer it. 3. Bot Fight Mode: ON. 4. PHP-specific rules: If CF WAF includes PHP-specific rule sets in the managed ruleset, ensure they are enabled. PHP object injection (POP chains) and deserialization vulnerabilities are common FreeScout CVE patterns.

FreeScout-specific caveat: The Postmark inbound email webhook (POST /api/webhooks/postmark/inbound in Raptor) previously went directly to FreeScout. After SC-E10 ships, inbound email flows through API Gateway → SNS → Lambda → FreeScout API. The WAF rules above cover the FreeScout admin surface; the email inbound path is covered under S10.


S6 — vault.raxx.app (Infisical, Heroku + CF Access)

Current state: CF-Access-gated (Class 3, strong MFA). Infisical self-hosted. CF proxied.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | | | OWASP Core Ruleset | Block (immediately, post-initial log period) | Holds all platform secrets; most critical surface | | Exposed Credentials Check | OFF | Infisical uses its own token-based auth behind CF Access |

Custom Rules: 1. CF Access enforcement: (not cf.access.jwt.verified) → Block. 2. Rate limit: 30 req/min per IP → Block (vault access should be near-zero at runtime; spikes indicate enumeration). 3. Machine identity allowlist: Infisical service tokens are used by machine identities (CI, Velvet, agents). These are service-to-service calls from known Heroku dyno IPs. CF Access service tokens handle this at the identity layer; WAF should pass-through requests carrying valid CF Access service-auth headers. 4. Bot Fight Mode: ON (human-facing); OFF for paths used by service tokens (e.g., /api/v3/secrets/raw). Distinguish with path-based rule.


S7 — status.raxx.app (Status page, CF Pages)

Current state: Public (Class 1). CF Pages.

Managed Rule Sets: | Rule Set | Mode | Rationale | |----------|------|-----------| | Cloudflare Free Managed Ruleset | Block | | | OWASP Core Ruleset | Log | Static CF Pages; low complexity | | Exposed Credentials Check | OFF | |

Custom Rules: 1. Rate limit: 300 req/min per IP → Challenge. Status page is public and should survive any surge of users checking status during an incident. 2. DDoS: Rely on CF's automatic DDoS protection (L3/L4 volumetric). Status page must remain reachable even under DDoS — CF Pages is inherently resilient; no additional rule needed. 3. Bot management: Allow all bots (status page content is intended to be machine-readable — uptime monitors, RSS aggregators).

Special requirement: Status page MUST remain reachable even when all other CF WAF rules are engaged. This is structurally guaranteed by CF Pages hosting — WAF rules on Pages are per-site and cannot be accidentally coupled to other surfaces.


S8, S9 — docs.raxx.app + internal-docs.raxx.app

S8 (docs.raxx.app, public): Standard managed rulesets (Block). Rate limit 200 req/min per IP. Bot Fight Mode ON (allow Googlebot/Bingbot).

S9 (internal-docs.raxx.app, CF-Access-gated Class 4): CF Access enforcement rule (Block non-JWT). Rate limit 60 req/min per IP. Bot Fight Mode ON (no bots should access internal docs).


S10 — API Gateway email inbound (tzkznyft9c.execute-api.us-east-1.amazonaws.com)

This surface has no Cloudflare WAF layer. WAF protections at the edge are not applicable — the URL is an AWS API Gateway public HTTPS endpoint that Postmark calls directly. Protection is at the application layer only.

Required controls (not WAF — app-layer and AWS-layer):

  1. Webhook signature validation (SC-E10 — not yet shipped as of 2026-05-11): API Gateway → Lambda or integration must validate the X-Postmark-Signature header (HMAC-SHA256 of request body, keyed with /raxx/email/postmark_inbound_webhook_token). Until this ships, any party with the URL can inject arbitrary payloads into the email pipeline. This is the most critical gap for this surface.

  2. AWS API Gateway resource policy: Restrict source IPs to Postmark's documented webhook IP ranges. Postmark publishes their sending IPs; a resource policy allowlisting those CIDRs is a defense-in-depth layer independent of signature validation.

  3. AWS API Gateway rate throttling: Default API Gateway throttling (10,000 req/s burst, 5,000 req/s steady state) is far more than needed. Set per-method throttling on POST /webhooks/postmark/inbound to 10 req/s burst, 1 req/s steady. Rationale: legitimate inbound email volume at v1 is ~100/month ≈ 0.00004 req/s. Even a spike of 100 concurrent inbound emails is well within 10 req/s.

  4. Replay protection: Postmark includes a MessageID per webhook. Lambda's idempotency check (DynamoDB dedup table per docs/architecture/durable-email-delivery.md Section 5) handles replay at the consumer layer. API Gateway should add a Idempotency-Key header check or rely on SQS FIFO's 5-minute dedup window.

  5. Route this URL through Cloudflare (post-v1 improvement): API Gateway custom domain + CF proxy would add a WAF layer. Out of scope for v1 but recommended as a post-launch hardening card.


S11 — raxx-queue-{prod,staging}.herokuapp.com (Queue C++ service)

Current state: Heroku origin URL. No CF proxy documented. No CF Access documented.

This is a critical architecture gap. Queue owns customer identity, sessions, RBAC, and audit. If Queue's Heroku URL is accessible directly, the following unauthenticated endpoints are reachable without transiting any Cloudflare control: - POST /api/v1/auth/webauthn/register/begin — account registration - POST /api/v1/auth/webauthn/login/begin — login challenge - POST /api/v1/auth/backup-codes/redeem — account recovery

Recommended controls: 1. Immediate (operator action): Confirm whether raxx-queue-prod.herokuapp.com is directly routable. If so, FLAG_ENFORCE_CF_ORIGIN equivalent must be implemented in Queue (analogous to cloudflare_origin_guard.py in Raptor). Routing: sre-agent. 2. CF proxy for Queue's custom domain: Add Queue to Cloudflare DNS as a proxied CNAME. Add CF WAF rules matching S3 (API surface rules) once proxied. 3. Until CF proxy is in place: Queue's app-layer rate limits (per api-contract.md) are the only rate-limiting layer. Confirm 429 rate_limited is operational for /auth/webauthn/login/begin, /register/begin, and /backup-codes/redeem.


Section 3 — Threat-Specific Mitigations

T1 — Account Enumeration via login/signup error timing

Threat: Attacker probes POST /api/v1/auth/webauthn/register/begin with sequential email addresses. If the API returns a distinct error for registered vs. unregistered emails (e.g., 409 email_already_registered vs. 200) at different latencies, the attacker can enumerate which emails exist.

Current posture (Queue contract): POST /api/v1/auth/email/send-verification returns 202 Always — no account enumeration per api-contract.md. This is correct. The registration begin endpoint returns 409 email_already_registered by design — this is an explicit enumeration vector.

WAF mitigation: - Rate limit POST /api/v1/auth/webauthn/register/begin to 5 req/min per IP at CF WAF (S3 Custom Rule 1). - Bot Fight Mode blocks headless enumeration scripts.

App-layer fix (out of scope for this memo, route to feature-developer): The registration begin endpoint should either (a) return a consistent 202 with side-effect (send a "you already have an account" email), or (b) document the 409 as an accepted enumeration risk with WAF rate-limit as the compensating control. The current Queue API contract documents 409 — this is an open design decision.

References: OWASP Testing Guide: OTG-IDENT-004 (Testing for Account Enumeration and Guessable User Account). CWE-204: Observable Response Discrepancy.


T2 — Brute-Force / Credential Stuffing

Threat: Automated submission of email/passkey-credential combinations from breach databases against Raxx auth endpoints. Less effective against WebAuthn (credentials are device-bound, not reusable) but the login flow's login/begin endpoint can be probed to confirm account existence, and backup-codes/redeem is a password-equivalent path.

WAF mitigation: - Auth endpoint rate limits (S3 Custom Rule 1): 10 req/min per IP → Block on /login/begin, /login/complete. - /backup-codes/redeem: 5 req/min per IP → Block. This is the highest-risk path because backup codes are pre-shared secrets (unlike WebAuthn credentials). - CF Exposed Credentials Check: ON in Log mode initially on api.raxx.app. This checks POST body fields against known credential-breach databases — won't directly catch WebAuthn (no passwords), but will flag if backup codes or other secrets appear in payloads.

App-layer existing control: Queue API contract specifies 429 rate_limited on backup-codes/redeem at 5 attempts/60s per IP. WAF is a second line of defense.

References: OWASP ASVS V2.1.1 (authentication brute force mitigation). CISA Advisory AA22-137A (credential stuffing).


T3 — API Scraping (trading data, market data, portfolio)

Threat: Attacker (competitor, data broker) scrapes Raptor's market data or trading endpoints at scale. At v1, endpoints like GET /api/market-data/*, GET /api/historical-data/*, GET /api/trading/positions expose data that could be monetized or used for competitive analysis.

WAF mitigation: - S3 Custom Rule 2: GET /api/trading/*, GET /api/market-data/*, GET /api/historical-data/*: 120 req/min per IP → Managed Challenge. - Bot Score < 30 for API endpoints → Log (investigate); < 10 → Block. - Post-auth-launch: all data endpoints will require a valid session JWT — unauthenticated requests will be rejected at the app layer before WAF rate limits are needed. This threat is highest during the pre-auth pre-launch window.

Caveat: Once session auth is enforced on all data endpoints, scraping requires a valid account. Rate limits then need to be per-session-token, not just per-IP (since attackers distribute requests across many IPs). App-layer rate limiting keyed on session ID is more effective post-auth. Recommend adding by_session_token=True mode to rate_limiter.py as a follow-on card.


T4 — Webhook Spoofing (Postmark)

Threat: Attacker sends forged POST /webhooks/postmark/inbound requests to API Gateway, injecting arbitrary email events into the SNS pipeline. Could cause ticket spam in FreeScout, exhaust Lambda concurrency, or inject malicious content into email-triggered workflows.

Current state: Signature validation is designed (SSM token, X-Postmark-Signature check) but SC-E10 has not shipped. The API Gateway URL is currently unprotected except by obscurity of the URL.

WAF mitigation: No Cloudflare WAF applicable at API Gateway level. Controls are entirely app-layer and AWS-layer (see S10 recommendations above).

Detection: If SC-E10 signature validation logs postmark_signature_invalid count > 0 per durable-email-delivery.md F11 detection specification, that alarm fires. Once SC-E10 ships, monitor this alarm.

Urgency: HIGH. Until SC-E10 ships, any party that discovers the API Gateway URL (trivially enumeratable from Postmark's inbound server webhook config, which may be stored in config or referenced in code) can inject arbitrary data. File as a HIGH finding (see Section 6).

References: OWASP Top 10 A07:2021 — Identification and Authentication Failures. RFC 8235 — Schnorr NIZK Proof (HMAC webhook authentication pattern).


T5 — DDoS (Volumetric + Application-Layer)

Threat: - Volumetric (L3/L4): High packet rate targeting Raxx infrastructure. - Application-layer (L7): HTTP flood targeting compute-intensive endpoints (POST /api/backtest/*, WebAuthn registration, etc.).

WAF mitigation: - Volumetric: Cloudflare's automatic DDoS protection (all plans) handles L3/L4 volumetric. No custom rule needed. CF Pages (S1, S7, S8) are inherently resilient via CF's global CDN. - Application-layer: - POST /api/backtest/*: 10 req/min per IP → Block (compute-intensive endpoint — primary L7 DDoS vector). - Global API rate limit: 300 req/min per IP → Challenge (S3 Custom Rule 3). - Status page (S7): Must survive DDoS to maintain customer trust during an incident. CF Pages' CDN handles this structurally. - Heroku dyno protection: Heroku provides no DDoS protection at the origin layer. The FLAG_ENFORCE_CF_ORIGIN middleware (cloudflare_origin_guard.py) ensures requests reaching Heroku have already passed CF. Until this flag is ON, Heroku dynos are directly reachable and can be flooded. Enabling FLAG_ENFORCE_CF_ORIGIN=true on raxx-api-prod is the single highest-ROI infra change for DDoS resilience.

References: Cloudflare DDoS Protection: https://www.cloudflare.com/ddos/. NIST SP 800-189 — Resilience of the Internet of Things (DDoS taxonomy).


T6 — SQLi, XSS, LFI, RFI (OWASP Injection)

Threat: Classic injection attacks via request parameters, headers, or body content.

WAF mitigation: - CF Managed Ruleset: Covers common SQLi (SQLI group), XSS (XSS group), and path traversal. Enable Block mode on all surfaces. - OWASP CRS: Deep injection coverage. Run in Log mode first on API surfaces (S3) due to WebAuthn binary payload false-positive risk. - App-layer defense (existing): Raptor uses SQLite with parameterized queries (bandit FP-02, FP-03 confirmed false-positive in scan 2026-05-06). No raw string interpolation in user-controlled query parameters confirmed. - Specific high-risk paths: - /api/historical-data/?symbol=<input> — if symbol parameter is passed to SQL, parameterization is critical. Bandit's B608 findings were confirmed FP (hardcoded fragments) — but any new symbol query path must use bound parameters. - /api/market-data/* — same parameterization requirement.

XSS in status page: Status page content is operator-written; no user-submitted content. XSS risk is low. Standard CF managed ruleset is sufficient.

LFI via static serve route: Raptor's /<path:path> catch-all route (M3 from 2026-04-24 review) is a potential LFI path if file extension allowlisting is not implemented. send_from_directory prevents directory traversal but the route still serves any file that exists in static/. Ensure only known extensions are served (follow-up for feature-developer, tracked per M3).


T7 — Bot Scraping (Price Discovery, Signup Probing)

Threat: Automated bots probing signup availability (email enumeration via /register/begin), scraping market data pricing for arbitrage, or probing feature availability ahead of launch.

WAF mitigation: - Bot Fight Mode: ON for all customer-facing surfaces (getraxx.com, app.raxx.app, api.raxx.app). - CF Bot Score thresholds: Per-surface (see Section 2). API surface uses Log < 30, Block < 10 to avoid false-positives on legitimate automated API clients. - Pre-launch extra restriction: Until 2026-05-23 UTC, getraxx.com and app.raxx.app are CF-Access-gated — bots cannot reach them at all. Bot Fight Mode becomes relevant at launch. - Challenge on high-value pages: Registration flow (/api/v1/auth/webauthn/register/begin) → Managed Challenge for Bot Score 10–30, Block < 10.


T8 — Replay Attacks on Auth + Webhook Endpoints

Threat: - Auth replay: Capture of a valid WebAuthn assertion and replay to establish a fraudulent session. - Webhook replay: Capture of a valid Postmark webhook payload and replay to create duplicate FreeScout tickets or trigger duplicate email sends.

WAF mitigation (WAF cannot fully address these — app-layer controls are primary):

Auth replay: WebAuthn assertions include a challenge nonce that is single-use and server-validated. Replay is structurally prevented by the WebAuthn protocol. CF WAF has no specific contribution here beyond rate limiting (which prevents brute-force challenge generation).

Webhook replay: Postmark's HMAC signature covers the request body but does NOT include a timestamp or nonce by default. A captured valid webhook payload could be replayed. - App-layer control (existing in design): DynamoDB dedup table keyed on MessageID (per durable-email-delivery.md Section 5). Replays are caught by idempotency check. - Strengthen (recommendation to feature-developer/sre-agent): Add a timestamp validation requirement in the API Gateway → SNS integration (SC-E10): reject webhooks where the Postmark Date header is > 60 seconds old. This closes the window for valid-signature replays. - WAF contribution: Rate limit on API Gateway endpoint (S10, AWS-layer throttling) prevents flood-replay. FIFO SQS dedup window provides 5-minute replay protection.

References: W3C WebAuthn Level 2, §7.2 (authentication ceremony, challenge uniqueness). OWASP ASVS V3.5.3 (replay attack prevention for tokens).


Section 4 — Layered Defense Map

LAYER 0 — DNS / CDN (Cloudflare)
  ├── Anycast routing: L3/L4 DDoS absorbed by CF network
  ├── TLS termination at edge (TLS 1.2 minimum, TLS 1.3 preferred)
  └── Certificate management (CF-managed, auto-renew)

LAYER 1 — CF WAF (Cloudflare WAF rules)
  ├── Catches: SQLi, XSS, LFI, RFI (via CRS + CF Managed Ruleset)
  ├── Catches: Volumetric L7 floods (rate limiting rules)
  ├── Catches: Bot scraping (Bot Fight Mode, Bot Score thresholds)
  ├── Catches: Geographic violations (CA-QC geo-block on signup)
  └── MISSES: Requests to Heroku origin URLs (not CF-proxied)
             Requests to API Gateway (no CF proxy)

LAYER 2 — CF Access (Zero Trust)
  ├── Catches: Unauthenticated access to operator surfaces (console, vault, tickets, internal-docs)
  ├── Catches: CF Access bypass attempts (no valid JWT → 403)
  └── MISSES: Heroku origin URL bypass (raxx-*.herokuapp.com)
             Queue service (no CF Access documented)

LAYER 3 — Origin Guard (Raptor middleware — cloudflare_origin_guard.py)
  ├── Catches: Direct-to-Heroku requests (when FLAG_ENFORCE_CF_ORIGIN=true)
  ├── CURRENTLY DISABLED: FLAG_ENFORCE_CF_ORIGIN=false on raxx-api-prod
  └── MISSING: Queue has no equivalent origin guard documented

LAYER 4 — App-Layer Auth + Rate Limiting
  ├── Catches: Unauthorized API calls (session JWT validation)
  ├── Catches: Brute force on auth endpoints (Queue rate limits, Raptor rate_limiter.py)
  ├── Catches: Duplicate webhook processing (DynamoDB dedup table)
  ├── Catches: RBAC violations (permission checks on all privileged endpoints)
  └── LIMITATION: In-memory rate limiter (Raptor) is per-process — 2x effective limit
                  on multi-worker Heroku dynos until Redis is enabled (#1039)

LAYER 5 — Audit + Detection
  ├── Catches: Anomalous patterns post-compromise (audit log + Sentry)
  ├── Catches: Webhook signature failures (SC-E10 alarm — not yet shipped)
  └── MISSING: CF WAF logs → Sentry / S3 ingestion pipeline not configured

Threat class → catching layer

Threat Primary catch layer Fallback layer
Volumetric DDoS (L3/L4) Layer 0 (CF Anycast) N/A — structural
Application-layer DDoS (L7 flood) Layer 1 (WAF rate limits) Layer 4 (app rate limiter)
SQLi / XSS / LFI / RFI Layer 1 (CRS) Layer 4 (parameterized queries)
Bot scraping Layer 1 (Bot Fight Mode) Layer 4 (rate limiter)
Brute-force / credential stuffing Layer 1 (auth endpoint rate limits) Layer 4 (Queue 429)
Account enumeration Layer 1 (rate limits) Layer 4 (app-layer anti-enum)
Heroku origin bypass LAYER 3 (DISABLED) Layer 4 (session auth)
CF Access bypass (operator surfaces) Layer 2 (CF Access JWT) Layer 3 (origin guard)
Webhook spoofing (Postmark) NO CF LAYER Layer 4 (SC-E10 signature validation — NOT YET SHIPPED)
JWT/session replay N/A (WebAuthn prevents) Layer 4 (session revocation table)
CSRF Layer 1 (SameSite cookie) Layer 4 (CORS allowlist)
API replay (webhook) Layer 4 (DynamoDB dedup) Layer 4 (SQS FIFO dedup)

Bypass-and-recover scenarios

Scenario A: CF WAF rule is bypassed (attacker uses Heroku origin URL) - Layer 1 (WAF) is bypassed entirely. - Layer 2 (CF Access) is bypassed for public surfaces; operative for operator surfaces IF Heroku URL is not reachable directly (tickets/vault run on Lightsail/Heroku with CF Access, but origin URL reachability is not confirmed blocked). - Layer 3 (origin guard) is the recovery layer — but FLAG_ENFORCE_CF_ORIGIN=false means it is currently NOT catching this. - Layer 4 (app auth) is the only layer that remains operative. Session JWT validation will reject unauthenticated requests even at the Heroku URL. - Current gap: Auth endpoints (login/begin, register/begin) are partially public by design. A flood targeting these endpoints directly at the Heroku URL bypasses CF rate limits.

Scenario B: CF Access misconfigured (operator surface exposed) - Layer 2 fails. - Layer 3 (origin guard) catches direct origin access if FLAG_ENFORCE_CF_ORIGIN=true. - Layer 4 (console passkey/Google OAuth) is the recovery layer for console.raxx.app. - For vault/tickets (Class 3 — unaudited app auth), CF Access failing means only the tool's native auth remains. Per ADR-0031 D5, this is insufficient — CF Access failure on Class 3 surfaces is treated as a critical incident.

Scenario C: SC-E10 not shipped — webhook spoof - No CF layer protects API Gateway. - No signature validation at API Gateway layer. - SNS topic receives arbitrary payload, DynamoDB dedup may or may not catch it (dedup key is MessageID from payload — attacker can supply any MessageID). - Lambda processes the payload and calls FreeScout API. - No recovery mechanism currently in place.


Section 5 — Operator Decisions Required

The following decisions must be made before the architect can finalize WAF rule configuration. These are policy questions, not technical questions.

D1 — Block vs. Challenge for customer-facing auth endpoints

Question: Should POST /api/v1/auth/webauthn/login/begin and /register/begin rate-limit violations result in a hard Block (HTTP 403) or a Managed Challenge (CAPTCHA)?

Security case for Block: A WebAuthn-only auth flow cannot be completed by a bot regardless. Rate-limit violations on these endpoints indicate automation, not a mistaken human user. Block is appropriate.

User experience case for Challenge: A customer on a shared IP (corporate NAT, VPN exit node) might share an IP with a bot. A Challenge allows the human to prove they're human without being blocked.

Recommendation: Block for violations > 2x the threshold (clearly malicious); Challenge for violations between 1x–2x threshold. Implement via two rules at different thresholds.

D2 — WAF log ingestion

Question: Should CF WAF logs be ingested into Sentry? Or into an S3 log archive?

Options: - CF Logpush → Sentry (via webhook): Near-real-time anomaly alerting. Requires Sentry DSN configuration in CF. - CF Logpush → S3: Archival; post-hoc analysis only. Lower cost, less operational overhead. - None: CF WAF provides basic event logging in the dashboard. Adequate for pre-launch.

Recommendation: Pre-launch: CF dashboard logging only (no Logpush). Post-launch: CF Logpush → S3 for 90-day retention + Logpush summary to Sentry for CRITICAL/HIGH WAF events. File a post-launch card for this.

D3 — Bot Fight Mode on api.raxx.app

Question: Bot Fight Mode on api.raxx.app will challenge requests with CF Bot Score < 30. Legitimate automated API clients (trading bots, iOS app background sessions, Velvet rotation service) may be scored as bots.

Decision needed: Should Bot Fight Mode be enabled on api.raxx.app with an explicit allowlist of known-automation sources, or disabled entirely for the API surface?

Recommendation: Bot Fight Mode OFF on api.raxx.app. Use Bot Score < 10 → Block for definitively-malicious automation. Allow all legitimate clients to pass without challenge.

D4 — Operator IP allowlist for console/vault/tickets

Question: Should WAF rules for console, vault, and tickets include an allowlist of the operator's known egress IPs (home network via Oracle Dyn, any VPN egress)?

Benefit: Reduces false-positive CF challenges for operator sessions. Risk: IP allowlists require maintenance; if operator IP changes (travel, VPN change), they may be challenged or blocked.

Recommendation: Implement a loose allowlist (operator home CIDR) as a low-priority rule to skip Challenge for operator IPs, but do not skip Block rules. The CF Access layer already provides identity confirmation; IP allowlisting is convenience, not security.

D5 — WAF strict mode pre-launch gate

Question: Should WAF rules be in Block mode at launch day (2026-05-23 UTC) or in Log/Challenge mode with a post-launch observation period?

Recommendation: Mixed: - CF Managed Ruleset: Block at launch (low false-positive rate, well-tested). - OWASP CRS on api.raxx.app: Log for 7 days post-launch, then Block after confirming no false-positives on WebAuthn payloads. - Auth endpoint rate limits: Block at launch (no legitimate case for >10 login attempts/min per IP). - Data endpoint rate limits: Challenge at launch, promote to Block after 7 days observation.


Section 6 — Findings Filed

The following security findings are filed as GitHub issues per the issue-filing convention. Issues are filed via the raxx-ops-bot identity.

CRITICAL findings

CRIT-WAF-1: Postmark webhook SC-E10 not shipped — API Gateway unprotected - S10 has no signature validation. Any party can inject into the email SNS pipeline. - Routes to: sre-agent (SC-E10 implementation) + feature-developer (Lambda signature validation) - See issue filing below.

HIGH findings

HIGH-WAF-1: FLAG_ENFORCE_CF_ORIGIN=false on raxx-api-prod - Heroku origin URL for api.raxx.app bypasses all CF WAF rules, CF Access, and rate limits. - Auth and trading endpoints reachable directly. - Routes to: sre-agent (enable flag after staging soak — per existing runbook in web-surface-posture.md)

HIGH-WAF-2: Queue service has no CF proxy or CF Access gate documented - raxx-queue-prod.herokuapp.com auth endpoints (register/begin, login/begin, backup-codes/redeem) reachable without transiting Cloudflare. - No origin guard equivalent in Queue. - Routes to: sre-agent (CF proxy) + software-architect (Queue origin guard design)

HIGH-WAF-3: CF WAF not configured on any Raxx surface - No Cloudflare Managed Ruleset, no OWASP CRS, no custom rate-limiting rules are in place on any surface as of 2026-05-11. - This memo establishes the per-surface recommendations; architect designs the implementation; sre-agent deploys. - Routes to: software-architect (design) → sre-agent (deploy)

MEDIUM findings

MED-WAF-1: App-layer rate limiter is per-process in-memory (not Redis-backed) - On multi-worker Heroku dynos, effective per-IP rate limit is 2x the configured value. - Issue #1039 is already filed for Redis enablement. - Confirm #1039 is on the pre-launch critical path.

MED-WAF-2: Auth endpoint registration returns 409 for existing emails — enumeration vector - POST /api/v1/auth/webauthn/register/begin returns 409 email_already_registered. - WAF rate limits compensate, but app-layer fix (always-202 with side-effect email) would be stronger. - Routes to: feature-developer (Queue auth endpoint behavior).

MED-WAF-3: Bot Fight Mode not configured - No Bot Fight Mode or Bot Score rules are currently set on any CF surface. - Routes to: sre-agent (configuration post-architect design).

MED-WAF-4: No timestamp validation on Postmark webhook payloads - Captured valid webhook payloads can be replayed (within DynamoDB dedup window). - Recommend adding Date header age check (reject > 60 seconds old) in SC-E10 implementation. - Routes to: feature-developer (SC-E10 / Lambda implementation).


Section 7 — Cross-Reference for Architect Agent

The software-architect agent is producing the integration design concurrently. The following table maps this threat model's recommendations to the architect's design scope:

This memo's recommendation Architect designs sre-agent deploys
S3 Custom Rule 1: auth endpoint rate limits CF WAF custom rules Terraform CF WAF Terraform apply
S3 Custom Rule 2: trading endpoint rate limits CF WAF custom rules Terraform CF WAF Terraform apply
S3 CF Access enforcement on console/vault/tickets CF Access policy update CF Access Terraform apply
S10 AWS API Gateway throttling + IP allowlist API Gateway Terraform (or console config) sre-agent
S11 CF proxy for Queue + origin guard CF DNS record + Queue origin guard middleware sre-agent
FLAG_ENFORCE_CF_ORIGIN on raxx-api-prod Existing design in web-surface-posture.md sre-agent (enable flag)
OWASP CRS in Log mode → Block after 7-day soak CF WAF managed ruleset Terraform sre-agent (toggle mode)
Bot Fight Mode per surface CF WAF zone settings Terraform sre-agent
Security headers via CF Transform Rules CF Transform Rule Terraform sre-agent
Redis-backed rate limiter (#1039) Already designed feature-developer (Raptor)

Coordination note: The architect agent should reference this memo's Section 2 WAF rule recommendations directly when designing the CF WAF Terraform module. Per-surface rule sets, rate-limit thresholds, and mode recommendations in Section 2 are the input. The architect's output (CF WAF config) will require operator decision on D1–D5 (Section 5) before finalization.