Módulo 1: Threat Modeling Andes Cargo

7. Manos a la obra: escribiendo el documento de modelo de amenazas

Descripción

Las lecciones 3 a 6 hicieron el trabajo de pensar. Esta lección hace el trabajo de documentar — y es, de las ocho lecciones de este módulo, la primera con un entregable real que no depende de LocalStack ni de ningún token de autenticación: THREAT-MODEL.md, un archivo Markdown que va a la raíz de andes-cargo-infra/, junto a versions.tf y al resto del proyecto que terraform-and-iac-guide dejó terminado. Este documento no es un resumen de las lecciones anteriores — es la fuente formal, ejecutada de verdad, que M2 a M8 de esta guía citan cada vez que necesitan justificar por qué un control existe.

Conexión con el módulo

Todo lo que necesitas para escribir este documento ya lo produjiste: el inventario de la lección 4, los siete riesgos de la lección 5, y el análisis de causa raíz de la lección 6. Esta lección no agrega ningún dato nuevo — organiza los que ya tienes en el formato que un modelo de amenazas real necesita para ser útil seis meses después, cuando nadie recuerde el razonamiento completo de memoria.


Por qué un modelo de amenazas se escribe como documento, no se queda en la cabeza de quien lo pensó

Vuelve a la analogía del auditor de seguros de la lección 4: caminar el edificio y anotar lo que encuentras vale poco si esas notas nunca se convierten en un documento que otra persona —un nuevo miembro del equipo, un auditor externo, tú mismo en seis meses— pueda leer sin tener que reconstruir todo el razonamiento de memoria. THREAT-MODEL.md es ese documento. Su prueba de calidad no es "¿suena completo?" — es "¿alguien que nunca vio este módulo podría leerlo y entender exactamente qué se identificó, con qué evidencia, y qué se va a hacer al respecto?".


Paso 1 — La estructura, antes del contenido

Un modelo de amenazas útil tiene cinco secciones, en este orden, siguiendo el patrón que documenta la OWASP Threat Modeling Cheat Sheet: alcance (qué sistema cubre este documento, y qué queda explícitamente afuera), descripción del sistema (el diagrama de flujo de datos, con los componentes reales), activos (qué hay que proteger, en orden de qué tan grave sería perderlo), hallazgos STRIDE (la tabla central, con evidencia), y mantenimiento del documento (cuándo y por qué se vuelve a revisar). Fíjate que las primeras tres secciones no son nuevas — son, literalmente, el trabajo de las lecciones 3 y 4, reorganizado para lectura, no para descubrimiento.


Paso 2 — El documento completo

En la raíz de andes-cargo-infra/, crea THREAT-MODEL.md con este contenido exacto:

# THREAT-MODEL.md — Andes Cargo Shipment Processing System

**Status:** Active · **Owner:** Platform/Security · **Methodology:** STRIDE (Microsoft)
**Covers:** `andes-cargo-infra/` as of `cicd-and-gitops-on-aws-guide` M4 (Terraform + CI/CD pipeline)
**Does not cover:** application-layer code inside `lambda/handler.py` beyond its declared IAM
permissions; the tracking app's own business logic; anything outside AWS account `000000000000`

## 1. Scope

This document models the threats to the AWS infrastructure that supports Andes Cargo's shipment
manifest processing pipeline: the S3 bucket that receives manifests, the Lambda function that
processes them, the DynamoDB table that stores shipment records, the two IAM roles that operate
this system, and the CI/CD pipeline that deploys changes to all of the above. It does not model
threats to the business logic inside the Lambda handler itself, nor to any system outside this
AWS account.

## 2. System overview

```text
  GitHub Actions runner (ci.yml, apply.yml)
  auth: static AWS credentials (test/test via .secrets, gitignored)
        │
        │ terraform apply
        ▼
  S3 bucket: andes-cargo-shipment-docs
  versioning: Enabled · policy: DenyInsecureTransport
  NO public-access-block declared
        │
        │ s3:ObjectCreated:* (prefix: manifests/)
        ▼
  Lambda: process-shipment-manifest (handler.py, packaged as function.zip, UNSIGNED)
  execution role: LambdaManifestProcessorRole (trust: lambda.amazonaws.com)
    - s3:GetObject / s3:ListBucket on the FULL bucket
    - dynamodb:PutItem on table Shipments only
        │
        │ PutItem
        ▼
  DynamoDB table: Shipments (PK: shipmentId, PAY_PER_REQUEST)
  NO deletion protection / prevent_destroy configured

  AppServerRole (trust: ec2.amazonaws.com) reads the SAME full-bucket scope
  as LambdaManifestProcessorRole — same actions, same resource ARNs.
```

