Skip to main content

SECURITY_MODEL — bff-consumer-service

Sibling: API_CONTRACTS · DATA_MODEL · APPLICATION_LOGIC · FAILURE_MODES

Cross-cutting: 07 Security/Compliance/Tenancy · Standards · ERROR_CODES

1. Threat model summary

ThreatSurfacePrimary controlSecondary control
Cross-tenant data leakComposition logic across tenantsWe read only from search-aggregation-service (already projected) and the read-only pricing-service /quotes/preview; never from authoritative per-tenant write modelsCode-review checklist + ESLint import boundary blocking direct reservation/inventory client imports
Handoff token forgeryBookingHandoff issued by us, consumed by bff-tenant-booking-serviceHMAC-SHA256 with hmacKeyId rotation; canonical signing string (no JSON ordering ambiguity)handoff_replay_log durable record; consumed flag; cross-BFF POST /internal/handoff/{id}/consume
Handoff token replayHandoff verificationSingle-use consumed flag with optimistic concurrency; receiving BFF marks consumed before user-visible actionReplay attempt emits bot_suspected.v1 and increments fingerprint penalty
Session hijackinggms_ cookie in transitHttpOnly, Secure, SameSite=Lax, 30-day TTL; cookie value is high-entropy ULIDCookie binding to cookieFingerprintHash (UA + Accept-Language + screen + tz) — mismatched fingerprint triggers re-bootstrap and emits bot_suspected.v1
Bot abuse / scrapingAll endpointsEdge: Cloud Armor + reCAPTCHA Enterprise; app-tier: rules-based BotDetector + token-bucket rate limitPer-fingerprint cadence list; 7-day bot_score_log for forensic
Cache poisoningMemorystore via single-flightSingle-flight key includes locale + currency + queryHash; no user-controlled data goes into cache key construction unsanitisedPeriodic cache audit (sample compare to live upstream)
PII leak via telemetryPub/Sub eventsWe hash IP, UA, fingerprint with peppered SHA-256; we never emit raw IP/UA/email/namePer-event schema validation in CI; sampled DLP scan in BigQuery sink
DDoS via campaign spikeAll anonymous endpointsCloud Armor adaptive rules; Cloud CDN absorption; per-instance rate-limit; campaign_mode raises cache TTLAuto-scale to 30 instances; circuit-break upstream calls if budget exceeded
Tenant suspension bypassHandoff to suspended tenantSuspended-tenant set in Memorystore + Postgres mirror; checked at mint timePeriodic re-fetch from tenant-service; pessimistic deny on cache miss
HMAC key compromiseSecret Manager rotationActive + grace key set; new tokens always sign with active; verifying BFF accepts both90-day rotation; emergency rotation runbook ≤ 30 min
Open redirect via handoff URLredirectUrl host constructionHost is computed server-side from tenant.tenantSlug, never accepted from clientAllow-list of host suffix .melmastoon.ghasi.io; CI test asserts this
Cookie injection / CSRFMutating routesSameSite=Lax + double-submit X-Idempotency-Key (header) requirement on all mutations; Origin header check on POSTOrigin allow-list of *.melmastoon.ghasi.io and known partner-marketing domains
Currency confusionX-Currency overrideStrict allow-list {AFN, USD, EUR, IRR, PKR, AED, GBP}; rejected with 422 otherwisePer-currency rounding policy enforced upstream

2. Authentication

This BFF is anonymous-first. There are three trust modes:

ModePhaseIdentity carrierWhere validated
Anonymous1 (current)gms_ cookieSessionBootstrapInterceptor
Anonymous + reCAPTCHA challenge1gms_ cookie + X-Recaptcha-TokenBotDetectionInterceptor
Authenticated consumer2Authorization: Bearer <jwt> from iam-service (consumer realm)JwtConsumerGuard (gated by feature flag, off in Phase 1)

A tenant-staff JWT is never accepted on this BFF. If one is presented, the request is rejected with MELMASTOON.BFF.SURFACE_MISMATCH (403).

Set-Cookie: gms_id=gms_01H...; HttpOnly; Secure; SameSite=Lax; Max-Age=2592000; Path=/
  • High-entropy ULID (128 bits).
  • HttpOnly blocks JavaScript access (XSS-resistant).
  • Secure requires HTTPS.
  • SameSite=Lax protects against most CSRF; mutating routes additionally require X-Idempotency-Key and an Origin header check.
  • Max-Age=2592000 (30 days). Inactive sessions are TTL-evicted from Memorystore on the same schedule.

