Saltar al contenido

Temas

Caso práctico: modelo de amenazas de una app de 3 capas

El modelo de amenazas completo de Citas, de los activos al riesgo residual, y una plantilla para hacer el tuyo.

IntermedioEn progreso 10 min de lectura1 oct 2026#modelado-de-amenazas#stride#aws#arquitectura#riesgo

La teoría del modelado de amenazas cabe en cuatro preguntas. Lo difícil es aplicarla a un sistema real sin acabar con un documento de 40 páginas que nadie vuelve a abrir. En esta nota modelamos Citas de principio a fin: una SaaS de reserva de citas para clínicas pequeñas, construida en tres capas sobre AWS. El resultado es un modelo de amenazas que cabe en el repositorio, se revisa en las PR y te dice qué hacer el lunes por la mañana. Al final tienes una plantilla para hacer lo mismo con tu propia aplicación.

Seguimos el marco de las cuatro preguntas: ¿en qué estamos trabajando?, ¿qué puede salir mal?, ¿qué vamos a hacer al respecto? y ¿hicimos un buen trabajo?

1. ¿En qué estamos trabajando?#

Alcance y supuestos#

Dentro del alcance: la SPA (S3 + CloudFront + AWS WAF), la autenticación con Amazon Cognito, la API Node.js en ECS Fargate detrás de un ALB, el worker que consume SQS y envía recordatorios por SNS/SES, RDS PostgreSQL, el bucket citas-adjuntos, Secrets Manager y el despliegue desde GitHub Actions.

Fuera del alcance: la seguridad física e hipervisor de AWS (es su parte del modelo de responsabilidad compartida), los dispositivos de las clínicas y la infraestructura interna de SES.

Supuestos (si alguno deja de ser cierto, el modelo hay que revisarlo):

  • Todo el tráfico va cifrado con TLS 1.2 o superior, también entre el ALB y la API.
  • Producción, staging y log-archive son cuentas separadas bajo AWS Organizations, con SCPs.
  • CloudTrail organizacional escribe en la cuenta log-archive, a la que el equipo de producto no tiene acceso de escritura.
  • Cada usuario pertenece a una clínica, y la clínica viaja como claim (custom:clinica_id) en el JWT de Cognito.

Activos#

Activo Dónde vive Propiedad que más importa
PII de pacientes (nombre, teléfono, email, notas) RDS, adjuntos en S3 Confidencialidad, integridad
Agenda de citas RDS Integridad, disponibilidad (una cita borrada es un paciente sin atender)
Credenciales y sesiones Cognito, rol de tarea ECS, rol de despliegue, Secrets Manager Confidencialidad
Registros de auditoría CloudWatch Logs, CloudTrail en log-archive Integridad, no repudio

Actores de amenaza#

  • Externo oportunista: credential stuffing contra el login y escaneo masivo de buckets y APIs.
  • Externo dirigido: phishing a recepcionistas para entrar con una cuenta legítima.
  • Insider de una clínica: un usuario autenticado que intenta ver pacientes de otra clínica. En una SaaS multi-tenant es el actor que más se olvida.
  • Cadena de suministro: una dependencia npm o una GitHub Action comprometidas.

DFD y límites de confianza#

El diagrama de flujo de datos (DFDDiagrama de flujo de datosDiagrama con entidades externas, procesos, almacenes y flujos de datos con dirección; es el modelo habitual sobre el que se buscan amenazas.) de nivel 1 tiene dos entidades externas (personal de la clínica y paciente, que solo recibe recordatorios), cuatro procesos (CloudFront/WAF, ALB, API y worker) y cuatro almacenes (RDS, S3, SQS y Secrets Manager). Lo importante son los flujos que cruzan un límite de confianzaLímite de confianzaFrontera donde cambia el nivel de confianza o de privilegio; todo lo que la cruza debe autenticarse, autorizarse, validarse y, normalmente, cifrarse.:

  • LC1: Internet → borde de AWS (navegador → CloudFront/WAF).
  • LC2: borde → VPC (CloudFront → ALB → API).
  • LC3: aplicación → datos (API → RDS y API → S3).
  • LC4: GitHub → cuenta de producción (despliegue vía OIDC). No aparece en la animación, pero es igual de real.
