English
Architecture
Security

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:

    1. Generate secret with otplib
    2. Create QR code for authenticator app
    3. User verifies with 6-digit code
    4. 10 backup codes generated
  • Login Flow:

    1. User enters email/password
    2. Frontend calls /api/auth/check-2fa to validate credentials
    3. If valid and 2FA enabled, shows 2FA code input
    4. User enters 6-digit code (or backup code)
    5. Frontend calls NextAuth signIn() with credentials + 2FA code
    6. Server verifies TOTP and completes login
  • Secret Storage:

    • Encrypted with AES-256-GCM
    • Encryption key from ENCRYPTION_KEY env 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:

RoleDescription
OWNERFull access, can delete organization
ADMINManage everything except billing
MEMBERCreate/edit components, incidents
VIEWERRead-only access
INCIDENT_MANAGERManage incidents and postmortems
ONCALL_RESPONDERManage on-call and alerts
STATUS_EDITOREdit status pages and branding
SUBSCRIBER_MANAGERManage 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 requests
  • X-RateLimit-Remaining - Remaining requests
  • X-RateLimit-Reset - Window reset timestamp
  • Retry-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: 10

Vulnerability Scanning

Regular audits recommended:

npm audit
npm audit fix

Reporting Vulnerabilities

If you discover a security vulnerability:

  1. Do not open a public issue
  2. Email security concerns privately
  3. Include steps to reproduce
  4. Allow time for a fix before disclosure