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:
get_current_user()— Validates Supabase JWT viaclient.auth.get_user(token)get_supabase_client()— Creates per-request Supabase client with user's JWT (RLS-scoped)get_tenant_id()— Readsuser_tenantstable (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
| Header | Value | Purpose |
|---|---|---|
X-Content-Type-Options | nosniff | Prevent MIME sniffing |
X-Frame-Options | DENY | Prevent clickjacking |
Cache-Control | no-store | Prevent sensitive data caching |
Referrer-Policy | strict-origin-when-cross-origin | Limit referrer leakage |
Permissions-Policy | Restricted | Disable unused browser APIs |
2. Rate Limiting (NIST SC-5)
backend/app/middleware/rate_limit.py
Token bucket algorithm per client IP with tiered limits:
| Tier | Limit | Paths |
|---|---|---|
| auth | 10 req/min | /api/tenant/credentials |
| mutation | 30 req/min | All POST/PUT/PATCH/DELETE |
| read | 120 req/min | All GET |
| deletion | 5 req/min | DELETE /api/tenant/data |
| unlimited | — | /api/health |
Features:
X-Forwarded-Forsupport for proxied deployments- 429 response with
Retry-After+X-RateLimit-Limit/X-RateLimit-Remainingheaders - 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_msauthenticated,body_hash(SHA-256),client_ip,user_agent
- Sensitive fields auto-redacted via
_REDACTED_FIELDSset
Frontend Middleware
frontend/src/middleware.ts adds headers on all Next.js responses:
Strict-Transport-Securitywithpreload- Content Security Policy (CSP)
X-Frame-Options: DENYPermissions-Policy(restrictive)X-Request-IDfor 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:
- Explicit key:
CREDENTIAL_ENCRYPTION_KEYenv var (recommended for production) - Automatic fallback: SHA-256 of
SUPABASE_SERVICE_ROLE_KEY
Credential API
backend/app/routers/credentials.py:
| Endpoint | Method | Purpose |
|---|---|---|
/api/tenant/credentials | GET | Returns boolean flags (never leaks secrets) + URL preview |
/api/tenant/credentials | PUT | Encrypts values before storing in tenants table |
/api/tenant/credentials | DELETE | Nulls all credential columns |
Credential Resolution Priority
When running Shopify sync, credentials are resolved in order:
- Request body (explicit per-request)
- DB (encrypted per-tenant via Fernet)
- Environment variables (global fallback)
Data Lifecycle Management (NIST SI-12)
backend/app/routers/data_lifecycle.py:
| Endpoint | Method | Purpose |
|---|---|---|
/api/tenant/data/export | GET | Exports all 13 tenant-scoped tables as structured JSON |
/api/tenant/data | DELETE | Deletes 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
| Workflow | Checks |
|---|---|
| CodeQL SAST | Static analysis for JS/TS + Python |
| Dependabot | Automated dependency updates (pip + npm + GitHub Actions) |
| Gitleaks | Secrets scanning on push |
| Trivy | Container vulnerability scanning (SARIF → GitHub Security) |
| SBOM | CycloneDX + Syft software bill of materials (90-day retention) |
| License Compliance | pip-licenses + license-checker (fails on copyleft) |
| OWASP ZAP DAST | Dynamic application security testing (weekly + on push) |
Container Hardening
backend/Dockerfile:
- Python 3.12 slim base
gcc/g++installed for native deps, then removed afterpip install- Non-root user (
appuser) for runtime - Minimal attack surface
Role-Based Access Control (RBAC)
Endpoint-level authorization via require_role() FastAPI dependency guard.
| Role | Capabilities |
|---|---|
| owner | Full control: team management, settings, credentials, data lifecycle, all pipelines |
| admin | Invite/remove members (not owner), configure settings, run all pipelines |
| member | View all data, run scrapes/matches, add competitors. Cannot manage team or settings |
| viewer | Read-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