Skip to main content

Security & Authentication

Comprehensive security model for the Noozer platform


Table of Contents

  1. Overview
  2. Authentication Flows
  3. API Key Management
  4. Admin Authentication
  5. Authorization & Scopes
  6. Rate Limiting
  7. Data Security
  8. Webhook Security
  9. Infrastructure Security
  10. Compliance & Privacy
  11. Security Monitoring

Overview

Security Layers

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY ARCHITECTURE │
└─────────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────────┐
│ LAYER 1: NETWORK │
│ ├── Cloudflare WAF (Web Application Firewall) │
│ ├── DDoS protection (automatic) │
│ ├── TLS 1.3 enforced │
│ └── Geographic restrictions (optional) │
├─────────────────────────────────────────────────────────────────────────────┤
│ LAYER 2: AUTHENTICATION │
│ ├── API Keys (client apps) │
│ ├── Bearer Tokens (admin console) │
│ ├── Service Bindings (worker-to-worker) │
│ └── HMAC Signatures (webhooks) │
├─────────────────────────────────────────────────────────────────────────────┤
│ LAYER 3: AUTHORIZATION │
│ ├── Scope-based permissions │
│ ├── Resource ownership validation │
│ ├── Tier-based feature access │
│ └── Admin role-based access control │
├─────────────────────────────────────────────────────────────────────────────┤
│ LAYER 4: DATA │
│ ├── Encryption at rest (D1, R2, KV) │
│ ├── Field-level encryption (PII) │
│ ├── Audit logging │
│ └── Data isolation (multi-tenant) │
└─────────────────────────────────────────────────────────────────────────────┘

Trust Boundaries


Authentication Flows

Client Authentication (API Keys)

┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT AUTHENTICATION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

1. REGISTRATION
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │ ──────► │ API │ ──────► │ D1 │
└─────────┘ POST └─────────┘ INSERT └─────────┘
/v1/auth/register
{email, password, name}

Response: {customer_id, email, message: "Verify email"}

2. EMAIL VERIFICATION
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Email │ ──────► │ Client │ ──────► │ API │
└─────────┘ click └─────────┘ GET └─────────┘
/v1/auth/verify?token=xxx

Response: {verified: true}

3. API KEY GENERATION
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │ ──────► │ API │ ──────► │ D1 │
└─────────┘ POST └─────────┘ INSERT └─────────┘
/v1/auth/keys
{name: "Production"}

Response: {
api_key: "nz_live_a1b2c3d4e5f6...", // ONLY SHOWN ONCE
key_id: "uuid",
prefix: "nz_live_a1b2",
created_at: "..."
}

4. API REQUEST
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Client │ ──────► │ API │ ──────► │ Handler │
└─────────┘ └─────────┘ └─────────┘
GET /v1/feed
X-API-Key: nz_live_a1b2c3d4e5f6...

Admin Authentication (Sessions + Tokens)

┌─────────────────────────────────────────────────────────────────────────────┐
│ ADMIN AUTHENTICATION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

1. LOGIN (Session Creation)
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Admin │ ──────► │ Admin │ ──────► │ D1 │
│ Console │ POST │ API │ Verify │ + KV │
└─────────┘ └─────────┘ └─────────┘
/v1/admin/auth/login
{email, password, mfa_code?}

Response: {
access_token: "eyJ...", // JWT, 15 min expiry
refresh_token: "rt_...", // Opaque, 7 day expiry
expires_at: "..."
}

2. TOKEN REFRESH
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Admin │ ──────► │ Admin │ ──────► │ KV │
│ Console │ POST │ API │ Lookup │ │
└─────────┘ └─────────┘ └─────────┘
/v1/admin/auth/refresh
{refresh_token: "rt_..."}

Response: {access_token: "eyJ...", expires_at: "..."}

