Skip to content

Latest commit

 

History

History
937 lines (747 loc) · 20.8 KB

File metadata and controls

937 lines (747 loc) · 20.8 KB

04 - Security Design

4.1 Security Overview

flowchart TB
    subgraph External["External Threats"]
        A1[Unauthorized Access]
        A2[Data Breach]
        A3[DDoS Attack]
        A4[Injection Attack]
    end
    
    subgraph Defense["Defense Layers"]
        D1[Authentication]
        D2[Encryption]
        D3[Rate Limiting]
        D4[Input Validation]
        D5[Audit Logging]
    end
    
    A1 --> D1
    A2 --> D2
    A3 --> D3
    A4 --> D4
    
    D1 --> APP[Application]
    D2 --> APP
    D3 --> APP
    D4 --> APP
    APP --> D5
Loading

4.2 Authentication

API Key Authentication Flow

sequenceDiagram
    participant C as Client
    participant G as Auth Guard
    participant S as Service
    participant DB as Database
    
    C->>G: Request + X-API-Key
    G->>G: Hash API Key
    G->>DB: Find by hash
    alt Key Valid
        DB-->>G: API Key record
        G->>G: Check permissions
        G->>G: Check expiration
        G->>S: Forward request
        S-->>C: Response
    else Key Invalid
        G-->>C: 401 Unauthorized
    end
Loading

API Key Format

Format: owa_<32-character-random-string>
Example: owa_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6

Storage: SHA-256 hash only (never store plain key)

Permission Model

Permission Description
* Full access (admin)
sessions:read View sessions
sessions:write Create/delete sessions
messages:send Send messages
messages:read Read message history
webhooks:manage CRUD webhooks
contacts:read View contacts
groups:read View groups
groups:write Manage groups

4.3 IP Whitelisting

IP whitelisting adds an extra security layer by restricting API key access to specific IP addresses.

IP Whitelist Flow

flowchart TB
    REQ[Incoming Request] --> AUTH[API Key Valid?]
    AUTH -->|No| R401[401 Unauthorized]
    AUTH -->|Yes| WL{IP Whitelist Enabled?}
    WL -->|No| ALLOW[Allow Request]
    WL -->|Yes| CHECK{IP in Whitelist?}
    CHECK -->|No| R403[403 Forbidden]
    CHECK -->|Yes| ALLOW
    ALLOW --> PROCESS[Process Request]
Loading

Configuration

// API to manage IP whitelist
interface IpWhitelistEntry {
  id: string;
  apiKeyId: string;
  ipAddress: string;      // Single IP: "203.0.113.50"
  cidrRange?: string;     // CIDR: "10.0.0.0/24"
  description?: string;
  active: boolean;
  createdAt: Date;
}

API Endpoints

Add IP to Whitelist

POST /api/auth/api-keys/:apiKeyId/whitelist

Request Body:

{
  "ipAddress": "203.0.113.50",
  "description": "Production server"
}

For CIDR Range:

{
  "ipAddress": "10.0.0.0",
  "cidrRange": "10.0.0.0/24",
  "description": "Internal network"
}

List Whitelisted IPs

GET /api/auth/api-keys/:apiKeyId/whitelist

Remove IP from Whitelist

DELETE /api/auth/api-keys/:apiKeyId/whitelist/:entryId

Implementation

// IP Whitelist Guard
@Injectable()
export class IpWhitelistGuard implements CanActivate {
  constructor(
    private readonly ipWhitelistService: IpWhitelistService,
  ) {}

  async canActivate(context: ExecutionContext): Promise<boolean> {
    const request = context.switchToHttp().getRequest();
    const apiKeyId = request.apiKey?.id;

    if (!apiKeyId) {
      return true; // Let other guards handle missing API key
    }

    const clientIp = this.getClientIp(request);
    const whitelist = await this.ipWhitelistService.getByApiKey(apiKeyId);

    // If no whitelist entries, allow all IPs
    if (whitelist.length === 0) {
      return true;
    }

    // Check if IP matches any whitelist entry
    const isAllowed = whitelist.some(entry =>
      this.ipMatches(clientIp, entry)
    );

    if (!isAllowed) {
      throw new ForbiddenException({
        code: 'IP_NOT_WHITELISTED',
        message: `IP address ${clientIp} is not in the whitelist`,
      });
    }

    return true;
  }

