Comprehensive security documentation for OpenAlert deployment and operation.
- Security Features
- Authentication
- Authorization (RBAC)
- Secure Configuration
- API Security
- Network Security
- Data Protection
- Vulnerability Management
- Incident Response
- Compliance
- Security Best Practices
OpenAlert implements multiple layers of security:
- Authentication: JWT-based authentication with 7-day token expiry
- Password Hashing: Bcrypt with 10 rounds (cost factor)
- Rate Limiting: Configurable per-endpoint throttling
- CORS Protection: Whitelist-based origin validation
- Security Headers: Helmet.js middleware for HTTP headers
- Input Validation: class-validator for DTO validation
- SQL Injection Prevention: Parameterized queries via Drizzle ORM
- XSS Prevention: Input sanitization and Content Security Policy
- RBAC: Role-Based Access Control with 4 global roles + team roles
- Audit Logging: Correlation IDs for request tracing
OpenAlert automatically sets these security headers via Helmet.js:
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: SAMEORIGIN
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Content-Security-Policy: default-src 'self'; style-src 'self' 'unsafe-inline'OpenAlert uses JSON Web Tokens (JWT) for stateless authentication.
Token Structure:
{
"sub": "user-id",
"email": "user@example.com",
"role": "responder",
"iat": 1706908800,
"exp": 1707513600
}Token Properties:
- Algorithm: HS256 (HMAC with SHA-256)
- Expiry: 7 days from issuance
- Storage: Client-side (localStorage or httpOnly cookie recommended)
- Transmission: Authorization header:
Bearer <token>
For local authentication, enforce strong passwords:
Minimum Requirements:
- At least 8 characters
- At least one uppercase letter
- At least one lowercase letter
- At least one number
- At least one special character
Implementation:
Update user creation to enforce password policy:
// Example password validation (implement in DTOs)
@IsStrongPassword({
minLength: 8,
minLowercase: 1,
minUppercase: 1,
minNumbers: 1,
minSymbols: 1
})
password: string;Passwords are hashed using bcrypt with a cost factor of 10:
import * as bcrypt from 'bcrypt';
const SALT_ROUNDS = 10;
const hashedPassword = await bcrypt.hash(plainPassword, SALT_ROUNDS);Never:
- Store passwords in plain text
- Log passwords (even hashed)
- Transmit passwords over unencrypted connections
- Share passwords between users
Status: Not yet implemented
Roadmap: MFA support planned for v2.0 with:
- TOTP (Time-based One-Time Password)
- SMS-based codes
- Authenticator app support
OpenAlert implements 4 global user roles:
| Role | Description | Permissions |
|---|---|---|
superadmin |
System administrator | Full access to all resources and settings |
admin |
Organization administrator | Manage teams, users, services, and configurations |
responder |
On-call responder | Manage incidents, acknowledge/resolve alerts |
observer |
Read-only user | View incidents and alerts only |
Within teams, users have additional role-based permissions:
| Role | Description | Permissions |
|---|---|---|
team_admin |
Team administrator | Manage team members, services, and schedules |
member |
Team member | Participate in on-call, manage incidents |
observer |
Team observer | View team resources (read-only) |
| Action | Superadmin | Admin | Responder | Observer |
|---|---|---|---|---|
| View incidents | ✅ | ✅ | ✅ | ✅ |
| Acknowledge incidents | ✅ | ✅ | ✅ | ❌ |
| Resolve incidents | ✅ | ✅ | ✅ | ❌ |
| Create services | ✅ | ✅ | ❌ | ❌ |
| Manage teams | ✅ | ✅ | ❌ | ❌ |
| Manage users | ✅ | ✅ | ❌ | ❌ |
| System settings | ✅ | ❌ | ❌ | ❌ |
| Manage integrations | ✅ | ✅ | Team Admin | ❌ |
| Manage schedules | ✅ | ✅ | Team Admin | ❌ |
Protecting Endpoints:
@Controller('incidents')
@UseGuards(JwtAuthGuard, RolesGuard)
export class IncidentsController {
// Superadmin, admin, and responder can acknowledge
@Patch(':id/acknowledge')
@RequireRole('superadmin', 'admin', 'responder')
async acknowledge(@Param('id') id: string) {
// ...
}
// Only superadmin can delete
@Delete(':id')
@RequireRole('superadmin')
async delete(@Param('id') id: string) {
// ...
}
}Team-Based Authorization:
@Get(':id')
@UseGuards(JwtAuthGuard, TeamMemberGuard)
@TeamResourceDecorator('incident')
async getIncident(@Param('id') id: string) {
// User must be a member of the team that owns this incident
}Critical: Use a cryptographically random secret for JWT signing.
Generate a Secure Secret:
# Node.js
node -e "console.log(require('crypto').randomBytes(64).toString('hex'))"
# OpenSSL
openssl rand -hex 64
# Python
python -c "import secrets; print(secrets.token_hex(64))"Output Example:
a7f3c2e9b1d4f6a8c3e7b9d2a5f8c1e4b7d9a3f6c8e2b5d7a9c4f1e6b8d3a7c2
Set in .env:
JWT_SECRET=a7f3c2e9b1d4f6a8c3e7b9d2a5f8c1e4b7d9a3f6c8e2b5d7a9c4f1e6b8d3a7c2Requirements:
- Minimum 32 characters (64+ recommended)
- Cryptographically random (not a dictionary word or phrase)
- Unique per environment (dev, staging, prod)
- Never committed to version control
- Rotated periodically (e.g., quarterly)
Similar to JWT_SECRET, generate a random secret for cookie signing:
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"Set in .env:
COOKIE_SECRET=b8e4c1f7a9d3e6b2c5f8a1d4e7b9c3f6a8d2e5b7c9f1a4e6d8b3c7f2a9e5PostgreSQL Password Requirements:
- Minimum 16 characters
- Mix of letters, numbers, and symbols
- Avoid common passwords
- Unique (not reused from other systems)
Generate Secure Password:
openssl rand -base64 24Store Securely:
- Use environment variables
- Use secret managers (AWS Secrets Manager, Azure Key Vault, HashiCorp Vault)
- Never log database credentials
- Restrict database user permissions (GRANT only necessary privileges)
Best Practices:
-
Never commit
.envto version control# Add to .gitignore .env .env.* !.env.example
-
Use different secrets per environment
- Development:
.env.dev - Staging:
.env.staging - Production:
.env.prod
- Development:
-
Restrict file permissions
chmod 600 .env
-
Use secret managers in production
- AWS: Secrets Manager or Parameter Store
- Azure: Key Vault
- GCP: Secret Manager
- Kubernetes: Secrets
-
Rotate secrets regularly
- JWT_SECRET: Quarterly
- Database passwords: Annually
- API keys: Per provider recommendation
Enforce HTTPS in production:
-
Obtain SSL Certificate
- Use Let's Encrypt (free, automated)
- Or purchase from CA (Digicert, Sectigo, etc.)
-
Configure Reverse Proxy (Nginx)
server { listen 443 ssl http2; ssl_certificate /etc/letsencrypt/live/openalert.example.com/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/openalert.example.com/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; }
-
Force HTTPS Redirect
server { listen 80; return 301 https://$server_name$request_uri; }
-
Enable HSTS
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
Rate limiting prevents abuse and DDoS attacks.
Default Limits:
- API Endpoints: 1000 requests/minute per user
- Webhooks: 100 requests/minute per integration key
- Public Endpoints: 100 requests/minute per IP
Configure Rate Limits:
# In .env
RATE_LIMIT=1000 # requests per minuteCustom Rate Limits (Code):
@Throttle({ default: { ttl: 60000, limit: 10 } }) // 10 req/min
@Post('login')
async login() {
// ...
}Monitor Rate Limiting:
- Log rate limit violations
- Alert on sustained high request rates
- Consider IP-based blocking for persistent abuse
Production CORS Settings:
# In .env
ALLOWED_ORIGINS=https://openalert.example.com,https://app.example.comNever use * in production:
// ❌ BAD - allows all origins
app.enableCors({ origin: '*' });
// ✅ GOOD - whitelist specific origins
app.enableCors({
origin: process.env.ALLOWED_ORIGINS.split(','),
credentials: true
});Integration Keys (Webhooks):
- 64-character random hex strings
- Generated server-side with crypto.randomBytes
- Stored hashed in database (optional for extra security)
- Rotatable without service disruption
- Scoped to specific service
Protecting Integration Keys:
- Don't log integration keys
- Don't expose in error messages
- Use HTTPS for all webhook traffic
- Rotate if compromised
Recommended Firewall Rules:
# Allow SSH (restrict to known IPs if possible)
sudo ufw allow from YOUR_IP to any port 22
# Allow HTTPS
sudo ufw allow 443/tcp
# Allow HTTP (for Let's Encrypt, redirects to HTTPS)
sudo ufw allow 80/tcp
# Deny all other inbound traffic
sudo ufw default deny incoming
# Allow all outbound traffic
sudo ufw default allow outgoing
# Enable firewall
sudo ufw enablePostgreSQL Security:
-
Restrict Network Access
# pg_hba.conf - only allow local connections host openalert openalert 127.0.0.1/32 md5 -
Disable Remote Connections (if DB is on same host)
# postgresql.conf listen_addresses = 'localhost' -
Use Strong Authentication
- Require password (md5 or scram-sha-256)
- Consider certificate-based auth for extra security
-
Regular Updates
sudo apt update && sudo apt upgrade postgresql-15
Redis Security:
-
Bind to Localhost Only (if on same host)
# redis.conf bind 127.0.0.1 -
Require Password
# redis.conf requirepass your-strong-redis-password -
Disable Dangerous Commands
# redis.conf rename-command FLUSHALL "" rename-command FLUSHDB "" rename-command CONFIG "" -
Enable Protected Mode
# redis.conf protected-mode yes
Data in Transit:
- All API traffic over HTTPS/TLS 1.2+
- Database connections over SSL/TLS
- Redis connections can use TLS (configure if needed)
Data at Rest:
- Database encryption (enable on managed services)
- Disk encryption (LUKS on Linux, BitLocker on Windows)
- Backup encryption (encrypt before uploading to S3/Azure)
What's Sensitive:
- Passwords (always hashed, never stored plain)
- JWT secrets
- API keys
- Integration keys
- User email addresses
- Phone numbers
- Notification content (may contain PII)
Best Practices:
- Hash passwords with bcrypt (cost factor 10+)
- Never log passwords or tokens
- Redact sensitive data in logs
- Mask phone numbers in UI (show last 4 digits)
- Encrypt backups containing sensitive data
OpenAlert stores:
- User names
- Email addresses
- Phone numbers (for notifications)
- Alert content (may contain infrastructure details)
User Rights (GDPR):
- Right to access:
GET /users/:id(own data) - Right to deletion: Implement user deletion endpoint
- Right to export: Implement data export functionality
Regular Security Audits:
# Check for vulnerabilities
npm audit
# Fix vulnerabilities
npm audit fix
# Fix with breaking changes
npm audit fix --forceAutomated Scanning:
- Enable GitHub Dependabot
- Use Snyk for continuous monitoring
- Set up automated PRs for dependency updates
Update Strategy:
- Monitor security advisories
- Test updates in staging
- Apply critical patches within 24-48 hours
- Apply non-critical updates monthly
Update Process:
# Update all dependencies
npm update
# Check for outdated packages
npm outdated
# Update specific package
npm update package-nameRecommended Schedule:
- Annual professional penetration test
- Quarterly internal security review
- Continuous automated scanning
Focus Areas:
- Authentication bypass
- Authorization flaws
- SQL injection
- XSS vulnerabilities
- CSRF attacks
- API abuse
If OpenAlert is Compromised:
-
Detect and Contain
- Monitor logs for suspicious activity
- Check for unauthorized access attempts
- Isolate affected systems
-
Assess Scope
- Identify what data was accessed
- Determine attack vector
- List affected users
-
Eradicate Threat
- Patch vulnerability
- Rotate all secrets (JWT_SECRET, database passwords, API keys)
- Force logout all users (invalidate all JWTs)
-
Recover
- Restore from clean backup if needed
- Re-deploy from known-good state
- Verify system integrity
-
Post-Incident
- Document incident
- Notify affected users (if PII compromised)
- Implement additional controls
- Update incident response plan
JWT Secret Rotation:
- Generate new JWT_SECRET
- Update environment variable
- Restart application
- All existing tokens become invalid
- Users must re-authenticate
Database Password Rotation:
# In PostgreSQL
ALTER USER openalert WITH PASSWORD 'new-secure-password';
# Update DATABASE_URL in .env
# Restart applicationIntegration Key Rotation:
# Via API
PATCH /integrations/:id/rotate-key
Authorization: Bearer {superadmin-token}
# Returns new integration key
# Update monitoring system with new URLEnable Audit Logging:
OpenAlert includes correlation IDs in all logs:
{
"timestamp": "2024-02-03T10:30:00Z",
"level": "info",
"message": "User authenticated",
"correlationId": "550e8400-e29b-41d4-a716-446655440000",
"userId": 123,
"action": "auth:login"
}Log Retention:
- Production: 90 days minimum
- Compliance: 1+ year if required
Review Logs For:
- Failed login attempts (potential brute force)
- Authorization failures (potential privilege escalation)
- Unusual API usage patterns
- Database errors (potential SQL injection)
If serving EU users, OpenAlert must comply with GDPR:
Required Features:
- User Consent: Obtain consent for data collection
- Data Access: Users can request their data
- Data Portability: Provide data export in JSON
- Data Deletion: Delete user data on request
- Breach Notification: Notify users within 72 hours of breach
Implementation:
// Data export endpoint
@Get('users/:id/export')
@UseGuards(JwtAuthGuard)
async exportUserData(@Param('id') id: string, @CurrentUser() user) {
// Verify user can only export own data
if (user.id !== Number(id) && user.role !== 'superadmin') {
throw new ForbiddenException();
}
return this.usersService.exportData(Number(id));
}
// Data deletion endpoint
@Delete('users/:id')
@UseGuards(JwtAuthGuard, RolesGuard)
@RequireRole('superadmin')
async deleteUser(@Param('id') id: string) {
return this.usersService.deleteUserData(Number(id));
}Define Retention Periods:
| Data Type | Retention Period | Rationale |
|---|---|---|
| User accounts | Until deletion requested | Account management |
| Incidents | 1 year | Historical analysis |
| Alerts | 90 days | Recent troubleshooting |
| Audit logs | 90 days (1 year for compliance) | Security review |
| Metrics | 30 days high-res, 1 year aggregated | Performance monitoring |
Implement Automatic Cleanup:
// Cron job to delete old data
@Cron('0 0 * * *') // Daily at midnight
async cleanupOldData() {
const ninetyDaysAgo = new Date(Date.now() - 90 * 24 * 60 * 60 * 1000);
await this.db.delete(alerts).where(lt(alerts.createdAt, ninetyDaysAgo));
}- Use parameterized queries (Drizzle ORM handles this)
- Validate all user input with class-validator
- Sanitize output to prevent XSS
- Use HTTPS in all environments (even dev)
- Never commit secrets to version control
- Review code for security issues before merge
- Use security linters (ESLint security plugins)
- Use strong, random secrets (JWT_SECRET, etc.)
- Enable HTTPS/TLS
- Configure CORS restrictively
- Set appropriate rate limits
- Enable security headers (Helmet.js)
- Restrict database/Redis network access
- Use separate credentials per environment
- Enable audit logging
- Monitor logs for suspicious activity
- Set up security alerts (failed logins, etc.)
- Keep dependencies updated
- Rotate secrets regularly
- Perform security audits
- Test backup restoration
- Maintain incident response plan
- Train team on security practices
- Enforce strong password policy
- Encourage MFA when available
- Train users on phishing awareness
- Document password reset process
- Limit admin access (principle of least privilege)
Before going to production, verify:
- HTTPS enforced
- Strong JWT_SECRET configured
- Database password is strong and unique
- CORS configured with specific origins
- Rate limiting enabled
- Security headers enabled (Helmet.js)
- Database not publicly accessible
- Redis not publicly accessible
- Firewall rules configured
- Secrets not in version control
- npm audit shows no critical vulnerabilities
- Backups encrypted
- Audit logging enabled
- Incident response plan documented
- Security contact defined
If you discover a security vulnerability in OpenAlert:
- Do NOT open a public GitHub issue
- Email security@openalert.example.com with:
- Description of the vulnerability
- Steps to reproduce
- Potential impact
- Suggested fix (if available)
- Allow 48 hours for initial response
- Allow 90 days for fix before public disclosure