3. API REQUEST
┌─────────┐ ┌─────────┐ ┌─────────┐
│ Admin │ ──────► │ Admin │ ──────► │ Handler │
│ Console │ │ API │ Verify └─────────┘
└─────────┘ └─────────┘ JWT
GET /v1/admin/pipeline/status
Authorization: Bearer eyJ...

4. LOGOUT
POST /v1/admin/auth/logout
- Revokes refresh token
- Clears KV session

API Key Management

Key Format

┌─────────────────────────────────────────────────────────────────────────────┐
│ API KEY FORMAT │
└─────────────────────────────────────────────────────────────────────────────┘

Format: nz_{environment}_{random}

Examples:
nz_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6 (production)
nz_test_x9y8z7w6v5u4t3s2r1q0p9o8n7m6l5k4 (testing)
nz_dev_1234567890abcdef1234567890abcdef (development)

Components:
├── Prefix: "nz_" (identifies as Noozer key)
├── Environment: "live" | "test" | "dev"
└── Random: 32 chars, base62 (a-z, A-Z, 0-9)

Total length: 41 characters

Key Generation

// Key generation pseudocode
function generateApiKey(environment: 'live' | 'test' | 'dev'): {
key: string;
hash: string;
prefix: string;
} {
// Generate 32 bytes of cryptographic randomness
const random = crypto.getRandomValues(new Uint8Array(32));
const randomStr = base62Encode(random); // 32 chars

const key = `nz_${environment}_${randomStr}`;
const prefix = key.substring(0, 12); // "nz_live_a1b2"

// Hash for storage (NEVER store plaintext)
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(key)
);

return {
key, // Return to user ONCE, never store
hash: hexEncode(hash), // Store this
prefix // Store for identification
};
}

Key Storage

API_KEYS table:
├── id: uuid
├── customer_id: uuid
├── key_hash: string # SHA-256 hash of full key
├── key_prefix: string # First 12 chars for identification
├── name: string # User-provided name
├── environment: enum # live | test | dev
├── scopes: string[] # Permissions granted
├── rate_limit_tier: enum # standard | elevated | unlimited
├── last_used_at: timestamp
├── last_used_ip: string?
├── expires_at: timestamp? # Optional expiration
├── is_active: boolean
├── created_at: timestamp
└── revoked_at: timestamp?

Key Validation

// Key validation pseudocode
async function validateApiKey(key: string): Promise<{
valid: boolean;
customer_id?: string;
scopes?: string[];
error?: string;
}> {
// 1. Check format
if (!key.match(/^nz_(live|test|dev)_[a-zA-Z0-9]{32}$/)) {
return { valid: false, error: 'Invalid key format' };
}

// 2. Hash and lookup
const hash = sha256(key);
const keyRecord = await db.query(
'SELECT * FROM api_keys WHERE key_hash = ? AND is_active = true',
[hash]
);

if (!keyRecord) {
return { valid: false, error: 'Invalid API key' };
}

// 3. Check expiration
if (keyRecord.expires_at && keyRecord.expires_at < now()) {
return { valid: false, error: 'API key expired' };
}

// 4. Update last used
await db.query(
'UPDATE api_keys SET last_used_at = ?, last_used_ip = ? WHERE id = ?',
[now(), request.ip, keyRecord.id]
);

return {
valid: true,
customer_id: keyRecord.customer_id,
scopes: keyRecord.scopes
};
}

Key Rotation

┌─────────────────────────────────────────────────────────────────────────────┐
│ KEY ROTATION FLOW │
└─────────────────────────────────────────────────────────────────────────────┘

1. Create new key (old key still active)
POST /v1/auth/keys

2. Update application with new key

3. Verify new key works
GET /v1/auth/me (with new key)

4. Revoke old key
DELETE /v1/auth/keys/{old_key_id}

Grace period: Keys can coexist for migration
Recommendation: Rotate every 90 days

Admin Authentication

JWT Structure