  private getClientIp(request: Request): string {
    // Handle proxies (X-Forwarded-For, X-Real-IP)
    const forwarded = request.headers['x-forwarded-for'];
    if (forwarded) {
      return (forwarded as string).split(',')[0].trim();
    }
    return request.headers['x-real-ip'] as string ||
           request.socket.remoteAddress ||
           '';
  }

  private ipMatches(clientIp: string, entry: IpWhitelistEntry): boolean {
    if (!entry.active) return false;

    if (entry.cidrRange) {
      return this.ipInCidr(clientIp, entry.cidrRange);
    }

    return clientIp === entry.ipAddress;
  }

  private ipInCidr(ip: string, cidr: string): boolean {
    // IPv4-only example. For IPv6 support, use a library like ipaddr.js.
    const [range, bits] = cidr.split('/');
    const mask = ~(2 ** (32 - parseInt(bits)) - 1);

    const ipNum = this.ipToNumber(ip);
    const rangeNum = this.ipToNumber(range);

    return (ipNum & mask) === (rangeNum & mask);
  }

  private ipToNumber(ip: string): number {
    return ip.split('.').reduce(
      (acc, octet) => (acc << 8) + parseInt(octet), 0
    ) >>> 0;
  }
}

Best Practices

Practice Description
Use CIDR notation For IP ranges, use CIDR instead of multiple entries
Trusted Proxies Configure trusted proxies for accurate client IP
Regular Review Review whitelist entries regularly
Audit Logging Log all blocked attempts for monitoring
Fallback Plan Prepare a process to update the whitelist when IPs change

IPv6 Support

For IPv6, use a library that supports IPv6 parsing (e.g., ipaddr.js) when performing ipInCidr.

4.4 Data Encryption

Encryption Strategy

flowchart LR
    subgraph Transit["In Transit"]
        TLS[TLS 1.3]
    end
    
    subgraph Rest["At Rest"]
        AES[AES-256-GCM]
    end
    
    subgraph Sensitive["Sensitive Data"]
        AUTH[Auth State]
        PROXY[Proxy Credentials]
        SECRET[Webhook Secrets]
    end
    
    Sensitive --> AES
    AES --> DB[(Database)]
    Client --> TLS --> Server
Loading

What Gets Encrypted

Data Encrypted Method
API Keys Yes SHA-256 hash
Auth State Yes AES-256-GCM
Webhook Secrets Yes AES-256-GCM
Proxy Passwords Yes AES-256-GCM
Message Content Optional AES-256-GCM
Session Config No -

4.5 Input Validation

Validation Rules

flowchart TB
    INPUT[User Input] --> V1{Type Check}
    V1 -->|Pass| V2{Length Check}
    V1 -->|Fail| ERR[400 Error]
    V2 -->|Pass| V3{Format Check}
    V2 -->|Fail| ERR
    V3 -->|Pass| V4{Sanitize}
    V3 -->|Fail| ERR
    V4 --> SAFE[Safe Input]
Loading

Validation Examples

Field Rules
chatId Pattern: ^\d+@(c\.us|g\.us)$
phone Pattern: ^\d{10,15}$
url Valid URL, HTTPS only for webhooks
text Max 65536 chars, sanitized
sessionName Alphanumeric + hyphen, 3-50 chars

DTO Validation

// Example DTO with validation
import { IsString, IsUrl, Matches, MaxLength } from 'class-validator';

export class SendTextDto {
  @IsString()
  @Matches(/^\d+@(c\.us|g\.us)$/, {
    message: 'Invalid chatId format',
  })
  chatId: string;

  @IsString()
  @MaxLength(65536)
  text: string;
}

export class CreateWebhookDto {
  @IsUrl({ protocols: ['https'], require_protocol: true })
  url: string;

  @IsArray()
  @IsIn(['message.received', 'message.sent', 'session.status'], { each: true })
  events: string[];
}

4.6 Rate Limiting

Rate Limit Configuration

flowchart LR
    REQ[Request] --> RL{Rate Limiter}
    RL -->|Under Limit| APP[Application]
    RL -->|Over Limit| ERR[429 Too Many Requests]
    
    subgraph Limits["Limit Tiers"]
        T1[Global: 1000/min]
        T2[Per Key: 100/min]
        T3[Per Endpoint: varies]
    end
