Seguridad por capa

Cómo aplicar seguridad en cada capa de la arquitectura: frontend, BFF, microservicios y base de datos, con estrategias específicas para cada nivel.

El principio de defensa en profundidad

La seguridad en una arquitectura distribuida no puede depender de un solo punto de control. Si un atacante supera una barrera, debe encontrar otra. Este principio se llama defensa en profundidad: cada capa implementa sus propias medidas de seguridad, independientemente de lo que hagan las demás.

[Frontend] → [API Gateway] → [BFF] → [Microservicios] → [Base de datos]
  Capa 1        Capa 2       Capa 3      Capa 4            Capa 5

Cada capa tiene amenazas específicas y, por lo tanto, necesita protecciones específicas.

Seguridad en el Frontend

El frontend es la superficie de ataque más expuesta: cualquier usuario puede inspeccionar el código, manipular requests y modificar el DOM.

Qué proteger

  • Inputs del usuario: todo dato que ingresa el usuario es potencialmente malicioso
  • Tokens de sesión: si un atacante obtiene el token, puede suplantar al usuario
  • Datos sensibles: nunca almacenar secretos, claves API o datos confidenciales en el frontend

Estrategias clave

Sanitización de inputs

Nunca confíes en los datos del usuario. Valida y sanitiza en el frontend como primera línea de defensa, pero siempre valida también en el backend.

// ❌ Peligroso: insertar HTML del usuario directamente
element.innerHTML = userInput;

// ✅ Seguro: usar textContent o sanitizar
element.textContent = userInput;
// O usar una librería de sanitización como DOMPurify
element.innerHTML = DOMPurify.sanitize(userInput);

Almacenamiento seguro de tokens

// ❌ Vulnerable a XSS: localStorage es accesible desde cualquier script
localStorage.setItem('token', jwt);

// ✅ Mejor: cookies httpOnly (configuradas desde el servidor)
// El frontend no puede acceder a la cookie, pero se envía automáticamente
Set-Cookie: token=jwt; HttpOnly; Secure; SameSite=Strict

Content Security Policy (CSP)

Configura headers CSP para limitar qué recursos puede cargar la página:

Content-Security-Policy: 
  default-src 'self';
  script-src 'self';
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.ejemplo.com;

Seguridad en el API Gateway

El API Gateway es el punto de entrada único al sistema. Es el lugar ideal para implementar controles de seguridad transversales.

Responsabilidades de seguridad

  1. Terminación TLS: todas las conexiones externas deben ser HTTPS
  2. Rate limiting: limitar el número de requests por IP o por usuario
  3. Validación de tokens: verificar que el JWT es válido antes de enrutar
  4. Filtrado de headers: eliminar headers internos que no deben llegar al exterior
  5. Logging de acceso: registrar cada request para auditoría

Rate limiting

# Ejemplo de configuración de rate limiting
rate_limit:
  global:
    requests_per_second: 100
  per_ip:
    requests_per_minute: 60
  per_user:
    requests_per_minute: 120
  endpoints:
    /auth/login:
      requests_per_minute: 5  # Más restrictivo para login

Validación de tokens en el Gateway

Request entrante
  → ¿Tiene header Authorization?
    → No → ¿Es ruta pública? → Sí → Permitir
                               → No → 401 Unauthorized
    → Sí → ¿Token válido (firma, expiración)?
      → No → 401 Unauthorized
      → Sí → Extraer claims, adjuntar al request, enrutar

Seguridad en el BFF

El BFF (Backend for Frontend) actúa como intermediario entre el frontend y los microservicios. Tiene responsabilidades de seguridad específicas.

Responsabilidades

  1. Transformación segura de datos: nunca exponer datos internos al frontend
  2. Validación de inputs: segunda línea de validación después del frontend
  3. Orquestación segura: al combinar datos de múltiples servicios, respetar los permisos del usuario
  4. Manejo de errores: no filtrar información interna en los mensajes de error

Ejemplo: filtrado de datos sensibles

// Respuesta del microservicio (datos internos)
const userFromService = {
  id: "user-123",
  email: "maria@ejemplo.com",
  passwordHash: "$2b$10$...",  // ¡Nunca exponer!
  internalId: "db-row-456",    // ¡Dato interno!
  role: "admin",
  lastLogin: "2024-01-15T10:30:00Z"
};

