Royal Clean CRM · Documentación
Operación

Seguridad

Modelo de amenazas, contraseñas, sesiones, secretos y lista de comprobación.

Qué protegemos

Ordenado por daño si se filtra:

  1. Códigos de acceso a propiedades. Filtrarlos equivale a entregar la llave de la casa de un cliente. Es el activo más sensible del sistema.
  2. Fotografías de propiedades privadas. Muestran el interior de casas ajenas.
  3. Credenciales de acceso. Dan control sobre todo lo anterior.
  4. Datos financieros. Tarifas, salarios y márgenes de un negocio privado.
  5. Datos de contacto de clientes.

Modelo de amenazas

AmenazaMitigación
Un empleado curiosea servicios de otroFiltro por asignación en el WHERE de SQL; se devuelve 404
Un empleado manipula un id en la URLGuardas centrales; 404 indistinguible de «no existe»
Un empleado intenta ver códigos de una propiedad ajenaLos campos se eliminan en el servidor antes de serializar
Un empleado intenta ver su tarifa o el precioEsos campos no se seleccionan en sus consultas
Fuerza bruta contra el loginRate limiting en base de datos por usuario y por IP
Enumeración de usuariosMensaje único; verificación contra un hash señuelo
Robo de cookie de sesiónhttpOnly, secure, sameSite lax; revocación inmediata disponible
Un ex-empleado conserva accesoDesactivar la cuenta revoca todas sus sesiones al instante
Volcado de la base de datosContraseñas con Argon2id; sesiones guardadas como SHA-256
Acceso directo a la evidencia en R2Bucket privado; sólo URLs firmadas de 10 minutos
Subida de un archivo maliciosoTipo y tamaño firmados en la URL; lista blanca estrecha
Path traversal en el nombre del archivoLa clave del objeto la genera el servidor, con UUID
CSRFServer Actions con validación de origen; cookies sameSite
Un secreto acaba en un logSaneado automático por nombre de clave en logger y auditoría
Un secreto acaba en la imagen de Docker.dockerignore excluye todo .env

Autenticación

Contraseñas. Argon2id con los parámetros recomendados por OWASP: 19 MiB de memoria, 2 iteraciones, paralelismo 1. Son suficientemente costosos para frenar el cracking por GPU sin ahogar una VPS modesta. El hash es autodescriptivo, así que subir los parámetros más adelante no invalida los existentes.

Política. Mínimo 8 caracteres y prohibición de las contraseñas triviales más habituales. Se evita exigir símbolos y mayúsculas a propósito: el personal de campo trabaja desde el teléfono y esas reglas empujan a escribir la contraseña en un papel, que es peor.

Enumeración de usuarios. El login siempre responde «Usuario o contraseña incorrectos». Además, cuando el usuario no existe se verifica la contraseña contra un hash señuelo real, calculado con los mismos parámetros, para que el tiempo de respuesta no delate qué cuentas existen.

Rate limiting. 5 intentos fallidos por usuario y 20 por IP en 15 minutos. Se comprueba antes de ejecutar Argon2, para que verificar el hash no se convierta en el propio vector de agotamiento de CPU.

Sesiones

  • Token de 32 bytes aleatorios, en una cookie httpOnly, secure en producción y sameSite=lax.
  • La base guarda el SHA-256 del token: un volcado de la tabla no permite suplantar a nadie.
  • Duración de 30 días con renovación deslizante.
  • Un usuario inactivo o archivado se trata como no autenticado aunque su cookie siga siendo válida.

Se revocan todas las sesiones al desactivar una cuenta, al restablecer una contraseña y al cambiarla (excepto la sesión desde la que se actúa).

Autorización

Documentada por completo en AUTHORIZATION.md.

Las dos ideas centrales: la comprobación ocurre siempre en el servidor, y un recurso ajeno devuelve 404, no 403.

Evidencia y almacenamiento

  • Bucket privado. No existe ninguna URL pública permanente.
  • Ver una foto exige pasar por /api/evidence/{id}, que comprueba el acceso al servicio y sólo entonces redirige a una URL firmada de 10 minutos.
  • Las URLs de subida duran 15 minutos y llevan el tipo y el tamaño firmados: el cliente no puede subir algo distinto de lo autorizado.
  • La clave del objeto la genera el servidor con un UUID. El nombre del archivo del usuario nunca se usa como ruta.
  • Sólo se aceptan image/webp, image/jpeg y tres formatos de vídeo.
  • Confirmar una subida verifica con HEAD que el objeto existe de verdad.

Detalle en R2_STORAGE.md.

Privacidad del personal

  • No se rastrea la ubicación. El único uso de mapas es abrir la dirección de la propiedad en Google Maps.
  • La compresión en el navegador descarta los metadatos EXIF, incluidas las coordenadas GPS y el modelo del dispositivo. Esos datos no salen del teléfono.
  • La cabecera Permissions-Policy deshabilita geolocalización y micrófono.

Registro

src/lib/logger.ts emite JSON por línea y sanea automáticamente por nombre de clave: contraseñas, hashes, tokens, secretos, cookies, credenciales y códigos de acceso se sustituyen por [redactado].

Las URLs firmadas se recortan antes de la firma: una URL completa en un log sería reutilizable mientras no expire.

La auditoría aplica el mismo saneado. Que un código de puerta cambie sí se audita; su valor, no.

Cabeceras HTTP

Configuradas en next.config.ts:

CabeceraValor
X-Content-Type-Optionsnosniff
X-Frame-OptionsDENY
Referrer-Policystrict-origin-when-cross-origin
Permissions-Policygeolocation=(), microphone=(), payment=(), camera=(self)

poweredByHeader está desactivado.

Secretos

  • Nunca en el repositorio. .gitignore excluye .env; .dockerignore impide que llegue a la imagen.
  • .env.example documenta cada variable sin valores reales.
  • La validación es perezosa, así que el build no necesita secretos.
  • El endpoint de trabajos usa comparación en tiempo constante del token.

Errores

Sólo se muestran al usuario los errores de dominio, escritos en español. Todo lo demás se registra completo en el servidor y se sustituye por un mensaje genérico: una traza de pila o un error de Prisma en pantalla es a la vez inútil y una filtración.

Lista de verificación previa a producción

Autenticación y sesiones

  • AUTH_SECRET con 32+ caracteres, generado aleatoriamente
  • Las cookies llegan con secure (HTTPS activo)
  • Desactivar un empleado corta su acceso de inmediato
  • Restablecer una contraseña obliga a cambiarla al ingresar

Autorización

  • Un empleado no puede abrir el servicio de otro (404)
  • Un empleado no ve códigos de acceso fuera de su servicio activo
  • Un empleado no ve tarifas, precios ni márgenes en ninguna pantalla
  • Las rutas de /admin rechazan a un empleado

Almacenamiento

  • El bucket de R2 es privado
  • Una URL firmada sin sus parámetros de firma es rechazada
  • El token de R2 tiene permiso sólo sobre el bucket de evidencia

Infraestructura

  • PostgreSQL no responde desde fuera de la VPS
  • HTTPS con renovación automática
  • /api/jobs/run sin token responde 401
  • El contenedor corre como usuario sin privilegios
  • NODE_ENV=production

Datos

  • Los respaldos funcionan y se ha probado una restauración
  • Ningún secreto en el historial de git
  • La semilla no se ha ejecutado en producción

Los tests de integración cubren buena parte de estos puntos de forma automática: ver tests/integration/authorization.test.ts y tests/integration/evidence-storage.test.ts.

On this page