┌─────────────────────────────────────────────────────────────────────────────┐
│ ADMIN JWT TOKEN │
└─────────────────────────────────────────────────────────────────────────────┘

Header:
{
"alg": "ES256", // ECDSA with P-256 curve
"typ": "JWT"
}

Payload:
{
"sub": "admin_user_id", // Admin user ID
"email": "admin@example.com",
"roles": ["admin", "reviewer"],
"permissions": [
"admin:read",
"admin:write",
"review:read",
"review:write"
],
"iat": 1702750800, // Issued at
"exp": 1702751700, // Expires (15 min later)
"iss": "noozer", // Issuer
"aud": "noozer-admin" // Audience
}

Signature:
ECDSA signature using private key

Refresh Token

Refresh Token Format: rt_{random_64_chars}

Storage (KV):
Key: refresh_token:{hash}
Value: {
admin_user_id: string,
created_at: timestamp,
expires_at: timestamp, // 7 days
user_agent: string,
ip_address: string
}
TTL: 7 days

Multi-Factor Authentication (MFA)

┌─────────────────────────────────────────────────────────────────────────────┐
│ MFA OPTIONS │
└─────────────────────────────────────────────────────────────────────────────┘

Option 1: TOTP (Time-based One-Time Password)
- Standard: RFC 6238
- Apps: Google Authenticator, Authy, 1Password
- Setup: QR code + secret key backup
- Validation: 6-digit code, 30-second window

Option 2: Email OTP (fallback)
- 6-digit code sent to verified email
- Expires in 10 minutes
- Rate limited: 3 requests per hour

Storage:
ADMIN_USERS:
├── mfa_enabled: boolean
├── mfa_secret: string (encrypted) # TOTP secret
├── mfa_backup_codes: string[] # 10 one-time codes
└── mfa_verified_at: timestamp

Session Management

┌─────────────────────────────────────────────────────────────────────────────┐
│ ADMIN SESSION CONTROLS │
└─────────────────────────────────────────────────────────────────────────────┘

Session Limits:
- Max 5 concurrent sessions per admin
- Oldest session revoked when limit exceeded

Session Timeout:
- Idle timeout: 30 minutes
- Absolute timeout: 8 hours
- Refresh token: 7 days

Session Revocation:
- On password change: all sessions revoked
- On role change: all sessions revoked
- Manual: individual or all sessions

IP Restrictions (optional):
- Allowlist specific IPs/ranges
- Block on geo mismatch
- Alert on new location

Authorization & Scopes

Client Scopes

┌─────────────────────────────────────────────────────────────────────────────┐
│ CLIENT API SCOPES │
└─────────────────────────────────────────────────────────────────────────────┘

Scope Description
─────────────────────────────────────────────────────────────────────────────
read:feed Read personalized feed
read:articles Read article details
read:stories Read story details
read:search Use search endpoints
read:briefings Access AI briefings
read:entities Read entity information
read:profile Read own profile/preferences

write:keywords Create/update keyword sets
write:profile Update profile/preferences
write:feedback Submit article feedback
write:subscriptions Manage story subscriptions

admin:billing Access billing (enterprise)
─────────────────────────────────────────────────────────────────────────────

Default scopes by tier:
Free: read:feed, read:articles, read:stories, write:feedback
Pro: All read:*, write:keywords, write:profile, write:subscriptions
Enterprise: All scopes

Admin Permissions

┌─────────────────────────────────────────────────────────────────────────────┐
│ ADMIN PERMISSIONS │
└─────────────────────────────────────────────────────────────────────────────┘

Permission Description
─────────────────────────────────────────────────────────────────────────────
admin:read View admin dashboard, metrics
admin:write Trigger pipelines, update config
admin:delete Delete resources, run retention

review:read View review queue
review:write Submit review decisions

taxonomy:read View taxonomy/rules
taxonomy:write Edit taxonomy/rules
taxonomy:delete Delete labels, rollback

