# Seguridad que puedes verificar en el repositorio

> La home ahora lista ocho protecciones concretas. Aquí está dónde vive cada una en el código, qué hace de verdad y qué sigue siendo tu responsabilidad después de comprar la plantilla.

La mayoría de las landing pages de SaaS tienen una sección de seguridad. Mencionan cifrado, logos de compliance y algo sobre infraestructura enterprise. No puedes saber si algo de eso es cierto sin firmar un NDA.

La home de CastorStack lista ocho protecciones específicas. Cada una apunta a código que puedes abrir, tests que rompen el build si alguien lo quita, o un script que corre antes de que el ZIP del comprador salga del repositorio. Este post recorre las ocho.

La autenticación y el RLS en las tablas de Supabase tienen su propio artículo ([El costo del primer login](/es/blog/el-costo-del-primer-login)). Lo que sigue es todo lo demás de la lista.

## Webhooks de pago firmados

Stripe, Paddle y Polar entregan eventos de suscripción por HTTP POST. Cualquiera en internet puede golpear esa URL. Lo único entre tu base de datos y un evento falsificado de "pago exitoso" es la verificación de firma.

En CastorStack cada proveedor tiene su verificador: Stripe usa el helper estándar de firma de webhook con HMAC-SHA256 y comparación en tiempo constante; Paddle interpreta el header `Paddle-Signature` igual; Polar revisa `webhook-signature` y `webhook-timestamp`. Los tres rechazan eventos de más de cinco minutos, lo que cierra la ventana de replay. Firmas que no coinciden fallan con `FixedTimeEquals`, no con comparación de strings que filtra información de timing.

Los eventos se guardan con clave de idempotencia antes de que corra cualquier lógica de negocio. Un reintento del proveedor con el mismo evento no crea una segunda fila de suscripción.

Las implementaciones están en `PaddlePaymentProvider.cs`, `StandardWebhookSignature.cs` y el provider de Polar. Los tests en `PaddlePaymentProviderTests.cs` y `StripeWebhookEndpointsTests.cs` cubren los casos de fallo.

## Rutas de admin bloqueadas de punta a punta

El admin panel es una app Vite separada con su propio deploy. Esa separación ayuda, pero no es el límite de seguridad. El límite es la API.

Cada endpoint bajo `/api/v1/admin/*` exige un usuario autenticado con rol de admin. No hay atajo de "lectura pública en admin" ni endpoint que revisa el rol en un sitio y lo olvida en otro. El atributo de autorización se aplica ruta por ruta, y los tests comprueban que llamadas sin autenticar o sin rol de admin reciben 401 o 403.

Si añades un endpoint de admin y te saltas el atributo, lo notarás rápido porque el patrón es consistente en los controllers existentes.

## Credenciales de PSP cifradas

Los secretos del proveedor de pago (clave secreta de Stripe, API key de Paddle, secrets de firma de webhook) viven en la base de datos porque el admin panel tiene que configurarlos en runtime. Guardarlos en texto plano significaría que cualquiera con lectura en Postgres podría cobrar tarjetas a tu nombre.

CastorStack los cifra con ASP.NET Data Protection antes de persistir. Las claves de cifrado están en la tabla `DataProtectionKeys`, y esa tabla tiene RLS activado como toda tabla gestionada por el servidor. Las respuestas de la API nunca devuelven el secreto descifrado; la UI del admin muestra un placeholder enmascarado después del primer guardado.

Rotar claves o cambiar de entorno exige entender el key ring de Data Protection, documentado en el setup de infraestructura. Son más piezas que variables de entorno, pero las variables de entorno no sobreviven al requisito de "configurar esto en el admin panel" como producto.

## Sin SQL crudo en la API

Entity Framework Core es el único camino de acceso a la base en el proyecto de la API. No hay `FromSqlRaw` con strings interpolados, ni consultas Dapper, ni SQL armado a mano en controllers.

Es una restricción deliberada. Consultas parametrizadas vía EF eliminan la superficie de inyección más común en una API de negocio. Cuando necesites SQL crudo, pertenece a un archivo de migración: revisado, versionado y nunca construido desde input del usuario.

## RLS impuesto por migración

Este casi se nos escapó, y merece más que un bullet.

Supabase expone cada tabla en `public` vía PostgREST. La clave anónima va en el bundle del frontend. Cualquiera puede leerla. Row Level Security es la protección real, y Postgres no lo activa automáticamente en las tablas que crean tus migraciones.

CastorStack tiene la migración `HardenRowLevelSecurity`, que activa RLS en toda tabla gestionada por el servidor, revoca grants de `anon` y `authenticated`, y cambia los privilegios por defecto del schema para que el próximo `CreateTable` nazca cerrado. Un test de regresión (`RowLevelSecurityCoverageTests`) parsea el source de cada migración y rompe el build si aparece una tabla nueva sin su `ENABLE ROW LEVEL SECURITY` correspondiente.

Ese test es la parte que no habría escrito hasta que alguien apuntara un cliente REST a producción.

## Escaneo del ZIP del comprador

Cuando empaquetas CastorStack para un comprador, `scripts/package-release.mjs` arma un ZIP y lo escanea antes de liberarlo. El escaneo busca claves live de Stripe, secretos con forma de JWT, contraseñas hardcodeadas y correos personales que no deberían viajar con la plantilla.

Si algo coincide, el script sale con error y el ZIP no sale. Es la última línea de defensa contra "commiteé un secreto hace tres meses y lo olvidé".

## CORS estricto por defecto

La política de CORS de la API lista orígenes permitidos de forma explícita en la configuración. Las credentials están desactivadas. Esa combinación impide que un navegador en un dominio aleatorio haga peticiones cross-origin autenticadas a tu API, lo que elimina una clase de ataques CSRF contra sesiones basadas en cookies.

Configuras los orígenes por entorno (`Cors:AllowedOrigins` en appsettings o la variable de entorno equivalente). Añadir un dominio de frontend nuevo es cambio de config, no de código.

## Allowlist de redirect en checkout

Los endpoints de checkout aceptan una URL `returnTo` para que el usuario vuelva a tu app después del pago. Sin validación, ese parámetro se convierte en open redirect: un atacante manda a la víctima por tu checkout y sale a una página de phishing que parece haber salido de ti.

CastorStack valida `returnTo` contra los mismos orígenes permitidos del CORS. Una URL en un dominio fuera de la lista devuelve 400. Los tests en `PublicCheckoutEndpointsTests.cs` cubren el camino feliz y el rechazo.

## Lo que no está aquí

Esto no es certificación SOC 2. No hay WAF, ni middleware de rate limiting en la plantilla, ni escaneo automático de dependencias en el CI más allá de lo que añadas tú. El pentest es tu responsabilidad después del deploy.

La autenticación en dos factores no está implementada. Tampoco el listado y la revocación de sesiones. El audit log registra cambios del lado de la API, no eventos de login de Supabase.

La lista en la home es lo que está en el repositorio hoy, no un roadmap disfrazado de matriz de features. Prefiero que encuentres los huecos aquí que en una revisión de seguridad que no programaste.

## Por qué listar claims verificables

El marketing de seguridad que no se puede comprobar erosiona la confianza más rápido que no tener sección. Un comprador que clona el repo y abre `RowLevelSecurityCoverageTests.cs` aprende más sobre lo que se lleva que con cualquier insignia de compliance.

Si estás evaluando CastorStack, empieza por esos ocho ítems. Abre los archivos. Corre los tests. La home no te pide que confíes en nuestra palabra.