## 3. Assets, ranked by impact if lost or exposed

| # | Asset | Why it matters |
|--:|---|---|
| 1 | `Shipments` table data | Sole source of truth for shipment state; loss stops the tracking app entirely |
| 2 | AWS credentials (`.secrets`) | Long-lived; compromise grants pipeline-equivalent access to AWS |
| 3 | Raw manifest documents (`manifests/` prefix) | Business-sensitive shipment data, not meant for the tracking app's read path |
| 4 | `process-shipment-manifest` deployment artifact | Unsigned; integrity between review and deployment is currently unverifiable |
| 5 | IAM role definitions | Define blast radius of any credential compromise |

## 4. STRIDE findings

| ID | Category | Finding | Evidence | Impact | Mitigated in |
|---|---|---|---|---|---|
| TM-01 | Spoofing | CI/CD pipeline authenticates with long-lived static credentials | `.secrets` (`AWS_ACCESS_KEY_ID=test`), read via `secrets.AWS_ACCESS_KEY_ID` in `ci.yml` | Anyone who obtains the credential value can authenticate as the pipeline, indistinguishable from the real one | Module 2 (OIDC federation) |
| TM-02 | Tampering | `function.zip` deployment artifact has no signature | `lambda.tf` (`data "archive_file"`, no post-build verification) | Nothing detects if the artifact changes between review and deployment | Module 6 (SBOM + `cosign`) |
| TM-03 | Repudiation | No CloudTrail trail exists | No `aws_cloudtrail` resource in `andes-cargo-infra/` | Manual out-of-band changes (e.g. `iam attach-role-policy`) leave no queryable record | Module 7 (CloudTrail, as far as LocalStack allows) |
| TM-04 | Information disclosure | S3 bucket has no public-access block | `modules/s3-bucket/` declares no `aws_s3_bucket_public_access_block` | Nothing in code prevents a future policy change from making the bucket public | Module 4 (`no-public-buckets.rego`) |
| TM-05 | Information disclosure | Credentials stored in plaintext on disk | `.secrets` (gitignored, but a flat plaintext file with no access control of its own) | Anyone with filesystem read access can read the credential, unlogged | Module 3 (SSM Parameter Store / Secrets Manager) |
| TM-06 | Denial of service | No control blocks a `plan` that destroys `Shipments` | No preventive policy evaluates `resource_changes[].actions` before `apply` | A bad `apply` — human error or a misled agent — removes the system's sole source of shipment state | Module 4 (`no-destroy-shipments.rego`) |
| TM-07 | Elevation of privilege | `AppServerRole` has the same full-bucket scope as `LambdaManifestProcessorRole` | `AppServerRole-policy` == `LambdaManifestProcessorRole-policy` (same actions, same resource ARNs) | A compromise of the read-only tracking app grants access to raw manifests it never needed to read | Module 2, lesson 7 (least-privilege role tightening) |

## 5. Reference incident: DataTalks.Club `terraform destroy` (Feb 2026)

A stale `terraform.tfstate` led an AI agent to interpret live production infrastructure as
orphaned, run `terraform destroy -auto-approve`, and remove ~2.5 years of data (1,943,200 rows in
the affected `courses_answer` table), with a ~24-hour recovery. The agent flagged the risk before
executing; a human approved anyway. This incident is the concrete precedent behind TM-06 and is
analyzed in full in this module's lesson 6. Source: [incidentdatabase.ai, Incident 1424](https://incidentdatabase.ai/cite/1424/).

## 6. Document maintenance

This document reflects `andes-cargo-infra/` as of the close of `cicd-and-gitops-on-aws-guide`.
Every module from M2 onward that closes a row in `RISK-MAP.md` (this repository, same root) must
update the corresponding row's status here. This document is reviewed, not rewritten from scratch,
whenever a new resource is added to `andes-cargo-infra/` or an existing IAM policy changes scope.

Paso 3 — Verificando el documento

Un modelo de amenazas real se lee, no se corre — pero puedes confirmar su forma con un par de comandos simples, exactamente igual que verificarías cualquier otro archivo Markdown del proyecto:

wc -l THREAT-MODEL.md
grep -c '^| TM-' THREAT-MODEL.md

Qué esperar (literal — el contenido de este documento lo escribiste tú, así que su forma es determinista, no depende de ningún servicio externo):