sources:read View sources
sources:write Add/edit sources
sources:delete Block/remove sources

customers:read View customer list
customers:write Update customer tier/settings
customers:delete Delete customer accounts

costs:read View cost reports
costs:write Set budgets/alerts

system:config Modify system configuration
system:secrets Access/rotate secrets
─────────────────────────────────────────────────────────────────────────────

Roles (bundled permissions):
viewer: *:read
operator: *:read, admin:write, review:write
editor: *:read, *:write
admin: *:read, *:write, *:delete
superadmin: All permissions including system:*

Resource Ownership

// Resource ownership validation
async function checkOwnership(
customerId: string,
resourceType: 'article' | 'keyword_set' | 'profile',
resourceId: string
): Promise<boolean> {

switch (resourceType) {
case 'keyword_set':
const ks = await db.query(
'SELECT customer_id FROM keyword_sets WHERE id = ?',
[resourceId]
);
return ks?.customer_id === customerId;

case 'profile':
const p = await db.query(
'SELECT customer_id FROM customer_profiles WHERE id = ?',
[resourceId]
);
return p?.customer_id === customerId;

case 'article':
// Articles are shared, but scores are per-customer
return true; // Anyone can read articles
}
}

Rate Limiting

Rate Limit Tiers

┌─────────────────────────────────────────────────────────────────────────────┐
│ RATE LIMIT TIERS │
└─────────────────────────────────────────────────────────────────────────────┘

Tier Requests/min Requests/hour Requests/day
─────────────────────────────────────────────────────────────────────────────
Free 60 1,000 10,000
Pro 300 5,000 100,000
Enterprise 1,000 50,000 1,000,000
Unlimited N/A N/A N/A
─────────────────────────────────────────────────────────────────────────────

Burst allowance: 2x rate for 10 seconds

Rate Limit Headers

HTTP/1.1 200 OK
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1702751700
X-RateLimit-Tier: free

# On rate limit exceeded:
HTTP/1.1 429 Too Many Requests
Retry-After: 45
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1702751700

Rate Limit Implementation

KV Storage:

Key: rate:{customer_id}:min:{minute_bucket}
Value: count (integer)
TTL: 2 minutes

Key: rate:{customer_id}:hour:{hour_bucket}
Value: count (integer)
TTL: 2 hours

Key: rate:{customer_id}:day:{day_bucket}
Value: count (integer)
TTL: 2 days

Endpoint-Specific Limits

┌─────────────────────────────────────────────────────────────────────────────┐
│ ENDPOINT-SPECIFIC RATE LIMITS │
└─────────────────────────────────────────────────────────────────────────────┘