Loading

Endpoint Limits

Endpoint Category Rate Limit Window
Session Create 5 1 minute
Session Read 60 1 minute
Message Send 30 1 minute
Message Read 60 1 minute
Webhook CRUD 10 1 minute
Health Check 120 1 minute

Response Headers

X-RateLimit-Limit: 30
X-RateLimit-Remaining: 25
X-RateLimit-Reset: 1706868060
Retry-After: 45

4.7 CORS Configuration

CORS Settings

// Secure CORS configuration
const corsOptions = {
  origin: (origin, callback) => {
    const allowedOrigins = process.env.CORS_ORIGINS?.split(',') || [];
    
    // Allow requests with no origin (mobile apps, Postman)
    if (!origin) return callback(null, true);
    
    if (allowedOrigins.includes(origin) || allowedOrigins.includes('*')) {
      callback(null, true);
    } else {
      callback(new Error('Not allowed by CORS'));
    }
  },
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'],
  allowedHeaders: ['Content-Type', 'X-API-Key', 'X-Request-ID'],
  exposedHeaders: ['X-RateLimit-Limit', 'X-RateLimit-Remaining'],
  maxAge: 86400, // 24 hours
};

4.8 Webhook Security

Webhook Signature

sequenceDiagram
    participant OW as OpenWA
    participant WH as Webhook Endpoint
    
    OW->>OW: Create payload
    OW->>OW: Sign with HMAC-SHA256
    OW->>WH: POST + X-OpenWA-Signature
    WH->>WH: Verify signature
    WH->>WH: Process if valid
    WH-->>OW: 200 OK
Loading

Signature Verification

// OpenWA: Generate signature
function signPayload(payload: object, secret: string): string {
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(JSON.stringify(payload));
  return 'sha256=' + hmac.digest('hex');
}

// Client: Verify signature
function verifySignature(
  payload: string,
  signature: string,
  secret: string
): boolean {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );
}

4.9 Security Headers

Recommended Headers

// Helmet configuration
app.use(helmet({
  contentSecurityPolicy: {
    directives: {
      defaultSrc: ["'self'"],
      styleSrc: ["'self'", "'unsafe-inline'"],
      scriptSrc: ["'self'"],
      imgSrc: ["'self'", "data:", "https:"],
    },
  },
  hsts: {
    maxAge: 31536000,
    includeSubDomains: true,
  },
  noSniff: true,
  referrerPolicy: { policy: 'strict-origin-when-cross-origin' },
}));

Security Headers Checklist

Header Value Purpose
Strict-Transport-Security max-age=31536000 Force HTTPS
X-Content-Type-Options nosniff Prevent MIME sniffing
X-Frame-Options DENY Prevent clickjacking
X-XSS-Protection 1; mode=block XSS filter
Referrer-Policy strict-origin Control referrer

4.10 Audit Logging

What Gets Logged

flowchart TB
    subgraph Events["Logged Events"]
        AUTH[Authentication attempts]
        SESS[Session operations]
        MSG[Message sends]
        WH[Webhook changes]
        ERR[Security errors]
    end
    
    Events --> LOG[Audit Log]
    LOG --> STORE[(Storage)]
    LOG --> ALERT[Alerts]
Loading

Log Format

{
  "timestamp": "2025-02-02T10:00:00.000Z",
  "level": "info",
  "event": "api.request",
  "requestId": "uuid",
  "apiKeyId": "uuid",
  "ip": "192.168.1.1",
  "method": "POST",
  "path": "/api/sessions/sess_123/messages/send-text",
  "statusCode": 200,
  "responseTime": 150,
  "userAgent": "MyApp/1.0"
}

Security Alerts

Event Severity Action
Multiple failed auth High Alert + temp block
Rate limit exceeded Medium Log + block
Invalid signature Medium Log
Unusual activity Low Log

4.11 Security Checklist

Development

  • Input validation on all endpoints
  • SQL injection prevention (parameterized queries)
  • XSS prevention (output encoding)
  • CSRF protection (if using cookies)
  • Secure dependencies (npm audit)
  • No secrets in code