80 THREAT-MODEL.md
7

Ochenta líneas, siete filas que empiezan con | TM- — una por cada riesgo de la lección 5, ni una de más ni de menos. Si tu conteo no da exactamente 7, revisa que no hayas perdido ni duplicado ninguna fila de la tabla del Paso 4 de la sección anterior.


Por qué cada sección existe, no solo qué dice

La sección de alcance (1) existe para que nadie asuma cobertura que el documento no tiene. Un modelo de amenazas que no declara explícitamente qué queda afuera invita a que alguien lo lea como "todo está cubierto" — la misma clase de falso sentido de seguridad que la lección 2 de este módulo nombró para un pipeline en verde.

La sección de sistema (2) es, literalmente, la primera pregunta de OWASP ("¿en qué estamos trabajando?") resuelta con el diagrama real de la lección 4 — no una versión nueva, la misma, ahora dentro de un documento versionado en Git junto al código que describe.

La sección de activos (3) fuerza una priorización explícita antes de leer los hallazgos — sin ella, es fácil que un lector le dé el mismo peso mental a los siete hallazgos de STRIDE, cuando en realidad no todos protegen lo mismo si fallan. Fíjate que Shipments encabeza la lista, no por casualidad: es exactamente el activo que TM-06 y el incidente de la lección 6 identifican como el más expuesto a día de hoy.

La sección de hallazgos (4) es el corazón del documento — la misma tabla de la lección 5, sin ningún cambio de contenido, ahora con los identificadores TM-01 a TM-07 fijados de forma permanente. A partir de este documento, cualquier módulo posterior de esta guía puede referirse a "TM-06" sin tener que reexplicar qué es cada vez.

La sección de referencia al incidente (5) ancla el documento a evidencia externa verificable, no solo al razonamiento interno del equipo — la misma disciplina que distingue un modelo de amenazas real de una lista de opiniones.

La sección de mantenimiento (6) es la que la mayoría de los modelos de amenazas reales omiten, y la que más rápido los vuelve inútiles. Un documento que nadie actualiza cuando la infraestructura cambia se vuelve, en meses, una descripción de un sistema que ya no existe — peor que no tener documento, porque genera confianza falsa en información desactualizada.


Errores comunes

Escribir hallazgos vagos ("falta seguridad en IAM") en vez de específicos, con evidencia verificable (de calidad del documento). Qué pasa: alguien, apurado, resume un hallazgo real como una preocupación genérica, perdiendo el nombre exacto del recurso o la política involucrada. Cómo detectarlo: si un hallazgo de tu tabla no incluye un nombre de archivo, un ARN, o un nombre de política que alguien podría volver a verificar corriendo un comando. Cómo corregirlo: cada fila de la tabla del Paso 2 nombra el archivo o recurso exacto (.secrets, AppServerRole-policy, modules/s3-bucket/) — esa especificidad es lo que separa un modelo de amenazas real de una lista de ansiedades sin anclaje.

Tratar THREAT-MODEL.md como un documento que se escribe una vez y nunca se vuelve a tocar (de expectativa, ver la sección 6 del documento). Qué pasa: alguien completa esta lección, y en el Módulo 4, cuando no-destroy-shipments.rego resuelve TM-06 de verdad, nunca vuelve a este documento para actualizar su estado. Cómo detectarlo: si al cerrar esta guía, THREAT-MODEL.md sigue mostrando los siete riesgos como abiertos, aunque varios ya tengan su control construido. Cómo corregirlo: la sección 6 del documento mismo lo dice explícitamente — cada módulo que cierra una fila del RISK-MAP.md (lección 8) tiene que reflejar ese cierre aquí también. Vas a practicar exactamente esta actualización como ejercicio en varios módulos posteriores de esta guía.

Confundir "el documento existe" con "el riesgo está resuelto" (de alcance, retomado de la lección 1). Qué pasa: alguien, después de escribir un THREAT-MODEL.md completo y bien estructurado, siente que ya avanzó en seguridad real. Cómo detectarlo: si tu sensación de progreso después de esta lección es la misma que esperarías sentir después de construir un control real. Cómo corregirlo: documentar un riesgo con precisión es un prerequisito necesario para resolverlo con criterio —no puedes priorizar ni construir un control para algo que no identificaste con claridad—, pero no es, por sí mismo, la resolución. AppServerRole sigue teniendo exactamente el mismo alcance amplio después de esta lección que antes; lo único que cambió es que ahora existe evidencia escrita y organizada de por qué eso importa.


