Docs MindStack Suite
10
Seguridad y Cumplimiento — MindStack
Auth, MFA, PCI scope, protección de webhooks, RBAC y Ley 8968 CR
Versión
v1.0
Fecha
Mayo 2026
Audiencia
Dev · Legal
Estado
DRAFT
MINDSTACK
El sistema operativo de tu PyME. Todo en uno. Sin complicaciones.
Portal — Cheryx Suite

[CONSEJO — Doc 10] Contrarian: MindStack tiene un perfil de riesgo mayor que los módulos individuales porque concentra la relación de facturación con el cliente. Un breach en MindStack no solo expone datos — expone métodos de pago, facturas históricas, y la confianza que el cliente tiene con toda la suite Cheryx. Invertir en seguridad del portal es invertir en la reputación de los 4 módulos simultáneamente. Executor: El 80% del riesgo de seguridad de un portal SaaS viene de 3 lugares: tokens JWT mal validados, webhooks sin verificar firma, y passwords sin hashear con bcrypt/argon2. Resolver estos 3 primero antes de preocuparse por threat modeling avanzado. La complejidad de seguridad se agrega después, cuando hay más superficie de ataque real.


1. Modelo de autenticación

JWT + Refresh Token

# Duración de tokens
ACCESS_TOKEN_TTL = timedelta(minutes=15)   # Vida corta
REFRESH_TOKEN_TTL = timedelta(days=30)     # Solo en cookie httpOnly

# Payload del JWT
{
    "sub": "tenant_id:user_id",
    "tenant_id": "uuid",
    "rol": "owner|admin|viewer",
    "modulos": ["stockmind", "salesmind"],  # Módulos activos del tenant
    "exp": 1234567890,
    "jti": "uuid"  # Para revocación en Redis
}

Refresh token rotation: cada refresh genera un nuevo par access+refresh. El refresh anterior queda inválido inmediatamente. Si se detecta reuso del mismo refresh token → invalidar todos los tokens del usuario (posible robo).

MFA — TOTP

import pyotp

class MFAService:
    def setup_mfa(self, user_id: UUID) -> SetupResponse:
        secret = pyotp.random_base32()
        encrypted = self.crypto.encrypt(secret)  # AES-256 antes de guardar
        await self.db.execute(
            "UPDATE ms_usuarios SET mfa_secret=:s WHERE id=:id",
            {'s': encrypted, 'id': user_id}
        )
        totp = pyotp.TOTP(secret)
        return SetupResponse(
            secret=secret,
            qr_uri=totp.provisioning_uri(name=user.email, issuer_name="MindStack")
        )

    def verify_totp(self, user_id: UUID, code: str) -> bool:
        encrypted = await self.db.get_mfa_secret(user_id)
        secret = self.crypto.decrypt(encrypted)
        totp = pyotp.TOTP(secret)
        return totp.verify(code, valid_window=1)  # ±30 segundos

MFA obligatorio para: owners con suscripción active + roles admin. Opcional para viewers. Recordatorio en dashboard hasta activar.

OAuth Google


2. RBAC — Control de acceso por rol

Permiso Owner Admin Viewer
Ver facturas
Cambiar plan
Cancelar suscripción
Agregar usuarios
Ver métricas de uso
Acceder a módulo activo
Activar nuevo módulo (trial)
def require_role(*roles: str):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, current_user: User = Depends(get_current_user), **kwargs):
            if current_user.rol not in roles:
                raise HTTPException(403, "Rol insuficiente")
            return await func(*args, current_user=current_user, **kwargs)
        return wrapper
    return decorator

# Uso
@router.post("/subscriptions/cancel")
@require_role("owner")
async def cancelar_suscripcion(...)

3. Protección de webhooks ONVO Pay

import hmac, hashlib

def verificar_firma_onvo(payload: bytes, signature_header: str) -> bool:
    if not signature_header:
        return False

    expected = hmac.new(
        key=settings.ONVO_WEBHOOK_SECRET.encode(),
        msg=payload,
        digestmod=hashlib.sha256
    ).hexdigest()

    received = signature_header.replace("sha256=", "")

    # Comparación de tiempo constante — prevenir timing attacks
    return hmac.compare_digest(expected, received)

@router.post("/webhooks/onvo/payment_success")
async def onvo_webhook(request: Request):
    body = await request.body()
    signature = request.headers.get("X-ONVO-Signature", "")

    if not verificar_firma_onvo(body, signature):
        raise HTTPException(403, "Firma inválida")

    # Idempotencia: verificar que el payment_id no fue procesado ya
    payload = ONVOWebhookPayload.model_validate_json(body)
    if await redis.exists(f"webhook:processed:{payload.payment_id}"):
        return {"status": "already_processed"}

    await redis.setex(f"webhook:processed:{payload.payment_id}", 86400, "1")
    # ... procesar webhook

