Plataforma técnica · Sevastopol
Middleware de Sevastopol
Sevastopol Middleware Seguridad
Propósito y Alcance
Section titled “Propósito y Alcance”src/middleware.ts se ejecuta en cada request al servidor SSR de Astro antes de renderizar la página o invocar una ruta API. Tiene tres responsabilidades:
- Autorización por sesión — bloquea acceso a páginas HTML protegidas si no hay cookie
sidválida. - Validación contra Orchestrator — para páginas HTML sensibles confirma con
/api/auth/validateque elsidno esté revocado. - Security Headers + CSP — agrega cabeceras de seguridad a toda respuesta del servidor, incluidas las del BFF proxy.
El cookie sid se emite por /api/auth/login y vive en el navegador con flags HttpOnly y Secure (gestionado por Orchestrator). El middleware nunca lee su contenido, sólo verifica existencia o delega validación al backend.
Flujo de decisión
Section titled “Flujo de decisión”flowchart TB
REQ["Request entrante"]
PUB{"¿Ruta pública?<br/>/ · /public/* · /api/auth/*"}
TOK["Lee cookie sid"]
HTML{"¿Es página HTML<br/>protegida?<br/>/dashboard · /settings · /registry"}
HASCK{"¿hay sid?"}
E2E{"sid == e2e-bypass-token?"}
VAL["fetch /api/auth/validate<br/>al Orchestrator"]
OK{"¿200 OK?"}
API{"¿Es /api/* sin sid?"}
NEXT["next() → ruta destino"]
REDIR["redirect('/', 302)"]
ERR401["Response 401 JSON"]
HDRS["withSecurityHeaders()<br/>aplica CSP + headers"]
RESP["Response al cliente"]
REQ --> PUB
PUB -->|sí| NEXT
PUB -->|no| TOK --> HTML
HTML -->|sí| HASCK
HASCK -->|no| REDIR
HASCK -->|sí| E2E
E2E -->|sí| NEXT
E2E -->|no| VAL --> OK
OK -->|sí| NEXT
OK -->|no o catch| REDIR
HTML -->|no| API
API -->|sí| ERR401
API -->|no| NEXT
NEXT --> HDRS
REDIR --> HDRS
ERR401 --> HDRS
HDRS --> RESP withSecurityHeaders() corre siempre en el camino de salida — ninguna respuesta del servidor sale sin CSP.
Rutas públicas
Section titled “Rutas públicas”Tres prefijos pasan directo sin requerir sesión:
| Patrón | Razón |
|---|---|
/ | Landing + página de login (formulario inicial) |
/public/* | Recursos accesibles sin sesión (assets, páginas marketing) |
/api/auth/* | Login, logout, validate, 2FA — el flujo de autenticación |
Cualquier otra ruta pasa por el resto del middleware.
Protección de páginas HTML
Section titled “Protección de páginas HTML”Tres prefijos representan el shell autenticado de Sevastopol:
if ( url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/settings') || url.pathname.startsWith('/registry')) { // ...}Para estas rutas el middleware exige doble verificación:
1. Presencia de cookie
Section titled “1. Presencia de cookie”const token = cookies.get('sid')?.value;if (!token) { return withSecurityHeaders(ctx.redirect('/', 302));}Sin sid el navegador vuelve al login (/).
2. Validación contra Orchestrator
Section titled “2. Validación contra Orchestrator”Con cookie, el middleware verifica que la sesión esté vigente:
const validateRes = await fetch(`${ORCHESTRATOR_BASE_URL}/api/auth/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cookie': `sid=${token}`, }, credentials: 'include',});
if (!validateRes.ok) { return withSecurityHeaders(ctx.redirect('/', 302));}El catch también redirige — si Orchestrator está caído, no se sirve la página. Esto previene servir el shell con datos desactualizados o cookie comprometida.
3. Bypass E2E
Section titled “3. Bypass E2E”Para tests de Playwright existe un atajo:
if (token === 'e2e-bypass-token') { return withSecurityHeaders(await next());}Con ese valor literal el middleware salta la validación con Orchestrator. Sólo debe usarse en entornos de test — el valor está hardcodeado en el código y no es un secreto.
Protección del BFF proxy
Section titled “Protección del BFF proxy”Para rutas bajo /api/* (las del BFF proxy), la verificación es más liviana:
if (url.pathname.startsWith('/api/') && !token) { return withSecurityHeaders(jsonErr(401, 'No session found'));}Sólo se verifica existencia de cookie. La validación real (firma, expiración, scopes) la hace Orchestrator cuando el proxy le reenvía la request. Esto evita una llamada extra a /api/auth/validate por cada petición de datos.
Por qué dos niveles de protección
Section titled “Por qué dos niveles de protección”| Tipo de ruta | Verificación middleware | Por qué |
|---|---|---|
| Página HTML protegida | Existencia + validación | Renderizar el shell tiene costo; mejor abortar temprano |
/api/* (BFF proxy) | Sólo existencia de cookie | Orchestrator validará al recibir el reenvío |
Una llamada API con sid revocada llega a Orchestrator, recibe 401, y el frontend dispara redirección al login vía handleAuthResponse.
Security Headers
Section titled “Security Headers”Toda respuesta (HTML, JSON, redirect, error) pasa por withSecurityHeaders() antes de salir.
Headers aplicados
Section titled “Headers aplicados”| Header | Valor | Propósito |
|---|---|---|
Content-Security-Policy | Ver detalle abajo | Restringe orígenes de scripts, estilos, conexiones |
X-Frame-Options | DENY | Prohíbe embeber Sevastopol en <iframe> externo |
X-Content-Type-Options | nosniff | Bloquea MIME sniffing en navegadores |
X-XSS-Protection | 1; mode=block | Activa filtro XSS legacy (defensa en profundidad) |
Referrer-Policy | strict-origin-when-cross-origin | Limita información del referer cross-origin |
Permissions-Policy | camera=(), microphone=(), geolocation=() | Deshabilita APIs sensibles del navegador |
Detalle de CSP
Section titled “Detalle de CSP”default-src 'self';script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline' https://fonts.googleapis.com;font-src 'self' https://fonts.gstatic.com;img-src 'self' data: https:;connect-src 'self' <ORCHESTRATOR_ORIGIN> ws://localhost:*;frame-src 'self' http://localhost:4322;frame-ancestors 'none';base-uri 'self';form-action 'self';Decisiones por directiva:
script-src 'unsafe-inline' 'unsafe-eval'— requerido por SolidJS en modo dev (HMR + hidratación). En producción ideal eliminarlo, pero las islands hidratadas usan eval para reactividad.connect-src— incluye dinámicamenteORCHESTRATOR_ORIGIN(calculado desdeORCHESTRATOR_BASE_URL) yws://localhost:*para HMR de Vite.frame-src 'self' http://localhost:4322— permite embeber jean_d_arc en dev (página de docs interna). Quitar en producción si no se usa.frame-ancestors 'none'— refuerzaX-Frame-Options: DENYcon el mecanismo moderno.
Construcción del header
Section titled “Construcción del header”ORCHESTRATOR_ORIGIN se computa al cargar el módulo, no por request:
import { ORCHESTRATOR_BASE_URL } from '@/lib/orchestratorUrl';const ORCHESTRATOR_ORIGIN = new URL(ORCHESTRATOR_BASE_URL).origin;Cambiar la URL del Orchestrator requiere reiniciar el servidor para que el CSP refleje el nuevo origen.
Implementación completa
Section titled “Implementación completa”export const onRequest = defineMiddleware(async (ctx, next) => { const { url, cookies } = ctx;
// 1. Rutas públicas if ( url.pathname === '/' || url.pathname.startsWith('/public') || url.pathname.startsWith('/api/auth/') ) return withSecurityHeaders(await next());
// 2. Token desde cookie const token = cookies.get('sid')?.value;
// 3. Páginas HTML protegidas if ( url.pathname.startsWith('/dashboard') || url.pathname.startsWith('/settings') || url.pathname.startsWith('/registry') ) { if (!token) return withSecurityHeaders(ctx.redirect('/', 302)); if (token === 'e2e-bypass-token') return withSecurityHeaders(await next());
try { const validateRes = await fetch(`${ORCHESTRATOR_BASE_URL}/api/auth/validate`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Cookie': `sid=${token}` }, credentials: 'include', }); if (!validateRes.ok) return withSecurityHeaders(ctx.redirect('/', 302)); } catch { return withSecurityHeaders(ctx.redirect('/', 302)); } return withSecurityHeaders(await next()); }
// 4. BFF proxy: sólo presencia de cookie if (url.pathname.startsWith('/api/') && !token) { return withSecurityHeaders(jsonErr(401, 'No session found')); }
const response = await next(); return withSecurityHeaders(response);});Cómo agregar una ruta protegida nueva
Section titled “Cómo agregar una ruta protegida nueva”- Decidir si es HTML (necesita renderizar shell autenticado) o API (proxy a Orchestrator).
- Para HTML, agregar el prefijo a la condición del bloque
/dashboard | /settings | /registry. - Para API, basta con que viva bajo
/api/*— la protección por cookie ya aplica. - Si la ruta carga recursos externos (fuentes, scripts CDN, imágenes), agregar el origen al
connect-src,script-src,font-srcoimg-srccorrespondiente del CSP. - Si rompe algo, abrir DevTools → Console: las violaciones de CSP se loggean con detalle del directiva violada.