Endpoint Limit (per min) Notes
─────────────────────────────────────────────────────────────────────────────
POST /v1/auth/register 5 Prevent spam signups
POST /v1/auth/login 10 Prevent brute force
POST /v1/qa 10 Expensive (LLM calls)
GET /v1/briefings/* 10 Expensive (LLM calls)
POST /v1/profiles/:id/rebuild 5 Expensive (embedding)
POST /v1/keywords 20 Reasonable creation rate
GET /v1/feed 60 Normal read rate
GET /v1/search 30 Moderate (may hit vectors)
─────────────────────────────────────────────────────────────────────────────

Data Security

Encryption at Rest

┌─────────────────────────────────────────────────────────────────────────────┐
│ ENCRYPTION AT REST │
└─────────────────────────────────────────────────────────────────────────────┘

Cloudflare-managed encryption:
- D1: AES-256 encryption (automatic)
- R2: AES-256 encryption (automatic)
- KV: AES-256 encryption (automatic)
- Vectorize: Encrypted (automatic)

Application-level encryption (sensitive fields):
- customer.email → encrypted (for GDPR)
- admin_user.mfa_secret → encrypted
- api_keys.key_hash → already hashed (SHA-256)
- webhook configs with secrets → encrypted

Encryption key management:
- Stored in Cloudflare Secrets
- Rotated quarterly
- Separate keys per environment

Field-Level Encryption

// Field encryption using AES-256-GCM
const ENCRYPTION_KEY = env.FIELD_ENCRYPTION_KEY; // 256-bit key

async function encryptField(plaintext: string): Promise<string> {
const iv = crypto.getRandomValues(new Uint8Array(12));
const key = await crypto.subtle.importKey(
'raw',
hexDecode(ENCRYPTION_KEY),
'AES-GCM',
false,
['encrypt']
);

const ciphertext = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv },
key,
new TextEncoder().encode(plaintext)
);

// Format: base64(iv) + "." + base64(ciphertext)
return base64Encode(iv) + '.' + base64Encode(ciphertext);
}

async function decryptField(encrypted: string): Promise<string> {
const [ivB64, ciphertextB64] = encrypted.split('.');
const iv = base64Decode(ivB64);
const ciphertext = base64Decode(ciphertextB64);

const key = await crypto.subtle.importKey(
'raw',
hexDecode(ENCRYPTION_KEY),
'AES-GCM',
false,
['decrypt']
);

const plaintext = await crypto.subtle.decrypt(
{ name: 'AES-GCM', iv },
key,
ciphertext
);

return new TextDecoder().decode(plaintext);
}

Data Isolation (Multi-tenancy)

┌─────────────────────────────────────────────────────────────────────────────┐
│ MULTI-TENANT DATA ISOLATION │
└─────────────────────────────────────────────────────────────────────────────┘

Principle: Every query includes customer_id filter

Example:
✅ SELECT * FROM keyword_sets WHERE customer_id = ? AND id = ?
❌ SELECT * FROM keyword_sets WHERE id = ?

Implementation:
- All customer-owned tables have customer_id column
- customer_id extracted from validated API key
- ORM/query builder enforces customer_id filter
- Audit log tracks any admin override

Shared data (no customer filter):
- articles (public content)
- sources (shared catalog)
- taxonomy_labels (shared classification)
- locations (reference data)

Webhook Security

Outbound Webhook Signing

┌─────────────────────────────────────────────────────────────────────────────┐
│ WEBHOOK SIGNATURE │
└─────────────────────────────────────────────────────────────────────────────┘

Signature format: HMAC-SHA256

Headers sent:
X-Noozer-Signature: sha256=abc123...
X-Noozer-Timestamp: 1702751700
X-Noozer-Delivery-ID: uuid

Signature computation:
payload = timestamp + "." + request_body
signature = HMAC-SHA256(webhook_secret, payload)

Verification (recipient side):
1. Extract timestamp from header
2. Check timestamp is within 5 minutes of now (replay protection)
3. Compute expected signature
4. Compare signatures (constant-time comparison)

Webhook Configuration

CUSTOMER_PROFILES.notify_channels:
{
"webhook": {
"url": "https://example.com/webhook",
"secret": "encrypted_secret", // Encrypted at rest
"headers": { // Custom headers (optional)
"X-Custom-Header": "value"
},
"retry_policy": {
"max_attempts": 5,
"backoff": "exponential" // 1s, 2s, 4s, 8s, 16s
},
"events": ["new_article", "story_update"]
}
}

Webhook Retry Policy

Attempt 1: Immediate
Attempt 2: +1 second
Attempt 3: +2 seconds (total: 3s)
Attempt 4: +4 seconds (total: 7s)
Attempt 5: +8 seconds (total: 15s)

After 5 failures:
- Mark webhook as failing
- Alert customer (if email configured)
- Continue trying every hour for 24h
- Then disable and notify

Infrastructure Security

Cloudflare Workers Security

┌─────────────────────────────────────────────────────────────────────────────┐
│ WORKER SECURITY CONFIGURATION │
└─────────────────────────────────────────────────────────────────────────────┘

wrangler.toml:
compatibility_date = "2024-01-01"

[vars]
ENVIRONMENT = "production"

# Secrets (stored encrypted, not in code)
# wrangler secret put JWT_SIGNING_KEY
# wrangler secret put FIELD_ENCRYPTION_KEY
# wrangler secret put ZENROWS_API_KEY
# wrangler secret put DATA4SEO_API_KEY

# Service bindings (worker-to-worker auth)
[[services]]
binding = "CRAWLER_SERVICE"
service = "noozer-crawler"

# D1 binding
[[d1_databases]]
binding = "DB"
database_name = "noozer-production"
database_id = "xxx"

Secret Management

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECRETS │
└─────────────────────────────────────────────────────────────────────────────┘

Secret Purpose Rotation
─────────────────────────────────────────────────────────────────────────────
JWT_SIGNING_KEY Sign admin JWTs Yearly
FIELD_ENCRYPTION_KEY Encrypt PII fields Yearly
ZENROWS_API_KEY External API auth On compromise
DATA4SEO_API_KEY External API auth On compromise
SHAREDCOUNT_API_KEY External API auth On compromise
WEBHOOK_SIGNING_KEY Sign outbound webhooks Yearly
ADMIN_BOOTSTRAP_KEY Initial admin setup Delete after use
─────────────────────────────────────────────────────────────────────────────

Storage: Cloudflare Secrets (wrangler secret put)
Access: Only available in worker runtime, not logged

Service Bindings

// Worker-to-worker authentication via Service Bindings
// No explicit auth needed - Cloudflare handles it

// In API Worker:
export default {
async fetch(request: Request, env: Env) {
// Call crawler service - automatically authenticated
const response = await env.CRAWLER_SERVICE.fetch(
new Request('https://internal/crawl', {
method: 'POST',
body: JSON.stringify({ keyword_set_id: '...' })
})
);
return response;
}
};

// env.CRAWLER_SERVICE is a service binding
// Only workers in your account can call it
// No network exposure, no auth tokens needed

Compliance & Privacy

GDPR Compliance

┌─────────────────────────────────────────────────────────────────────────────┐
│ GDPR REQUIREMENTS │
└─────────────────────────────────────────────────────────────────────────────┘

Data Subject Rights:

1. Right to Access (Article 15)
GET /v1/privacy/export
- Returns all personal data
- Format: JSON or CSV
- Delivered within 30 days

2. Right to Rectification (Article 16)
PUT /v1/profiles, PUT /v1/auth/me
- Users can update their data

3. Right to Erasure (Article 17)
DELETE /v1/auth/me
- Deletes: customer, profiles, keywords, scores, events
- Retains: articles (public), anonymized analytics
- Processing: within 30 days

4. Right to Data Portability (Article 20)
GET /v1/privacy/export?format=portable
- Machine-readable format
- Includes: keywords, profiles, scores, events

5. Right to Object (Article 21)
PUT /v1/privacy/preferences
- Opt out of marketing
- Opt out of analytics

Personal Data Inventory:
- Email (encrypted)
- Name
- API keys (hashed)
- Usage logs (retained 90 days)
- Feedback submitted

Data Retention

┌─────────────────────────────────────────────────────────────────────────────┐
│ DATA RETENTION POLICY │
└─────────────────────────────────────────────────────────────────────────────┘

Data Type Retention After Expiry
─────────────────────────────────────────────────────────────────────────────
Articles (metadata) 90 days Archive to R2, then delete
Articles (raw HTML) 30 days Delete
Customer events 90 days Aggregate, then delete
API request logs 30 days Delete
Cost events 90 days Aggregate to rollups
Notification logs 30 days Delete
Pipeline errors 30 days Delete
Stories Indefinite Archive after 1 year inactive
─────────────────────────────────────────────────────────────────────────────

Customer data after account deletion:
- Immediate: Anonymize or delete PII
- 30 days: Complete deletion from all storage
- Retained: Aggregated, anonymized statistics

Audit Logging

AUDIT_LOGS table:
├── id: uuid
├── timestamp: timestamp
├── actor_type: enum # customer | admin | system | api_key
├── actor_id: string # Who performed the action
├── action: string # create | read | update | delete
├── resource_type: string # customer | keyword_set | taxonomy | etc
├── resource_id: string # ID of affected resource
├── changes: json # Before/after for updates
├── ip_address: string
├── user_agent: string
├── request_id: string # For correlation
└── created_at: timestamp

Logged actions:
- All admin write/delete operations
- Customer account changes
- API key creation/revocation
- Taxonomy/rules changes
- Configuration changes
- Failed authentication attempts

Security Monitoring

Security Events

┌─────────────────────────────────────────────────────────────────────────────┐
│ SECURITY EVENTS TO MONITOR │
└─────────────────────────────────────────────────────────────────────────────┘

Event Threshold Action
─────────────────────────────────────────────────────────────────────────────
Failed logins (same IP) 5 in 5 min Block IP 1 hour
Failed logins (same account) 10 in 1 hour Lock account
Invalid API keys 100 in 1 min Alert + investigate
Rate limit hits 1000 in 1 min Alert if unusual
Admin login from new IP Any Email notification
Privilege escalation attempt Any Alert + audit
API key usage from new country Any Optional: alert
Unusual API pattern ML-detected Alert + investigate
─────────────────────────────────────────────────────────────────────────────

Security Alerts

Alert Channels:
- PagerDuty (critical)
- Slack #security (high/medium)
- Email digest (low)

Critical (immediate page):
- Suspected breach
- Data exfiltration attempt
- System compromise indicators

High (immediate Slack):
- Brute force attacks
- Unusual admin activity
- Service degradation

Medium (batched Slack):
- Rate limit abuse
- Failed auth spikes
- New source patterns

Low (daily email):
- Login anomalies
- API usage trends
- Compliance reminders

Incident Response

┌─────────────────────────────────────────────────────────────────────────────┐
│ INCIDENT RESPONSE PLAYBOOK │
└─────────────────────────────────────────────────────────────────────────────┘

1. DETECT
- Automated alerts
- Customer reports
- Routine monitoring

2. CONTAIN
- Isolate affected systems
- Revoke compromised credentials
- Block malicious IPs/actors

3. INVESTIGATE
- Collect logs (audit, access, error)
- Identify scope and impact
- Determine root cause

4. REMEDIATE
- Patch vulnerability
- Rotate affected secrets
- Restore from backup if needed

5. RECOVER
- Restore normal operations
- Monitor for recurrence
- Validate integrity

6. POST-MORTEM
- Document timeline
- Identify improvements
- Update runbooks
- Notify affected parties (if required)

Security Checklist

Pre-Launch

[ ] All secrets in Cloudflare Secrets (not code)
[ ] JWT signing key generated and stored
[ ] Field encryption key generated and stored
[ ] Admin MFA enforced
[ ] Rate limiting configured
[ ] WAF rules enabled
[ ] Audit logging enabled
[ ] GDPR export endpoint working
[ ] Account deletion flow tested
[ ] Webhook signing working
[ ] IP allowlist for admin (if required)
[ ] Security monitoring alerts configured
[ ] Incident response runbook documented

Ongoing

[ ] Rotate JWT signing key (yearly)
[ ] Rotate field encryption key (yearly)
[ ] Review admin access (quarterly)
[ ] Review API key usage (monthly)
[ ] Audit log review (weekly)
[ ] Penetration test (yearly)
[ ] Dependency vulnerability scan (weekly)
[ ] Review rate limit thresholds (monthly)
[ ] Update WAF rules (as needed)
[ ] Incident response drill (yearly)

Last updated: [DATE] Security contact: [EMAIL]