// Respuesta del BFF al frontend (datos filtrados)
const userForFrontend = {
  id: userFromService.id,
  email: userFromService.email,
  role: userFromService.role,
  lastLogin: userFromService.lastLogin
};

Manejo seguro de errores

// ❌ Expone información interna
res.status(500).json({
  error: "Connection refused to postgres://db-host:5432/users"
});

// ✅ Error genérico para el frontend
res.status(500).json({
  error: "Error interno del servidor. Intente nuevamente."
});
// El detalle se registra en los logs internos

Seguridad en los Microservicios

Cada microservicio debe asumir que los requests que recibe pueden ser maliciosos, incluso si vienen de otros servicios internos.

Zero Trust entre servicios

El modelo Zero Trust establece que ningún servicio confía automáticamente en otro, incluso dentro de la misma red:

  • Cada servicio verifica la identidad del llamante
  • Cada servicio valida los permisos para la operación solicitada
  • La comunicación entre servicios está cifrada (mTLS)

Validación de inputs en cada servicio

// Cada microservicio valida sus propios inputs
function createOrder(data) {
  // Validar estructura
  if (!data.userId || !data.items || data.items.length === 0) {
    throw new ValidationError("Datos de orden inválidos");
  }
  
  // Validar tipos y rangos
  if (typeof data.userId !== 'string' || data.userId.length > 50) {
    throw new ValidationError("userId inválido");
  }
  
  for (const item of data.items) {
    if (item.quantity < 1 || item.quantity > 1000) {
      throw new ValidationError("Cantidad fuera de rango");
    }
  }
  
  // Procesar solo después de validar
  return orderRepository.create(data);
}

Principio de mínimo privilegio

Cada microservicio solo debe tener acceso a los recursos que necesita:

  • El servicio de órdenes no necesita acceso a la tabla de usuarios
  • El servicio de notificaciones no necesita poder modificar órdenes
  • Las credenciales de base de datos son únicas por servicio

Seguridad en la Base de Datos

La base de datos es la última línea de defensa. Si un atacante llega aquí, los datos deben estar protegidos.

Estrategias clave

Cifrado en reposo

Los datos sensibles deben estar cifrados en disco:

-- Ejemplo: columnas cifradas
CREATE TABLE users (
  id UUID PRIMARY KEY,
  email VARCHAR(255) NOT NULL,
  password_hash VARCHAR(255) NOT NULL,  -- Hash, no cifrado reversible
  ssn_encrypted BYTEA,                  -- Dato sensible cifrado con AES-256
  created_at TIMESTAMP DEFAULT NOW()
);

Acceso con mínimo privilegio

-- Cada servicio tiene su propio usuario de BD con permisos limitados
-- Servicio de órdenes: solo acceso a tablas de órdenes
GRANT SELECT, INSERT, UPDATE ON orders TO orders_service;
GRANT SELECT ON products TO orders_service;
-- No tiene acceso a la tabla de usuarios

-- Servicio de usuarios: solo acceso a tablas de usuarios
GRANT SELECT, INSERT, UPDATE ON users TO users_service;
-- No tiene acceso a la tabla de órdenes

Prevención de inyección SQL

// ❌ Vulnerable a inyección SQL
const query = `SELECT * FROM users WHERE email = '${email}'`;

// ✅ Consulta parametrizada
const query = 'SELECT * FROM users WHERE email = $1';
const result = await db.query(query, [email]);

Auditoría de acceso

Registra quién accede a qué datos y cuándo:

CREATE TABLE audit_log (
  id SERIAL PRIMARY KEY,
  table_name VARCHAR(100),
  operation VARCHAR(10),  -- SELECT, INSERT, UPDATE, DELETE
  user_id VARCHAR(100),
  service_name VARCHAR(100),
  timestamp TIMESTAMP DEFAULT NOW(),
  details JSONB
);

Resumen

La seguridad por capas garantiza que no existe un único punto de fallo. Cada capa — frontend, API Gateway, BFF, microservicios y base de datos — implementa sus propias defensas. El frontend sanitiza inputs y protege tokens, el Gateway controla el acceso y limita el tráfico, el BFF filtra datos sensibles, los microservicios validan todo y aplican mínimo privilegio, y la base de datos cifra y audita. Juntas, estas capas crean un sistema donde comprometer una no significa comprometer todas.