Reglas de webhooks: - Rechazar con 403 si la firma falta o no coincide — log del intento. - Idempotencia obligatoria — ONVO puede reenviar el mismo evento hasta 3 veces. - Tiempo de respuesta < 5 segundos — procesar en background (Celery) si es necesario. - IP allowlist de ONVO Pay en el firewall de Cloudflare (cuando estén documentadas).


4. PCI DSS scope

MindStack no es un merchant directo — usa ONVO Pay hosted checkout. El cliente es redirigido al dominio de ONVO para ingresar datos de tarjeta. Implicaciones:

Aspecto Estado
Datos de tarjeta almacenados ❌ Nunca — ONVO los guarda
PCI compliance requerido SAQ A (más simple) — solo redirect
Token de pago almacenado onvo_payment_id (referencia, no datos sensibles)
Checkout iframe embebido ❌ Evitar — preferir redirect para mantener SAQ A

Documentación: mantener registro de que MindStack usa redirect checkout (no embedded) para auditorías PCI. Cambiar a embedded iframe requeriría escalar a SAQ A-EP.


5. Protección de datos — Ley 8968 (Costa Rica)

Ley de Protección de la Persona frente al tratamiento de sus Datos Personales
N° 8968, vigente desde 2011.

Datos recopilados en MindStack

Dato Categoría Base legal Retención
email_admin Personal identificable Ejecución de contrato Duración suscripción + 5 años
nombre_empresa Organizacional Ejecución de contrato Ídem
mfa_secret Sensible (cifrado) Ejecución de contrato Hasta que usuario deshabilite MFA
onvo_payment_id Referencia de pago Ejecución de contrato 5 años (fiscal)
ms_billing_events Historial financiero Obligación legal 5 años mínimo
ms_uso_modulo Uso del servicio Interés legítimo 24 meses rolling
ultimo_login Seguridad Interés legítimo 12 meses rolling

Derechos del usuario (Arts. 8-13, Ley 8968)

/tenant/data-rights
  GET  /export       → Exportar todos los datos del tenant (JSON/CSV)
  POST /delete       → Solicitar eliminación (requiere cancelar suscripción primero)
  GET  /portability  → Exportar datos en formato portable

Plazo de respuesta: 5 días hábiles para cualquier solicitud de derechos.

Eliminación: datos personales anonimizados en 30 días desde solicitud. ms_billing_events se retiene anonimizado (requerimiento fiscal) — el billing_event queda, el tenant_id se reemplaza por NULL o un UUID anónimo.


6. Seguridad de infraestructura

Variables de entorno (nunca en código)

# .env (nunca commitear)
DATABASE_URL=postgresql+asyncpg://...
REDIS_URL=redis://...
JWT_SECRET=<256-bit aleatorio>
JWT_REFRESH_SECRET=<256-bit aleatorio diferente>
ONVO_WEBHOOK_SECRET=<proporcionado por ONVO>
ONVO_API_KEY=<proporcionado por ONVO>
MFA_ENCRYPTION_KEY=<AES-256 key>
RESEND_API_KEY=...
SENTRY_DSN=...

Headers de seguridad HTTP

# FastAPI middleware
@app.middleware("http")
async def security_headers(request: Request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    response.headers["Referrer-Policy"] = "strict-origin-when-cross-origin"
    response.headers["Content-Security-Policy"] = (
        "default-src 'self'; "
        "script-src 'self' 'unsafe-inline' plausible.io; "
        "connect-src 'self' api.onvo.me sentry.io"
    )
    return response

Rate limiting

from slowapi import Limiter

limiter = Limiter(key_func=get_remote_address)

@router.post("/auth/login")
@limiter.limit("5/minute")  # Anti-brute force
async def login(request: Request, ...): ...

@router.post("/webhooks/onvo/payment_success")
@limiter.limit("100/minute")  # Webhooks son legítimos en volumen
async def webhook(...): ...

7. Monitoreo de seguridad

Evento Respuesta Destino
Login fallido × 5 Bloquear IP 15 min + notificar usuario Redis + Sentry
Firma webhook inválida Log + alerta Sentry
Refresh token reutilizado Invalidar todas las sesiones del usuario Redis + email usuario
MFA disabled en cuenta owner Email de confirmación al dueño Resend
Cambio de plan Email de confirmación + billing_event Resend + DB
Cancelación de suscripción Email de confirmación + encuesta NPS Resend

Ver también: Doc 07 (SRD Backend — webhook handler) · Doc 08 (Modelo de Datos — ms_billing_events inmutable) · Doc 09 (Motor Core — guardrails G4, G6)