Ejercicios

Ejercicio 1 — Verifica que el documento no perdió ningún dato de las lecciones anteriores. Compara, fila por fila, la tabla de hallazgos del Paso 2 de esta lección contra la tabla de la lección 5. ¿Coinciden exactamente los siete identificadores, categorías y módulos de mitigación?

Ver solución

Sí, deberían coincidir exactamente: TM-01 (Spoofing, M2), TM-02 (Tampering, M6), TM-03 (Repudiation, M7), TM-04 (Information disclosure, M4), TM-05 (Information disclosure, M3), TM-06 (Denial of service, M4), TM-07 (Elevation of privilege, M2). Si encontraste alguna diferencia, revisa que no se haya perdido ningún dato al transcribir de la lección 5 al documento — un modelo de amenazas que diverge silenciosamente de su propio análisis previo es peor que uno incompleto, porque genera una falsa sensación de trazabilidad.

Ejercicio 2 — Explica por qué la sección de alcance excluye explícitamente la lógica de negocio del handler. El documento del Paso 2 dice explícitamente que no cubre "application-layer code inside lambda/handler.py beyond its declared IAM permissions". ¿Por qué esta exclusión es una decisión correcta, y no una laguna?

Ver solución

Porque un modelo de amenazas de infraestructura y uno de seguridad de aplicación son disciplinas relacionadas pero distintas, con métodos y herramientas diferentes (SAST/DAST para código de aplicación, frente a IAM/policy-as-code/escaneo de IaC para infraestructura) — mezclarlos en un solo documento sin declararlo produciría un documento que promete más cobertura de la que realmente tiene. Declarar la frontera explícitamente, como hace la sección 1, es exactamente la misma disciplina de honestidad de alcance que sostiene toda esta guía (ver la lección 1: qué NO entra en esta guía, y por qué).

Ejercicio 3 — Predice qué cambiaría en la sección 6 (mantenimiento) cuando el Módulo 2 se complete. Basándote en la instrucción de la sección 6 del documento, escribe la actualización exacta que le harías a la fila TM-01 de la tabla de hallazgos una vez que el Módulo 2 construya la identidad federada OIDC de verdad.

Ver solución

La fila de TM-01 seguiría teniendo la misma evidencia histórica (el hallazgo real que existía en el momento de este módulo), pero la columna "Mitigated in" pasaría de nombrar el módulo futuro a confirmar que ya está resuelta —una nota adicional del tipo "Resolved in M2.5: OIDC identity provider + scoped trust policy applied; .secrets/static credentials removed from ci.yml"—. La estructura de la tabla no cambia; lo que cambia es que una fila deja de describir un riesgo abierto y empieza a documentar, con la misma precisión, cómo se cerró. Esto es, en los hechos, lo que vas a hacer como parte del proyecto de cierre del Módulo 2.


Resumen y siguiente paso

En esta lección escribiste THREAT-MODEL.md, el primer entregable real de esta guía: seis secciones —alcance, sistema, activos, hallazgos STRIDE, incidente de referencia, mantenimiento—, construidas enteramente sobre el trabajo ya hecho en las lecciones 3 a 6, sin ningún dato nuevo, organizadas para que alguien que nunca vio este módulo pueda leerlas y entender el estado real de seguridad de andes-cargo-infra/. Confirmaste su forma con wc -l y un conteo de filas, y viste por qué cada una de las seis secciones existe, no solo qué dice.

Antes de avanzar deberías poder: explicar la función de cada una de las seis secciones del documento; recitar los siete identificadores TM-01 a TM-07 con su categoría y módulo de mitigación; y explicar por qué escribir el documento no resuelve, por sí mismo, ningún riesgo.

La lección 8, el proyecto que cierra este módulo, toma exactamente estos mismos siete riesgos y los convierte en RISK-MAP.md — el documento que ordena, no por categoría de STRIDE sino por la secuencia real en que esta guía los resuelve, el camino completo de M2 a M8.

Recursos

  1. OWASP Cheat Sheet Series — Threat Modeling Cheat Sheet — la estructura de documento (alcance, sistema, activos, hallazgos) que esta lección sigue.
  2. Microsoft Learn — Threats: Microsoft Threat Modeling Tool — las definiciones de STRIDE citadas en la tabla de hallazgos.
  3. incidentdatabase.ai — Incident 1424 — la fuente citada en la sección 5 del documento.
  4. Este módulo, lecciones 4 y 5 — la fuente completa de cada dato del documento: el inventario y el análisis STRIDE.