Security Hardening
Overview
Section titled “Overview”Guía de prácticas de seguridad para infraestructura, aplicaciones y operaciones del ecosistema Nostromo.
Objetivo: Proteger datos sensibles de clientes (payroll, contratos, información personal) según mejores prácticas de seguridad.
Infraestructura
Section titled “Infraestructura”Firewall Configuration
Section titled “Firewall Configuration”UFW (Ubuntu Firewall):
# Estado actualsudo ufw status verbose
# Política por defecto: denegar todo entrantesudo ufw default deny incomingsudo ufw default allow outgoing
# Permitir SSH (solo desde IPs conocidas)sudo ufw allow from 203.0.113.0/24 to any port 22
# Permitir HTTP/HTTPSsudo ufw allow 80/tcpsudo ufw allow 443/tcp
# Denegar acceso directo a PostgreSQL (solo localhost)sudo ufw deny 5432/tcp
# Denegar acceso directo a Orchestrator (solo via Nginx)sudo ufw deny 8000/tcp
# Habilitar firewallsudo ufw enableSSH Hardening
Section titled “SSH Hardening”Config recomendada (/etc/ssh/sshd_config):
# Deshabilitar login como rootPermitRootLogin no
# Solo autenticación por clavePasswordAuthentication noPubkeyAuthentication yes
# Limitar usuarios permitidosAllowUsers deployer admin
# Cambiar puerto por defecto (opcional, security through obscurity)# Port 2222
# Limitar intentos de conexiónMaxAuthTries 3MaxSessions 2
# Timeout de sesiones inactivasClientAliveInterval 300ClientAliveCountMax 2Aplicar cambios:
sudo sshd -t # Validar sintaxissudo systemctl restart sshdSSL/TLS Certificates
Section titled “SSL/TLS Certificates”Certbot (Let’s Encrypt):
# Instalar certbotsudo apt install certbot python3-certbot-nginx
# Obtener certificadosudo certbot --nginx -d app.nostromo.cl -d api.nostromo.cl
# Verificar renovación automáticasudo certbot renew --dry-run
# Cron de renovación (automático con certbot)cat /etc/cron.d/certbotNginx SSL Config (/etc/nginx/conf.d/ssl.conf):
ssl_protocols TLSv1.2 TLSv1.3;ssl_prefer_server_ciphers on;ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;ssl_session_cache shared:SSL:10m;ssl_session_timeout 1d;ssl_stapling on;ssl_stapling_verify on;
# Headers de seguridadadd_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;add_header X-Frame-Options "SAMEORIGIN" always;add_header X-Content-Type-Options "nosniff" always;add_header X-XSS-Protection "1; mode=block" always;Aplicación
Section titled “Aplicación”Autenticación
Section titled “Autenticación”JWT Configuration:
// ✅ Buenas prácticasconst JWT_CONFIG = { secret: process.env.JWT_SECRET, // Min 32 caracteres, random expiresIn: "24h", // Expiración razonable algorithm: "HS256", // O RS256 para mayor seguridad};
// Cookie settings (HTTP-only para prevenir XSS)const COOKIE_CONFIG = { httpOnly: true, // No accesible via JavaScript secure: true, // Solo HTTPS sameSite: "strict", // Previene CSRF maxAge: 24 * 60 * 60 * 1000, // 24 horas};Referencia: ADR-005: Auth Strategy
Password Policy
Section titled “Password Policy”Validación de contraseñas:
const PASSWORD_REQUIREMENTS = { minLength: 12, requireUppercase: true, requireLowercase: true, requireNumbers: true, requireSpecialChars: true,};
// Hash con bcrypt (cost factor 12)import bcrypt from "bcrypt";const SALT_ROUNDS = 12;
async function hashPassword(password: string): Promise<string> { return bcrypt.hash(password, SALT_ROUNDS);}Input Validation
Section titled “Input Validation”Sanitización de inputs:
// ✅ Usar prepared statements (previene SQL injection)const result = await client.query( "SELECT * FROM employees WHERE rut = $1", [rut], // Parámetro sanitizado automáticamente);
// ❌ NUNCA concatenar directamente// const result = await client.query(`SELECT * FROM employees WHERE rut = '${rut}'`);
// Validación con Zodimport { z } from "zod";
const EmployeeSchema = z.object({ rut: z.string().regex(/^\d{1,2}\.\d{3}\.\d{3}-[\dkK]$/), email: z.string().email(), name: z.string().min(2).max(100),});
// Sanitizar HTML (si aplica)import DOMPurify from "dompurify";const cleanHtml = DOMPurify.sanitize(userInput);Rate Limiting
Section titled “Rate Limiting”Express middleware:
import rateLimit from "express-rate-limit";
// Limitar requests por IPconst limiter = rateLimit({ windowMs: 15 * 60 * 1000, // 15 minutos max: 100, // 100 requests por ventana message: "Too many requests, please try again later.", standardHeaders: true, legacyHeaders: false,});
app.use("/api/", limiter);
// Rate limit más estricto para loginconst loginLimiter = rateLimit({ windowMs: 60 * 60 * 1000, // 1 hora max: 5, // 5 intentos fallidos message: "Too many login attempts.",});
app.use("/api/command/auth/login", loginLimiter);CORS Configuration
Section titled “CORS Configuration”import cors from "cors";
const corsOptions = { origin: [ "https://app.nostromo.cl", "https://sevastopol.nostromo.cl", // NO incluir localhost en producción ], credentials: true, methods: ["GET", "POST", "PUT", "DELETE", "PATCH"], allowedHeaders: ["Content-Type", "Authorization", "X-Tenant-ID"], maxAge: 86400, // Cache preflight por 24h};
app.use(cors(corsOptions));Base de Datos
Section titled “Base de Datos”PostgreSQL Security
Section titled “PostgreSQL Security”pg_hba.conf:
# Solo conexiones localeslocal all postgres peerhost nostromo nostromo_user 127.0.0.1/32 scram-sha-256host nostromo nostromo_user ::1/128 scram-sha-256
# Rechazar todo lo demáshost all all 0.0.0.0/0 rejectRoles con mínimo privilegio:
-- Rol de aplicación (solo lo necesario)CREATE ROLE nostromo_app WITH LOGIN PASSWORD '<STRONG>';GRANT CONNECT ON DATABASE nostromo TO nostromo_app;GRANT USAGE ON SCHEMA central, parametros TO nostromo_app;GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA central TO nostromo_app;
-- NO dar SUPERUSER ni CREATEDB-- REVOKE ALL ON DATABASE postgres FROM nostromo_app;Encryption at Rest
Section titled “Encryption at Rest”Datos sensibles en columnas:
-- Usar pgcrypto para datos sensiblesCREATE EXTENSION IF NOT EXISTS pgcrypto;
-- Encriptar RUT (ejemplo)UPDATE employeesSET rut_encrypted = pgp_sym_encrypt(rut, 'encryption_key')WHERE rut_encrypted IS NULL;Secrets Management
Section titled “Secrets Management”Variables de Entorno
Section titled “Variables de Entorno”Archivo .env.production (NO commitear a Git):
# DatabaseDB_HOST=localhostDB_PORT=5432DB_USER=nostromo_userDB_PASSWORD=<STRONG_RANDOM_PASSWORD>
# JWTJWT_SECRET=<RANDOM_STRING_MIN_32_CHARS>
# EncryptionENCRYPTION_KEY=<RANDOM_STRING_32_CHARS>
# External APIsSII_API_KEY=<API_KEY>Permisos de archivo:
chmod 600 .env.productionchown deployer:deployer .env.productionRotación de Secrets
Section titled “Rotación de Secrets”Procedimiento de rotación:
- Generar nuevo secret
- Actualizar en producción
- Reiniciar servicios
- Revocar secret anterior
- Documentar en audit log
# Generar secret randomopenssl rand -base64 32
# Actualizar y reiniciarnano .env.productionpm2 restart orchestratorLogging & Audit
Section titled “Logging & Audit”Audit Log
Section titled “Audit Log”Eventos a registrar:
| Evento | Criticidad | Retención |
|---|---|---|
| Login success | Info | 30 días |
| Login failure | Warning | 90 días |
| Password change | High | 1 año |
| Admin actions | High | 1 año |
| Data export | High | 1 año |
| Tenant switch | Info | 30 días |
Estructura de log:
interface AuditLogEntry { timestamp: Date; userId: string; tenantId: string; action: string; resource: string; ipAddress: string; userAgent: string; status: "success" | "failure"; details?: Record<string, unknown>;}Log Security
Section titled “Log Security”No loguear datos sensibles:
// ❌ MAL - Expone passwordlogger.info(`Login attempt for ${email} with password ${password}`);
// ✅ BIEN - Solo info necesarialogger.info(`Login attempt for ${email}`, { ip: req.ip });Rotation de logs:
/var/log/nostromo/*.log { daily rotate 30 compress delaycompress notifempty create 640 deployer deployer}Checklist de Seguridad
Section titled “Checklist de Seguridad”Pre-Deploy
Section titled “Pre-Deploy”- Variables de entorno configuradas
- JWT_SECRET es random y >= 32 chars
- Contraseñas de DB son fuertes
- SSL/TLS certificados válidos
- Firewall configurado
- Backups funcionando
Semanal
Section titled “Semanal”- Revisar logs de autenticación fallida
- Verificar certificados SSL no expiran pronto
- Verificar backups recientes existen
- Revisar usuarios activos y permisos
Mensual
Section titled “Mensual”- Actualizar dependencias (
npm audit) - Revisar CVEs de PostgreSQL/Node.js
- Test de restore de backup
- Revisar audit logs de acciones admin
Incident Response
Section titled “Incident Response”Si Credenciales Comprometidas
Section titled “Si Credenciales Comprometidas”- Rotar inmediatamente el secret afectado
- Revocar sesiones activas (invalidar JWTs)
- Notificar usuarios afectados
- Investigar logs para alcance del breach
- Documentar en post-mortem
Si SQL Injection Detectado
Section titled “Si SQL Injection Detectado”- Bloquear IP del atacante
- Revisar logs de queries ejecutadas
- Verificar integridad de datos
- Parchar código vulnerable
- Escanear otras posibles vulnerabilidades
Related Documentation
Section titled “Related Documentation”Changelog
Section titled “Changelog”| Fecha | Version | Cambios |
|---|---|---|
| 2026-01-18 | 1.0 | Guía inicial creada |