Deployment

  • HTTPS only (TLS 1.2+)
  • Security headers configured
  • Rate limiting enabled
  • CORS properly configured
  • Firewall rules set
  • Regular security updates

Operations

  • Audit logging enabled
  • Log monitoring setup
  • Backup encryption
  • Incident response plan
  • Regular security audits

4.12 Secrets Management

Secrets Inventory

Secret Type Storage Rotation
Database credentials Environment variable 90 days
Redis password Environment variable 90 days
Encryption key Environment variable 365 days
API master key Environment variable 180 days
Webhook secrets Database (encrypted) Per webhook
Session auth state File system (default) / Database (optional, encrypted) Never (tied to WA session)

Environment Variables Security

# ❌ BAD: Secrets in code or docker-compose.yml
DATABASE_URL=postgresql://user:password123@localhost:5432/db

# ✅ GOOD: Use .env file (not committed)
DATABASE_URL=${DATABASE_URL}

# ✅ BETTER: Use Docker secrets or vault
docker secret create db_password ./secret.txt

Docker Secrets

# docker-compose.prod.yml
version: '3.8'

services:
  app:
    image: openwa:latest
    secrets:
      - db_password
      - encryption_key
      - api_master_key
    environment:
      - DATABASE_PASSWORD_FILE=/run/secrets/db_password
      - ENCRYPTION_KEY_FILE=/run/secrets/encryption_key

secrets:
  db_password:
    external: true
  encryption_key:
    external: true
  api_master_key:
    external: true

Reading Secrets in Application

// config/secrets.ts
import { readFileSync, existsSync } from 'fs';

export function getSecret(name: string): string {
  // Try file-based secret first (Docker secrets)
  const filePath = process.env[`${name}_FILE`];
  if (filePath && existsSync(filePath)) {
    return readFileSync(filePath, 'utf8').trim();
  }
  
  // Fall back to environment variable
  const envValue = process.env[name];
  if (!envValue) {
    throw new Error(`Secret ${name} not configured`);
  }
  
  return envValue;
}

// Usage
const encryptionKey = getSecret('ENCRYPTION_KEY');
const dbPassword = getSecret('DATABASE_PASSWORD');

Key Rotation Procedure

flowchart TB
    A[Generate New Key] --> B[Update Secret Store]
    B --> C[Deploy with Both Keys]
    C --> D[Re-encrypt Data with New Key]
    D --> E[Verify All Data Accessible]
    E --> F[Remove Old Key]
    F --> G[Deploy with New Key Only]
Loading
// Key rotation for encrypted data
async function rotateEncryptionKey(
  oldKey: string,
  newKey: string
): Promise<void> {
  // 1. Get all encrypted records
  const sessions = await sessionRepo.find();
  
  for (const session of sessions) {
    // 2. Decrypt with old key
    const authState = decrypt(session.authState, oldKey);
    
    // 3. Re-encrypt with new key
    session.authState = encrypt(authState, newKey);
    
    await sessionRepo.save(session);
  }
  
  logger.log('Key rotation completed', { 
    recordsUpdated: sessions.length 
  });
}

4.13 Dependency Security

npm Audit Workflow

# Check for vulnerabilities
npm audit

# Auto-fix non-breaking vulnerabilities
npm audit fix

# View detailed report
npm audit --json > audit-report.json

GitHub Dependabot Configuration

# .github/dependabot.yml
version: 2
updates:
  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "weekly"
      day: "monday"
    open-pull-requests-limit: 10
    groups:
      development-dependencies:
        dependency-type: "development"
      production-dependencies:
        dependency-type: "production"
    ignore:
      # Major version updates require manual review
      - dependency-name: "*"
        update-types: ["version-update:semver-major"]

Security Scanning in CI

# .github/workflows/security.yml
name: Security Scan

on:
  push:
    branches: [main, develop]
  schedule:
    - cron: '0 0 * * 1'  # Weekly on Monday

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          
      - name: Install dependencies
        run: npm ci
        
      - name: Run npm audit
        run: npm audit --audit-level=high
        
      - name: Run Snyk security scan
        uses: snyk/actions/node@master
        env:
          SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
        with:
          args: --severity-threshold=high
          
      - name: SAST with CodeQL
        uses: github/codeql-action/analyze@v2

Allowed/Blocked Packages

