Security
ReliaPulse implements multiple layers of security to protect your data and ensure safe operation.
Authentication
Password Security
Passwords are hashed using bcrypt with cost factor 10:
import bcrypt from "bcryptjs";
// Hashing
const hash = await bcrypt.hash(password, 10);
// Verification
const isValid = await bcrypt.compare(password, hash);Password requirements:
- Minimum 8 characters
- Validated on both client and server
Two-Factor Authentication (2FA)
TOTP-based 2FA following RFC 6238:
-
Setup Flow:
- Generate secret with
otplib - Create QR code for authenticator app
- User verifies with 6-digit code
- 10 backup codes generated
- Generate secret with
-
Login Flow:
- User enters email/password
- Frontend calls
/api/auth/check-2fato validate credentials - If valid and 2FA enabled, shows 2FA code input
- User enters 6-digit code (or backup code)
- Frontend calls NextAuth
signIn()with credentials + 2FA code - Server verifies TOTP and completes login
-
Secret Storage:
- Encrypted with AES-256-GCM
- Encryption key from
ENCRYPTION_KEYenv var
// Encryption
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = cipher.update(secret, 'utf8', 'hex');
// Decryption
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
const decrypted = decipher.update(encrypted, 'hex', 'utf8');API Key Authentication
API keys for programmatic access:
- Storage: SHA-256 hashed (not reversible)
- Format:
sk_live_<random_base64url>(43 chars) - Permissions: Granular, per-resource permissions
// Key generation
const rawKey = `sk_live_${crypto.randomBytes(32).toString('base64url')}`;
const hash = crypto.createHash('sha256').update(rawKey).digest('hex');
// Validation
const inputHash = crypto.createHash('sha256').update(inputKey).digest('hex');
const isValid = inputHash === storedHash;SSO/SAML
Enterprise SSO via SAML Jackson:
- SAML 2.0 compliant
- IdP metadata import
- SP metadata export
- Domain-based SSO detection
Authorization
Role-Based Access Control (RBAC)
8 predefined roles with granular permissions:
| Role | Description |
|---|---|
| OWNER | Full access, can delete organization |
| ADMIN | Manage everything except billing |
| MEMBER | Create/edit components, incidents |
| VIEWER | Read-only access |
| INCIDENT_MANAGER | Manage incidents and postmortems |
| ONCALL_RESPONDER | Manage on-call and alerts |
| STATUS_EDITOR | Edit status pages and branding |
| SUBSCRIBER_MANAGER | Manage subscribers and channels |
Custom Roles
Organizations can create custom roles:
// Create custom role with specific permissions
POST /api/v1/organization/roles
{
"name": "Developer",
"permissions": [
"COMPONENTS_VIEW",
"COMPONENTS_CREATE",
"INCIDENTS_VIEW",
"INCIDENTS_CREATE"
]
}Permission Checking
Every API route validates permissions:
import { requirePermission, PERMISSIONS } from "@/lib/rbac";
export async function POST(request: NextRequest) {
const authResult = await requirePermission(
request,
PERMISSIONS.COMPONENTS_CREATE
);
if (authResult.error) return authResult.error;
// User has permission, proceed
}Input Validation
Request Validation
All inputs validated with Zod:
const createComponentSchema = z.object({
name: z.string().min(1).max(100),
description: z.string().max(500).optional(),
type: z.enum(["SERVICE", "ENDPOINT", "METRIC"]),
url: z.string().url().optional(),
});
// Validate request body
const body = await request.json();
const result = createComponentSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.issues },
{ status: 400 }
);
}URL Validation
External URLs validated for SSRF protection.
SSRF Protection
Server-Side Request Forgery prevention:
import { validateExternalUrl } from "@/lib/security/ssrf";
// Blocked addresses
const BLOCKED = [
// Localhost
"127.0.0.0/8", "localhost", "::1",
// Private networks
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
// Link-local
"169.254.0.0/16", "fe80::/10",
// Cloud metadata
"169.254.169.254", "metadata.google.internal"
];
// Validation
const validation = await validateExternalUrl(url);
if (!validation.isValid) {
throw new Error(validation.error);
}Applied to:
- Webhook subscriber URLs
- Notification channel endpoints
- Integration URLs (Prometheus, Grafana)
- Uptime monitor targets
Rate Limiting
In-memory rate limiting with sliding window:
// middleware.ts
const RATE_LIMITS = {
api: { window: 60_000, max: 100 }, // 100/min
auth: { window: 60_000, max: 30 }, // 30/min
webhook: { window: 60_000, max: 200 }, // 200/min
public: { window: 60_000, max: 60 }, // 60/min
};Response headers:
X-RateLimit-Limit- Maximum requestsX-RateLimit-Remaining- Remaining requestsX-RateLimit-Reset- Window reset timestampRetry-After- Seconds until retry (on 429)
Data Protection
Multi-Tenancy Isolation
All queries filter by organization:
// Every query includes organizationId
const components = await prisma.component.findMany({
where: {
organizationId: session.user.organizationId,
},
});Sensitive Data Masking
API keys and secrets masked in responses:
// API key response
{
"id": "key-123",
"key": "sk_live_...abc", // Only last 3 chars shown
"permissions": ["components:read"]
}
// Integration config
{
"type": "DATADOG",
"config": {
"apiKey": "***REDACTED***",
"appKey": "***REDACTED***"
}
}Audit Logging
All sensitive operations logged:
await createAuditLog({
action: "COMPONENT_CREATE",
entityType: "COMPONENT",
entityId: component.id,
userId: session.user.id,
organizationId,
changes: { name: component.name },
});Logged actions:
- Authentication events
- Resource CRUD operations
- Permission changes
- Configuration updates
Request Tracing
Every request assigned a unique ID:
// middleware.ts
const requestId = request.headers.get('x-request-id')
|| crypto.randomUUID();
response.headers.set('x-request-id', requestId);Used for:
- Log correlation
- Debugging
- Support tickets
Security Headers
Recommended headers (configure in next.config.js):
const securityHeaders = [
{ key: 'X-DNS-Prefetch-Control', value: 'on' },
{ key: 'Strict-Transport-Security', value: 'max-age=63072000' },
{ key: 'X-Frame-Options', value: 'DENY' },
{ key: 'X-Content-Type-Options', value: 'nosniff' },
{ key: 'Referrer-Policy', value: 'origin-when-cross-origin' },
];Environment Variables
Required Secrets
# Authentication
NEXTAUTH_SECRET=<random-32-chars>
# Database
DATABASE_URL=postgresql://...
# Encryption (for 2FA secrets)
ENCRYPTION_KEY=<random-32-chars>Generation
Generate secure secrets:
# Using openssl
openssl rand -base64 32
# Using Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Security Policies
Organization Policies
Organizations can enforce:
- Require 2FA - All members must enable 2FA
- SSO Only - Disable password login
- Email Domains - Restrict member invites to specific domains
PATCH /api/v1/organization
{
"require2FA": true,
"ssoEnforced": true,
"allowedEmailDomains": ["company.com"]
}Dependency Security
Automated Updates
Dependabot configured for security updates:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10Vulnerability Scanning
Regular audits recommended:
npm audit
npm audit fixReporting Vulnerabilities
If you discover a security vulnerability:
- Do not open a public issue
- Email security concerns privately
- Include steps to reproduce
- Allow time for a fix before disclosure