VantageDash

Security Model

RLS, middleware, credential encryption, NIST 800-53 mapping

VantageDash implements defense-in-depth security aligned with NIST 800-53 Rev. 5 controls. The system has 39 mapped controls across 10 families.

Authentication & Authorization

Supabase Auth

  • Method: Email/password authentication via Supabase Auth
  • JWT flow: Supabase issues JWTs → frontend stores in cookies → backend validates on each request
  • Auto-provisioning: DB trigger handle_new_user() creates tenant + user_tenants row on signup

Backend Auth Chain

Every API request (except /api/health) goes through:

  1. get_current_user() — Validates Supabase JWT via client.auth.get_user(token)
  2. get_supabase_client() — Creates per-request Supabase client with user's JWT (RLS-scoped)
  3. get_tenant_id() — Reads user_tenants table (RLS ensures only current user's row)

Row Level Security (RLS)

All data tables have RLS policies enforced via get_user_tenant_id():

CREATE POLICY "tenant_isolation" ON competitors
FOR ALL USING (tenant_id = get_user_tenant_id());

This SQL function resolves auth.uid()user_tenants.tenant_id, ensuring queries only return data for the authenticated user's tenant.

Service Role Client

Background tasks (scraping, matching, syncing) use a service role client that bypasses RLS. These tasks always filter by tenant_id explicitly:

svc_client = create_client(url, service_role_key)
svc_client.table("product_tracking").select("*").eq("tenant_id", tenant_id).execute()

Middleware Stack

Three security middleware layers process every request:

1. Security Headers (NIST SC-8)

backend/app/middleware/security_headers.py

HeaderValuePurpose
X-Content-Type-OptionsnosniffPrevent MIME sniffing
X-Frame-OptionsDENYPrevent clickjacking
Cache-Controlno-storePrevent sensitive data caching
Referrer-Policystrict-origin-when-cross-originLimit referrer leakage
Permissions-PolicyRestrictedDisable unused browser APIs

2. Rate Limiting (NIST SC-5)

backend/app/middleware/rate_limit.py

Token bucket algorithm per client IP with tiered limits:

TierLimitPaths
auth10 req/min/api/tenant/credentials
mutation30 req/minAll POST/PUT/PATCH/DELETE
read120 req/minAll GET
deletion5 req/minDELETE /api/tenant/data
unlimited/api/health

Features:

  • X-Forwarded-For support for proxied deployments
  • 429 response with Retry-After + X-RateLimit-Limit/X-RateLimit-Remaining headers
  • Automatic stale bucket eviction (5min TTL, 10k max buckets)

3. Audit Trail (NIST AU-2, AU-3)

backend/app/middleware/audit.py

  • Injects X-Request-ID (UUID4) on every response
  • Logs all POST/PUT/PATCH/DELETE with structured JSON:
    • request_id, method, path, status, duration_ms
    • authenticated, body_hash (SHA-256), client_ip, user_agent
  • Sensitive fields auto-redacted via _REDACTED_FIELDS set

Frontend Middleware

frontend/src/middleware.ts adds headers on all Next.js responses:

  • Strict-Transport-Security with preload
  • Content Security Policy (CSP)
  • X-Frame-Options: DENY
  • Permissions-Policy (restrictive)
  • X-Request-ID for tracing

Credential Encryption (NIST SC-28)

Per-tenant Shopify credentials are encrypted at rest using Fernet (AES-128-CBC + HMAC-SHA256).

Key Derivation

backend/app/crypto.py:

  1. Explicit key: CREDENTIAL_ENCRYPTION_KEY env var (recommended for production)
  2. Automatic fallback: SHA-256 of SUPABASE_SERVICE_ROLE_KEY

Credential API

backend/app/routers/credentials.py:

EndpointMethodPurpose
/api/tenant/credentialsGETReturns boolean flags (never leaks secrets) + URL preview
/api/tenant/credentialsPUTEncrypts values before storing in tenants table
/api/tenant/credentialsDELETENulls all credential columns

Credential Resolution Priority

When running Shopify sync, credentials are resolved in order:

  1. Request body (explicit per-request)
  2. DB (encrypted per-tenant via Fernet)
  3. Environment variables (global fallback)

Data Lifecycle Management (NIST SI-12)

backend/app/routers/data_lifecycle.py:

EndpointMethodPurpose
/api/tenant/data/exportGETExports all 13 tenant-scoped tables as structured JSON
/api/tenant/dataDELETEDeletes all tenant data (FK-ordered), clears credentials, preserves tenant row
  • Handles session→log FK relationships (logs deleted via parent session IDs)
  • Resilient to partial failures
  • Export excludes encrypted credential columns
  • Designed for GDPR data portability and right-to-erasure

CI/CD Security

Pre-commit

  • Husky + lint-staged: Runs gitleaks secrets scan before every commit
  • .gitleaks.toml: Allowlists for CI placeholder keys + custom rules for Supabase/OpenAI/Shopify secrets

GitHub Actions

WorkflowChecks
CodeQL SASTStatic analysis for JS/TS + Python
DependabotAutomated dependency updates (pip + npm + GitHub Actions)
GitleaksSecrets scanning on push
TrivyContainer vulnerability scanning (SARIF → GitHub Security)
SBOMCycloneDX + Syft software bill of materials (90-day retention)
License Compliancepip-licenses + license-checker (fails on copyleft)
OWASP ZAP DASTDynamic application security testing (weekly + on push)

Container Hardening

backend/Dockerfile:

  • Python 3.12 slim base
  • gcc/g++ installed for native deps, then removed after pip install
  • Non-root user (appuser) for runtime
  • Minimal attack surface

Role-Based Access Control (RBAC)

Endpoint-level authorization via require_role() FastAPI dependency guard.

RoleCapabilities
ownerFull control: team management, settings, credentials, data lifecycle, all pipelines
adminInvite/remove members (not owner), configure settings, run all pipelines
memberView all data, run scrapes/matches, add competitors. Cannot manage team or settings
viewerRead-only: view dashboards, export data. Cannot trigger any mutations

Implementation: get_user_role() reads from user_tenants (RLS-scoped). require_role("owner", "admin") returns 403 if the user's role is not in the allowed set. Applied per-endpoint on team management, credential management, and data lifecycle routes.

Invite flow security: Invitations use unique UUID4 tokens, 7-day expiry, and partial unique index preventing duplicate pending invites. The handle_new_user() trigger validates invitations at signup time (database-level enforcement).

NIST 800-53 Control Mapping

39 controls mapped across 10 families. Full mapping in docs/nist-800-53-mapping.md.

Key families:

  • AC (Access Control): JWT auth, RLS, tenant isolation, RBAC (owner/admin/member/viewer)
  • AU (Audit): Structured logging, request IDs, body hashing
  • SC (System & Communications): TLS, rate limiting, encryption at rest, security headers
  • SI (System & Information Integrity): Input validation, data lifecycle, DAST
  • CM (Configuration Management): Dependabot, SBOM, license compliance
  • SA (System Acquisition): CodeQL SAST, mutation testing
  • RA (Risk Assessment): Trivy scanning, secrets detection

Test Coverage

Security-specific tests (~200+):

  • Tenant isolation (cross-tenant data leaks)
  • JWT validation edge cases
  • Input validation (XSS, SQLi attempts)
  • Rate limiting behavior
  • Credential encryption round-trips
  • Data export/deletion completeness
  • Security header presence
  • Brute-force protection
  • Timing attack resistance
  • Chaos/fault injection
  • Cryptographic validation
  • API protocol fuzzing