Ir al contenido

Seguridad y privacidad

SoftSys Edu Analytics aplica un modelo de seguridad en profundidad: autenticación en cada capa, CORS con lista blanca en producción, secretos validados al arranque de los workers, y cabeceras de seguridad en todas las respuestas.

  • Las claves API viajan en la cabecera HTTP X-SSEA-Key para llamadas servicio-a-servicio (p. ej. desde el plugin Moodle al endpoint de ingesta).
  • HTTPS obligatorio en producción. Cloudflare siempre termina TLS en el edge.
  • Las claves se almacenan como hashes SHA-256 en la tabla api_keys (columna key_hash). El valor en texto plano nunca se persiste después del aprovisionamiento.
  • Formato: ssea_ + 32 caracteres hex (160 bits de entropía).
  • Expiración opcional mediante expires_at — claves expiradas se rechazan con HTTP 401.
  • Revocación: poner is_active = 0 en la fila de api_keys; surte efecto inmediatamente.
  • La API del dashboard acepta Bearer JWTs (HS256) para la comunicación navegador ↔ API.
  • Los JWTs son de vida corta (1 hora por defecto) y se firman con un secret gestionado por Cloudflare Workers.
  • La expiración se verifica en cada request usando payload.exp.
  • El flujo típico: el plugin Moodle firma un token con secreto compartido y lo intercambia por un JWT de sesión vía POST /v1/auth/sso.

Viewer headers (lecturas embebidas en Moodle)

Sección titulada «Viewer headers (lecturas embebidas en Moodle)»

Los endpoints /v1/tenants/:id/viewer/** requieren además:

  • X-SSEA-Viewer-Context: payload JSON codificado en base64url con tenant_id, role, moodle_user_id, exp.
  • X-SSEA-Viewer-Signature: HMAC-SHA256 del contexto, firmado con el viewer shared secret configurado en el plugin.

Esto permite que un estudiante o profesor vea sus propios datos desde una iframe dentro de Moodle sin tener JWT de dashboard.

Cada consulta a la base de datos obligatoriamente filtra por tenant_id:

WHERE tenant_id = ?1

La API del dashboard además verifica que el tenant autenticado coincida con el recurso solicitado:

if (tenantId !== auth.tenant_id) return 403 Forbidden

No existe ningún endpoint admin de bypass en el código de producción. Un tenant nunca puede leer datos de otro.

  • Las direcciones IP nunca se almacenan en texto plano.
  • El worker de ingesta calcula SHA-256(ip + salt) antes de encolar el evento y descarta la IP original.
  • La sal (salt) es un secret de Cloudflare Workers, rotable.
  • El plugin Moodle soporta una opción no_ip que excluye completamente la IP del payload antes de enviarlo — útil si tu política interna prohíbe que los datos abandonen el origen con IPs.

El plugin expone un classes/privacy/provider.php estándar que se integra con el flujo GDPR nativo de Moodle:

  • Exportación de datos del sujeto: usuarios pueden exportar su información a través del flujo estándar de Moodle.
  • Derecho al olvido: cuando un usuario se elimina en Moodle, el plugin inmediatamente deja de generar eventos para ese user_id.
  • Sin datos locales persistentes: la única tabla transient del plugin (local_ssea_queue) se vacía cada 5 minutos al enviar los eventos a la ingesta.

Ver Retención de datos para el ciclo de vida completo de los datos una vez ingestados.

FaseMecanismo
En tránsitoTLS 1.3 entre plugin Moodle ↔ ingest, ingest ↔ queue, queue ↔ processor, dashboard ↔ API. Certificados gestionados por Cloudflare.
En reposoLos datos en Cloudflare D1 (SQLite) y Cloudflare R2 (object storage) están cifrados at rest por defecto (AES-256).
Claves y secretosGestionados como secretos de Cloudflare Workers. Nunca en wrangler.toml, logs, ni control de versiones.

Todas las respuestas HTTP incluyen:

CabeceraValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
X-Request-IDUUID único por request (propagado si el cliente lo envía, generado si no)

CORS se controla por la variable de entorno CORS_ALLOWED_ORIGINS (gestionada como secret del worker en producción).

  • Formato: lista separada por comas de patrones de origen. Wildcards (*) matchean una etiqueta DNS.
  • Producción por defecto: https://*.softsysanalytics.com.
  • Desarrollo: añade http://localhost:*, http://127.0.0.1:*.
  • Orígenes no permitidos reciben Access-Control-Allow-Origin: null.
  • Todas las respuestas CORS incluyen Vary: Origin.
ParámetroValor por defecto
Requests por ventana (por tenant)1 000
Ventana60 segundos
AlmacenamientoCloudflare KV

Los requests limitados reciben HTTP 429 con cabecera Retry-After. En planes Enterprise el límite es mayor (hasta 10 000 req/60s), negociado por contrato.

Ver API — Ingesta de eventos para ver cómo se reflejan los errores 429 en el cliente.

Cada respuesta incluye una cabecera X-Request-ID (UUID). Si el cliente envía un X-Request-ID en el request, el valor se propaga y se loggea en cada worker por donde pasa el request. Esto permite correlacionar logs entre los distintos componentes del backend usando un único identificador.

Los workers validan los secretos requeridos antes de atender cualquier request. Si falta un secret crítico, el worker responde:

HTTP 503 Service Unavailable
{
"ok": false,
"error": "Service unavailable — missing configuration",
"code": "MISCONFIGURED"
}

Esto previene fallos silenciosos donde la ausencia de un secret causa comportamientos inesperados.

Si descubres una vulnerabilidad de seguridad, envía un email a security@softsyssolutions.com.

No abras un issue público en ninguna plataforma para reportes de seguridad.

Te responderemos dentro de las 48 horas hábiles y coordinaremos la divulgación responsable.