Seguridad
ReliaPulse implementa múltiples capas de seguridad para proteger tus datos y asegurar una operación segura.
Autenticación
Seguridad de Contraseñas
Las contraseñas se hashean usando bcrypt con factor de costo 10:
import bcrypt from "bcryptjs";
// Hasheo
const hash = await bcrypt.hash(password, 10);
// Verificación
const isValid = await bcrypt.compare(password, hash);Requisitos de contraseña:
- Mínimo 8 caracteres
- Validada tanto en cliente como en servidor
Autenticación de Dos Factores (2FA)
2FA basado en TOTP siguiendo RFC 6238:
-
Flujo de Configuración:
- Generar secreto con
otplib - Crear código QR para app autenticadora
- Usuario verifica con código de 6 dígitos
- Se generan 10 códigos de respaldo
- Generar secreto con
-
Flujo de Login:
- Usuario ingresa email/contraseña
- Frontend llama a
/api/auth/check-2fapara validar credenciales - Si son válidas y 2FA está habilitado, muestra input de código
- Usuario ingresa código de 6 dígitos (o código de respaldo)
- Frontend llama a NextAuth
signIn()con credenciales + código 2FA - Servidor verifica TOTP y completa el login
-
Almacenamiento del Secreto:
- Encriptado con AES-256-GCM
- Clave de encriptación desde variable
ENCRYPTION_KEY
// Encriptación
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
const encrypted = cipher.update(secret, 'utf8', 'hex');
// Desencriptación
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
const decrypted = decipher.update(encrypted, 'hex', 'utf8');Autenticación con API Key
API keys para acceso programático:
- Almacenamiento: Hash SHA-256 (no reversible)
- Formato:
sk_live_<random_base64url>(43 chars) - Permisos: Granulares, por recurso
// Generación de key
const rawKey = `sk_live_${crypto.randomBytes(32).toString('base64url')}`;
const hash = crypto.createHash('sha256').update(rawKey).digest('hex');
// Validación
const inputHash = crypto.createHash('sha256').update(inputKey).digest('hex');
const isValid = inputHash === storedHash;SSO/SAML
SSO empresarial via SAML Jackson:
- Compatible con SAML 2.0
- Importación de metadata del IdP
- Exportación de metadata del SP
- Detección SSO basada en dominio
Autorización
Control de Acceso Basado en Roles (RBAC)
8 roles predefinidos con permisos granulares:
| Rol | Descripción |
|---|---|
| OWNER | Acceso completo, puede eliminar organización |
| ADMIN | Gestionar todo excepto facturación |
| MEMBER | Crear/editar componentes, incidentes |
| VIEWER | Acceso solo lectura |
| INCIDENT_MANAGER | Gestionar incidentes y postmortems |
| ONCALL_RESPONDER | Gestionar on-call y alertas |
| STATUS_EDITOR | Editar páginas de estado y branding |
| SUBSCRIBER_MANAGER | Gestionar suscriptores y canales |
Roles Personalizados
Las organizaciones pueden crear roles personalizados:
// Crear rol personalizado con permisos específicos
POST /api/v1/organization/roles
{
"name": "Developer",
"permissions": [
"COMPONENTS_VIEW",
"COMPONENTS_CREATE",
"INCIDENTS_VIEW",
"INCIDENTS_CREATE"
]
}Verificación de Permisos
Cada ruta API valida permisos:
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;
// Usuario tiene permiso, continuar
}Validación de Entrada
Validación de Requests
Todas las entradas validadas con 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(),
});
// Validar body del request
const body = await request.json();
const result = createComponentSchema.safeParse(body);
if (!result.success) {
return NextResponse.json(
{ error: result.error.issues },
{ status: 400 }
);
}Validación de URLs
URLs externas validadas para protección SSRF.
Protección SSRF
Prevención de Server-Side Request Forgery:
import { validateExternalUrl } from "@/lib/security/ssrf";
// Direcciones bloqueadas
const BLOCKED = [
// Localhost
"127.0.0.0/8", "localhost", "::1",
// Redes privadas
"10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
// Link-local
"169.254.0.0/16", "fe80::/10",
// Metadata cloud
"169.254.169.254", "metadata.google.internal"
];
// Validación
const validation = await validateExternalUrl(url);
if (!validation.isValid) {
throw new Error(validation.error);
}Aplicado a:
- URLs de suscriptores webhook
- Endpoints de canales de notificación
- URLs de integración (Prometheus, Grafana)
- Targets de monitores uptime
Rate Limiting
Rate limiting en memoria con ventana deslizante:
// 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
};Headers de respuesta:
X-RateLimit-Limit- Requests máximosX-RateLimit-Remaining- Requests restantesX-RateLimit-Reset- Timestamp de reset de ventanaRetry-After- Segundos hasta reintento (en 429)
Protección de Datos
Aislamiento Multi-Tenant
Todas las queries filtran por organización:
// Cada query incluye organizationId
const components = await prisma.component.findMany({
where: {
organizationId: session.user.organizationId,
},
});Enmascaramiento de Datos Sensibles
API keys y secretos enmascarados en respuestas:
// Respuesta de API key
{
"id": "key-123",
"key": "sk_live_...abc", // Solo últimos 3 chars mostrados
"permissions": ["components:read"]
}
// Config de integración
{
"type": "DATADOG",
"config": {
"apiKey": "***REDACTED***",
"appKey": "***REDACTED***"
}
}Logging de Auditoría
Todas las operaciones sensibles registradas:
await createAuditLog({
action: "COMPONENT_CREATE",
entityType: "COMPONENT",
entityId: component.id,
userId: session.user.id,
organizationId,
changes: { name: component.name },
});Acciones registradas:
- Eventos de autenticación
- Operaciones CRUD de recursos
- Cambios de permisos
- Actualizaciones de configuración
Trazabilidad de Requests
Cada request asignado con ID único:
// middleware.ts
const requestId = request.headers.get('x-request-id')
|| crypto.randomUUID();
response.headers.set('x-request-id', requestId);Usado para:
- Correlación de logs
- Debugging
- Tickets de soporte
Headers de Seguridad
Headers recomendados (configurar en 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' },
];Variables de Entorno
Secretos Requeridos
# Autenticación
NEXTAUTH_SECRET=<random-32-chars>
# Base de datos
DATABASE_URL=postgresql://...
# Encriptación (para secretos 2FA)
ENCRYPTION_KEY=<random-32-chars>Generación
Generar secretos seguros:
# Usando openssl
openssl rand -base64 32
# Usando Node.js
node -e "console.log(require('crypto').randomBytes(32).toString('base64'))"Políticas de Seguridad
Políticas de Organización
Las organizaciones pueden aplicar:
- Requerir 2FA - Todos los miembros deben habilitar 2FA
- Solo SSO - Deshabilitar login con contraseña
- Dominios de Email - Restringir invitaciones a dominios específicos
PATCH /api/v1/organization
{
"require2FA": true,
"ssoEnforced": true,
"allowedEmailDomains": ["company.com"]
}Seguridad de Dependencias
Actualizaciones Automáticas
Dependabot configurado para actualizaciones de seguridad:
# .github/dependabot.yml
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
open-pull-requests-limit: 10Escaneo de Vulnerabilidades
Auditorías regulares recomendadas:
npm audit
npm audit fixReportar Vulnerabilidades
Si descubres una vulnerabilidad de seguridad:
- No abras un issue público
- Envía email con preocupaciones de seguridad privadamente
- Incluye pasos para reproducir
- Permite tiempo para una corrección antes de divulgación