Modelo de amenazas de Citas (3 capas)
INTERNETPRESENTACIÓNAPLICACIÓN · VPCDATOS · AISLADASUsuarioclínicaCloudFront+ AWS WAFALBpúblicoAPIECS FargateRDSPostgreSQL · KMSS3citas-adjuntosLC1LC2LC3SRobo de cuentasRRepudio de citasDDDoS capa 7TSQLi en /citasERol IAM amplioISnapshot compartidoIBucket públicoSMFA + WAF ATPRAceptado · logsDWAF por tasaTConsultas param.EIAM mínimoISCP + KMSIBlock Public Access7 amenazas · 4 altas · 3 mediasRiesgo residual: 6 mitigadas · 1 aceptada · 0 altas abiertas

Paso 1 de 6Arquitectura

Citas en tres capas: el navegador llega a CloudFront con WAF, el ALB enruta a la API en Fargate y los datos viven en RDS y en el bucket de adjuntos.

2. ¿Qué puede salir mal?#

Recorremos cada elemento con STRIDE (amenazas por elemento) y anotamos solo amenazas concretas, con su mitigación en AWS. Impacto y probabilidad en escala alto/medio/bajo.

ID Elemento STRIDE Amenaza Impacto × Prob. Mitigación en AWS Estado
T01 Personal de la clínica S Credential stuffing o phishing para entrar con una cuenta real Alto × Alta MFA obligatorio en Cognito, threat protection (plan Plus), regla ATP de AWS WAF en el login Mitigada
T02 Personal de la clínica R Una recepcionista niega haber cancelado una cita Medio × Media Log de auditoría por acción (quién, qué, cuándo) en tabla append-only y CloudWatch Logs con 1 año de retención Aceptada
T03 CloudFront / WAF D DDoS de capa 7 contra el endpoint de disponibilidad Medio × Media AWS Shield Standard, reglas rate-based de WAF, caché de disponibilidad en CloudFront Mitigada
T04 SPA en S3 T Modificación del bundle JS servido a los usuarios Alto × Baja Bucket solo accesible por CloudFront (OAC), escritura solo desde el rol de CI, versionado, CSP Mitigada
T05 API T SQL injection en la búsqueda de citas Alto × Media Consultas parametrizadas, AWSManagedRulesSQLiRuleSet, SAST en CI Mitigada
T06 API I IDOR/BOLA: una clínica lee pacientes de otra Alto × Alta clinica_id sacado del JWT y nunca del request, Row-Level Security en PostgreSQL, tests de autorización En curso
T07 API (rol de tarea) E El rol de tarea ECS tiene s3:* y rds:* Alto × Media Rol mínimo (Get/Put solo en citas-adjuntos/<clinica>/*), IAM Access Analyzer de accesos no usados Mitigada
T08 API → RDS I Credenciales de la base de datos en variables de entorno o en el código Alto × Media Secrets Manager con rotación, inyectado como secrets en la task definition Mitigada
T09 RDS I Snapshot compartido con una cuenta externa Alto × Baja Clave KMS del cliente con key policy restrictiva, SCP sobre rds:ModifyDBSnapshotAttribute, alerta en EventBridge Mitigada
T10 RDS D Borrado accidental o malicioso de la base de datos Alto × Baja Deletion protection, backups automáticos y AWS Backup en otra cuenta con Vault Lock Mitigada
T11 S3 citas-adjuntos I Bucket u objetos públicos Alto × Media Block Public Access en la cuenta, URLs prefirmadas de 5 minutos, SSE-KMS Mitigada
T12 S3 citas-adjuntos T Subida de archivos maliciosos como adjunto Medio × Media Validación de tipo y tamaño, GuardDuty Malware Protection for S3, prefijo de cuarentena En curso
T13 SQS (worker) S Mensajes falsos en la cola que disparan recordatorios fraudulentos a pacientes Medio × Baja Política de cola: solo el rol de la API puede sqs:SendMessage; cifrado SSE Mitigada
T14 GitHub → AWS (LC4) E Un workflow de otro repositorio asume el rol de despliegue Alto × Media Trust policy OIDC con sub limitado a repo:clinicas/citas:environment:prod, revisión obligatoria en el entorno Mitigada
T15 CloudTrail / logs R El atacante desactiva o borra los registros para cubrir su rastro Alto × Baja Trail organizacional en log-archive, S3 Object Lock, SCP que deniega cloudtrail:StopLogging y cloudtrail:DeleteTrail, hallazgos de GuardDuty Mitigada

3. ¿Qué vamos a hacer al respecto?#

Priorización: impacto × probabilidad#

Impacto \ Probabilidad Baja Media Alta
Alto T04, T09, T10, T15 T05, T07, T08, T11, T14 T01, T06
Medio T13 T02, T03, T12 —
Bajo — — —

El orden de trabajo sale solo: primero T01 y T06, después la fila alto × media. T06 está «en curso», así que es la prioridad número uno del trimestre. La mitigación combina dos capas: la API toma la clínica del token y PostgreSQL lo impone aunque la API tenga un bug.

migrations/015_rls_citas.sql
ALTER TABLE citas ENABLE ROW LEVEL SECURITY;
ALTER TABLE citas FORCE ROW LEVEL SECURITY;
CREATE POLICY aislamiento_por_clinica ON citas
USING (clinica_id = current_setting('app.clinica_id', true)::uuid)
WITH CHECK (clinica_id = current_setting('app.clinica_id', true)::uuid);
api/src/db/withTenant.ts
export async function withTenant<T>(claims: CognitoClaims, fn: (tx: Tx) => Promise<T>) {
const clinicaId = claims['custom:clinica_id'];
if (!clinicaId) throw new ForbiddenError('token sin clínica');
return db.transaction(async (tx) => {
// is_local = true: la variable solo vive dentro de esta transacción
await tx.query("SELECT set_config('app.clinica_id', $1, true)", [clinicaId]);
return fn(tx);
});
}

Riesgos aceptados#

Aceptar un riesgoRiesgoCombinación de la probabilidad de que una amenaza se materialice y del impacto que tendría. Se mitiga, transfiere, evita o acepta. es una decisión legítima siempre que sea explícita: tiene responsable, justificación y fecha de revisión. Si no lo escribes, no lo has aceptado: lo has ignorado.

threat-model/riesgos-aceptados.yaml
- id: T02
riesgo: Repudio de cancelaciones por parte del personal de la clínica
decision: aceptado
justificacion: >
El impacto es operativo (discusión interna en la clínica), no de seguridad de datos.
Firmar cada acción del usuario tiene un coste desproporcionado para este riesgo.
control_parcial: log de auditoría append-only (90 días en caliente, 1 año en S3)
responsable: tech-lead de Citas
revisar: 2027-04-01

4. ¿Hicimos un buen trabajo?#

Una amenaza «mitigada» sin evidencia es una opinión. Cada mitigación debe tener un control verificable:

Amenazas Cómo se valida Frecuencia
T05, T06 Tests de integración de autorización en CI (un usuario de la clínica A no lee ni modifica recursos de la clínica B) y DAST contra staging Cada PR / semanal
T07, T14 aws accessanalyzer validate-policy en CI y hallazgos de accesos externos y no usados de IAM Access Analyzer Cada PR / continuo
T11 Reglas de AWS Config s3-account-level-public-access-blocks-periodic y s3-bucket-level-public-access-prohibited Continuo
T15 Hallazgo de GuardDuty Stealth:IAMUser/CloudTrailLoggingDisabled enrutado a alertas Continuo
T01 Game day: credential stuffing simulado contra staging para comprobar el bloqueo Semestral
Todas Pentest externo con el modelo de amenazas como entrada Anual y tras cambios grandes
Terminal
# Lista solo errores y avisos de seguridad; en CI, falla el job si la salida no está vacía
aws accessanalyzer validate-policy \
--policy-type IDENTITY_POLICY \
--policy-document file://iam/api-task-role.json \
--query 'findings[?findingType==`ERROR` || findingType==`SECURITY_WARNING`]'

Mantenerlo vivo#

Un modelo de amenazas envejece en cuanto cambia la arquitectura. Para que no muera:

  • Guárdalo en threat-model/ dentro del repositorio, en Markdown, junto al código que describe. Herramientas como Threat Composer de AWS o OWASP Threat Dragon exportan a JSON versionable.
  • Añade a la plantilla de PR una pregunta que obligue a revisarlo cuando toca:
.github/pull_request_template.md
### Seguridad
- [ ] ¿Añade o cambia un flujo que cruza un límite de confianza?
- [ ] ¿Toca PII, credenciales o permisos IAM?
- [ ] Si alguna respuesta es sí: `threat-model/` actualizado y amenaza enlazada (Txx).
  • Revísalo cada trimestre y después de cada incidente. Un incidente que el modelo no contemplaba es la mejor señal de que le falta una rama.

Tu turno

Modela una app de 3 capas que ya hayas construido

Elige una aplicación de tres capas (presentación, aplicación, datos) que hayas construido o mantenido. Tómate 90 minutos y rellena la plantilla. No busques la perfección: busca las 3–5 amenazas que más te preocupan y un control verificable para cada una.

  • ¿Qué activo haría más daño perder: datos personales, dinero, disponibilidad?
  • ¿Dónde están tus límites de confianza y qué flujos los cruzan?
  • ¿Tu app es multi-tenant? ¿Qué impide que un cliente lea los datos de otro?
  • ¿Qué amenaza aceptarías hoy, quién firma esa decisión y cuándo la revisarías?
  • ¿Qué evidencia podrías enseñar mañana de que cada mitigación funciona?
threat-model/README.md
# Modelo de amenazas: <nombre de la app>
Versión: 1.0 · Fecha: AAAA-MM-DD · Responsable: <nombre>
## 1. ¿En qué estamos trabajando?
### Alcance
- Dentro: ...
- Fuera: ...
### Supuestos
- ...
### Activos
| Activo | Dónde vive | Propiedad que más importa (C/I/A) |
| ------ | ---------- | --------------------------------- |
### Actores de amenaza
- ...
### DFD (nivel 1) y límites de confianza
- Entidades externas: ...
- Procesos: ...
- Almacenes: ...
- Flujos que cruzan límites: LC1 ..., LC2 ..., LC3 ...
## 2. ¿Qué puede salir mal?
| ID | Elemento | STRIDE | Amenaza | Impacto × Prob. | Mitigación | Estado |
| --- | -------- | ------ | ------- | --------------- | ---------- | ------ |
| T01 | | | | | | |
## 3. ¿Qué vamos a hacer al respecto?
### Prioridades (impacto × probabilidad)
### Riesgos aceptados (responsable, justificación, fecha de revisión)
## 4. ¿Hicimos un buen trabajo?
### Validación por amenaza (test, regla, alerta o revisión)
### Próxima revisión: AAAA-MM-DD

Reto

¿Por qué el IDOR entre clínicas (T06) no lo detiene AWS WAF?

▸Mostrar solución

Porque la petición maliciosa es indistinguible de una legítima: usuario autenticado, formato correcto, sin patrones de ataque. Solo cambia un identificador de recurso. La defensa está en la autorización a nivel de objeto: tomar la clínica del token, comprobar la pertenencia en cada acceso y, como segunda capa, Row-Level Security en la base de datos.

Reto

Has marcado T11 (bucket público) como mitigada. ¿Qué evidencia debería existir?

▸Mostrar solución

Block Public Access activado a nivel de cuenta, una SCP que impide desactivarlo (s3:PutAccountPublicAccessBlock), las reglas de AWS Config en estado compliant y una alerta si alguna deja de cumplirse. Y, como prueba funcional, que el objeto solo sea accesible mediante una URL prefirmada de corta duración.

En resumen#

  • Un modelo de amenazas útil responde a las cuatro preguntas y cabe en el repositorio: alcance, activos, actores, DFD con límites de confianza, amenazas, prioridades, riesgos aceptados y validación.
  • Las amenazas se anotan por elemento con STRIDE y siempre con una mitigación concreta y un estado.
  • En una SaaS multi-tenant, la amenaza más grave suele ser de autorización (IDOR/BOLA), y ningún control perimetral la resuelve.
  • Prioriza con impacto × probabilidad. Lo que no mitigues, acéptalo por escrito, con responsable y fecha.
  • «Mitigada» significa que existe un control que una máquina puede verificar. Revisa el modelo en cada PR que cambie la arquitectura.

Referencias#

Notas relacionadas

ESC
↑↓ para navegar↵ para abriresc para cerrar