// package.json
{
  "overrides": {
    // Force specific version for security fix
    "lodash": "^4.17.21"
  },
  "scripts": {
    "preinstall": "npx npm-force-resolutions"
  }
}

Vulnerability Response Matrix

Severity Response Time Action
Critical 24 hours Immediate patch or disable
High 72 hours Patch in next release
Medium 2 weeks Plan for next sprint
Low 1 month Backlog item

4.14 Incident Response

Incident Severity Levels

Level Description Example Response Time
P1 - Critical Service down, data breach Auth bypass, data leak 15 minutes
P2 - High Major feature broken Session creation fails 1 hour
P3 - Medium Partial degradation Slow webhook delivery 4 hours
P4 - Low Minor issue UI glitch 24 hours

Incident Response Flow

flowchart TB
    DETECT[Detect Incident] --> ASSESS[Assess Severity]
    ASSESS --> CONTAIN[Contain Threat]
    CONTAIN --> NOTIFY[Notify Stakeholders]
    NOTIFY --> INVESTIGATE[Investigate Root Cause]
    INVESTIGATE --> REMEDIATE[Remediate]
    REMEDIATE --> RECOVER[Recover Service]
    RECOVER --> POSTMORTEM[Post-mortem]
    POSTMORTEM --> IMPROVE[Implement Improvements]
Loading

Security Incident Checklist

## Immediate Actions (First 15 Minutes)
- [ ] Confirm incident is real (not false positive)
- [ ] Assess severity level
- [ ] Create incident channel/thread
- [ ] Assign incident commander

## Containment (First Hour)
- [ ] Identify affected systems
- [ ] Isolate compromised components
- [ ] Preserve evidence (logs, snapshots)
- [ ] Block attacker if identified

## Investigation
- [ ] Timeline of events
- [ ] Entry point identification
- [ ] Scope of compromise
- [ ] Data accessed/exfiltrated

## Recovery
- [ ] Patch vulnerability
- [ ] Reset compromised credentials
- [ ] Restore from clean backup if needed
- [ ] Verify system integrity

## Post-Incident
- [ ] Document lessons learned
- [ ] Update security controls
- [ ] Notify affected users if required
- [ ] Schedule blameless post-mortem

Emergency Contacts

# config/incident-response.yml
contacts:
  primary_oncall:
    name: "On-Call Engineer"
    phone: "+62xxx"
    slack: "@oncall"
    
  security_lead:
    name: "Security Lead"
    email: "security@openwa.dev"
    
  escalation:
    - level: 1
      wait: 15m
      contact: primary_oncall
    - level: 2  
      wait: 30m
      contact: security_lead

communication:
  internal_channel: "#incident-response"
  status_page: "https://status.openwa.dev"

Runbooks

## Runbook: Suspected Data Breach

### Detection Signals
- Unusual API access patterns
- Large data exports
- Authentication from new locations
- Failed auth attempts spike

### Immediate Steps
1. Rotate all API keys for affected accounts
2. Enable IP whitelisting if not already
3. Check audit logs for scope
4. Snapshot affected database

### Evidence Collection
- Export API access logs: `npm run logs:export --since="2h"`
- Database query logs
- Network traffic captures
- System metrics at incident time

Post-Mortem Template

# Incident Post-Mortem: [Title]

**Date:** YYYY-MM-DD
**Severity:** P1/P2/P3
**Duration:** X hours
**Author:** [Name]

## Summary
Brief description of what happened.

## Impact
- Users affected: X
- Data compromised: None/Partial/Full
- Revenue impact: $X

## Timeline
| Time (UTC) | Event |
|------------|-------|
| 10:00 | Alert triggered |
| 10:05 | Incident confirmed |
| 10:15 | Containment started |
| 11:00 | Root cause identified |
| 12:00 | Service restored |

## Root Cause
Technical explanation of what went wrong.

## What Went Well
- Detection was quick
- Communication was clear

## What Went Wrong
- Missing monitoring for X
- Delayed response due to Y

## Action Items
| Item | Owner | Due Date | Status |
|------|-------|----------|--------|
| Add monitoring for X | @eng | 2026-02-15 | Open |
| Update runbook | @security | 2026-02-10 | Open |

## Lessons Learned
Key takeaways for preventing future incidents.