cookieFingerprintHash = sha256(pepper + userAgent + acceptLanguage + screenSize + timezoneOffset). On every request we compare; mismatch triggers re-bootstrap and emits bot_suspected.v1 with kind='fingerprint-collision'.

4. Authorization

Only two authorization decisions exist on this BFF:

  1. Wishlist + session ownership — every read/write under /wishlist/* and /session/* requires the request to carry the gms_ cookie that owns the row. Cookie absent → bootstrap; cookie mismatch on identifier → 403 MELMASTOON.BFF.CONSUMER.SESSION_OWNERSHIP_VIOLATION.
  2. Handoff target tenant not suspended — checked at POST /handoff mint time and again at the receiving BFF.

There is no RBAC, no ABAC, and no RLS on this BFF (no tenant_id column on any owned row).

5. HMAC handoff signing

5.1 Algorithm

HMAC-SHA256 over the canonical signing string defined in DOMAIN_MODEL §2.4. Output base64url-encoded; no padding.

5.2 Key management

  • Keys live in Google Secret Manager at projects/<gcp-project>/secrets/bff-consumer-handoff-hmac/versions/<n>.
  • Service account bff-consumer-sa@<project>.iam.gserviceaccount.com has roles/secretmanager.secretAccessor on the secret.
  • Each key has a keyId like hmac-2026-04; the keyId is included in the token payload so the receiving BFF can disambiguate.
  • Rotation cadence: 90 days. The active key is used for signing; the previous key is kept in the grace set for verification only for 14 days after rotation.
  • Emergency rotation runbook: rotate, deploy both BFFs to pick up the new active version, re-publish a new active id within 30 minutes; old tokens minted before rotation continue to verify until they expire (max 30 min window).

5.3 Verification (performed by bff-tenant-booking-service)

Documented here for completeness so both BFFs share the same understanding:

1. Parse v1.<payload-b64>.<sig-b64>.
2. Decode payload JSON.
3. Reject if version != 'v1'.
4. Reject if expiresAt < now or mintedAt > now + 60 s skew tolerance.
5. Look up key by hmacKeyId; reject if not in {active, grace}.
6. Reconstruct canonical signing string per DOMAIN_MODEL §2.4.
7. HMAC-verify; reject on mismatch.
8. POST /internal/handoff/{id}/consume to bff-consumer; receive 'ok' | 'already-consumed'.
9. If 'already-consumed', reject with MELMASTOON.BFF.TENANT.HANDOFF_REPLAYED and emit bot_suspected.v1 on the receiving side.

6. Bot mitigation

6.1 Layers

LayerMechanismDecision
EdgeCloud Armor preconfigured rules + adaptive protectionHard-block clear-bot patterns at the LB
EdgereCAPTCHA Enterprise (invisible) on /search, /handoffScore threshold 0.3 → challenge
AppUA pattern matcher (curated list incl. headlessChrome, common scrapers)Weight 0.4 in BotScore.signals
AppCadence (sliding window, last 60 s requests per fingerprint)Weight 0.3
AppFingerprint collision (>50 sessions sharing fp in 24 h)Weight 0.2
AppHeader anomaly (missing Accept, malformed Accept-Language)Weight 0.1
App (Phase 2)LLM-augmented judge via ai-orchestrator-serviceAdvisory

6.2 Verdict thresholds

score < 0.6 → 'human' → no action
0.6 ≤ s < 0.85 → 'suspect' → challenge with reCAPTCHA, bff returns 429 + X-Recaptcha-SiteKey
score ≥ 0.85 → 'bot' → hard-block, returns 429, increments soft-deny counter on fingerprint

bot_suspected.v1 events are emitted at 100% sample for verdict ∈ {suspect, bot}.

7. Rate limiting

BucketCapacityRefillCost per request
consumer:search:<fingerprint>600600/min1
consumer:search:<ip>12001200/min1
consumer:handoff:<fingerprint>3030/min5 (handoff is heavy)
consumer:wishlist:<fingerprint>6060/min1
consumer:session:<fingerprint>3030/min1
consumer:telemetry:<fingerprint>600600/min1

Buckets are token-bucket implemented as Redis counters (rl:<bucket> key + rl:<bucket>:reset). Edge adds a coarse per-IP layer at Cloud Armor (per-IP-1000-rpm).

8. Tenancy isolation (the BFF's special posture)

This BFF is the only Melmastoon service whose owned data is not tenant-scoped. The tenancy isolation guarantee here is therefore inverted:

  • No row owned by this service references a tenant for tenancy enforcementtenantId appears only as a target reference on BookingHandoff and a cache-key dimension on BrandPeek.
  • All cross-tenant reads go through search-aggregation-service and never include PII fields per 02 §6.3. The CI test cross-tenant-read-allowlist.spec.ts enforces that the only HTTP clients we instantiate are for the four allow-listed upstream services.
  • Postgres has no RLS because there is no tenant_id column.

A separate CI test no-tenant-id-column.spec.ts parses the Drizzle schema and asserts no tenant_id-named column exists outside handoff_replay_log (where it is a target reference) and wishlist_anonymous (same) and tenant_suspended_cache (where it is the natural primary key for the cache).

9. Encryption + secrets

DataAt restIn transitNotes
Postgres rowsCloud SQL CMEK with per-environment KMS keyTLS 1.3 (Cloud SQL connector)KMS key rotation 365 d
Memorystore valuesAES-256 (Memorystore default)TLS 1.2+ (Memorystore in-transit encryption enabled)n/a
HMAC handoff secretSecret Manager + CMEKTLS to Secret Manager90-day rotation
reCAPTCHA site keySecret Manager (Cloud build inject)TLSRotated on partner request
Pepper for hashesSecret ManagerTLSAnnual rotation; old peppers retained for join continuity
HTTP cookiesn/aTLS onlyNo persistence outside browser

10. Audit logging

EventSinkRetention
Handoff mintmelmastoon.bff.consumer.handoff.initiated.v1audit-service1 year
Bot detected (suspect or bot)melmastoon.bff.consumer.bot_suspected.v1audit-service1 year
Handoff replay attemptmelmastoon.bff.consumer.bot_suspected.v1 (kind=handoff-replay)1 year
Tenant-suspension bypass attemptCloud Logging structured log + bot_suspected.v11 year
HMAC key rotationCloud Audit Logs (Secret Manager)7 years
Postgres CMEK key accessCloud Audit Logs7 years
Cookie ownership violationMELMASTOON.BFF.CONSUMER.SESSION_OWNERSHIP_VIOLATION → structured log90 days

All structured logs include requestId, traceId, guestSessionId (when available), ipHash, fingerprintHash. Raw IP/UA never appear in logs.

11. GDPR + DSAR

  • Right to access (Article 15): A guest can request their session data via the consent banner UI (Phase 1.5). The BFF returns the sanitised GuestSession blob + the wishlist_anonymous rows + a paginated export of MetaPageView and ConversionFunnelEvent rows joined on guestSessionId.
  • Right to erasure (Article 17): POST /session/clear purges Memorystore + Postgres mirrors + queues a 30-day BigQuery sweep job for telemetry events.
  • Right to portability (Article 20): JSON export from the access-request endpoint.
  • Lawful basis: Legitimate interest (telemetry sampling for product improvement); consent (marketing flag opt-in); contract (handoff necessary for booking initiation).
  • No PII is collected or stored Phase 1; the obligations above apply to derived identifiers (guestSessionId, hashed network identifiers).

12. Security headers (response)

Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
X-Content-Type-Options: nosniff
X-Frame-Options: DENY
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: geolocation=(self), camera=(), microphone=()
Content-Security-Policy: default-src 'none'; connect-src 'self' https://api.melmastoon.ghasi.io
Cross-Origin-Resource-Policy: same-site

CSP is enforced by the gateway; the BFF emits these headers as belt-and-braces.

13. Dependency + supply-chain security

  • All workspace dependencies pinned via pnpm-lock.yaml.
  • pnpm audit blocks PRs with high/critical vulnerabilities.
  • SBOM published on every release via cyclonedx-npm.
  • Container images built on Cloud Build, signed with Cosign, deployed to Cloud Run with binary authorization policy require-cosign-signature.

14. Pen-test scope (annual)

  • Anonymous handoff token forgery + replay.
  • Cookie hijacking.
  • CSRF on mutating routes.
  • Cache poisoning via crafted query parameters.
  • Cross-tenant data exposure via search params.
  • Bot bypass (curated tooling: ZAP, Burp, custom headless harness).

15. Incident playbook references

ScenarioRunbook
HMAC key suspected compromiserunbooks/bff-consumer/hmac-rotation-emergency.md
Bot wave detectedrunbooks/bff-consumer/bot-wave.md
Tenant-suspended cache desyncrunbooks/bff-consumer/tenant-suspended-cache-desync.md
Handoff replay alert spikerunbooks/bff-consumer/handoff-replay-spike.md
Memorystore failoverrunbooks/bff-consumer/memorystore-failover.md