You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
// API to manage IP whitelistinterfaceIpWhitelistEntry{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;}
// IP Whitelist Guard
@Injectable()exportclassIpWhitelistGuardimplementsCanActivate{constructor(privatereadonlyipWhitelistService: IpWhitelistService,){}asynccanActivate(context: ExecutionContext): Promise<boolean>{constrequest=context.switchToHttp().getRequest();constapiKeyId=request.apiKey?.id;if(!apiKeyId){returntrue;// Let other guards handle missing API key}constclientIp=this.getClientIp(request);constwhitelist=awaitthis.ipWhitelistService.getByApiKey(apiKeyId);// If no whitelist entries, allow all IPsif(whitelist.length===0){returntrue;}// Check if IP matches any whitelist entryconstisAllowed=whitelist.some(entry=>this.ipMatches(clientIp,entry));if(!isAllowed){thrownewForbiddenException({code: 'IP_NOT_WHITELISTED',message: `IP address ${clientIp} is not in the whitelist`,});}returntrue;}privategetClientIp(request: Request): string{// Handle proxies (X-Forwarded-For, X-Real-IP)constforwarded=request.headers['x-forwarded-for'];if(forwarded){return(forwardedasstring).split(',')[0].trim();}returnrequest.headers['x-real-ip']asstring||request.socket.remoteAddress||'';}privateipMatches(clientIp: string,entry: IpWhitelistEntry): boolean{if(!entry.active)returnfalse;if(entry.cidrRange){returnthis.ipInCidr(clientIp,entry.cidrRange);}returnclientIp===entry.ipAddress;}privateipInCidr(ip: string,cidr: string): boolean{// IPv4-only example. For IPv6 support, use a library like ipaddr.js.const[range,bits]=cidr.split('/');constmask=~(2**(32-parseInt(bits))-1);constipNum=this.ipToNumber(ip);constrangeNum=this.ipToNumber(range);return(ipNum&mask)===(rangeNum&mask);}privateipToNumber(ip: string): number{returnip.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
// Secure CORS configurationconstcorsOptions={origin: (origin,callback)=>{constallowedOrigins=process.env.CORS_ORIGINS?.split(',')||[];// Allow requests with no origin (mobile apps, Postman)if(!origin)returncallback(null,true);if(allowedOrigins.includes(origin)||allowedOrigins.includes('*')){callback(null,true);}else{callback(newError('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
// config/secrets.tsimport{readFileSync,existsSync}from'fs';exportfunctiongetSecret(name: string): string{// Try file-based secret first (Docker secrets)constfilePath=process.env[`${name}_FILE`];if(filePath&&existsSync(filePath)){returnreadFileSync(filePath,'utf8').trim();}// Fall back to environment variableconstenvValue=process.env[name];if(!envValue){thrownewError(`Secret ${name} not configured`);}returnenvValue;}// UsageconstencryptionKey=getSecret('ENCRYPTION_KEY');constdbPassword=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 dataasyncfunctionrotateEncryptionKey(oldKey: string,newKey: string): Promise<void>{// 1. Get all encrypted recordsconstsessions=awaitsessionRepo.find();for(constsessionofsessions){// 2. Decrypt with old keyconstauthState=decrypt(session.authState,oldKey);// 3. Re-encrypt with new keysession.authState=encrypt(authState,newKey);awaitsessionRepo.save(session);}logger.log('Key rotation completed',{recordsUpdated: sessions.length});}
## Runbook: Suspected Data Breach### Detection Signals- Unusual API access patterns
- Large data exports
- Authentication from new locations
- Failed auth attempts spike
### Immediate Steps1. 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.