Módulo 1: Threat Modeling Andes Cargo

4. Manos a la obra: mapeando la superficie de ataque real de Andes Cargo

Descripción

Esta lección resuelve la primera pregunta del modelado de amenazas —"¿en qué estamos trabajando?"— para Andes Cargo, con comandos reales, no con un diagrama imaginado de memoria. Vas a correr cuatro consultas de inventario contra la infraestructura heredada de terraform-and-iac-guide: quién puede asumir cada rol, qué políticas tiene adjuntas cada uno, qué dice la política del bucket, y quién puede invocar la función. El resultado es el inventario exacto que la lección 5 va a analizar con STRIDE.

Honestidad de ejecución, antes del primer comando. Este entorno de escritura no tiene un LOCALSTACK_AUTH_TOKEN exportado —el mismo límite exacto que ya viste en cicd-and-gitops-on-aws-guide, Módulo 4, lección 7—, así que el contenedor de LocalStack no arranca aquí. Cada comando awslocal de esta lección es, por lo tanto, representativo: la salida que se muestra es la que produciría ese comando contra andes-cargo-infra/ ya aplicado, reconstruida campo por campo a partir del HCL real de terraform-and-iac-guide (Módulos 6 y 7) y de las verificaciones que esa guía ya confirmó con terraform plan/apply reales —nunca inventada, siempre con su fuente exacta señalada al lado del bloque. Corriendo estos mismos comandos por tu cuenta, con LocalStack levantado y andes-cargo-infra/ aplicado, deberías ver exactamente esta salida.

Conexión con el módulo

La lección 3 te dio el vocabulario (STRIDE) y un boceto genérico del diagrama de flujo de datos de Andes Cargo. Esta lección llena ese boceto con nombres reales, extraídos con los mismos cuatro comandos que un profesional de seguridad correría primero frente a cualquier cuenta de AWS que tuviera que auditar. La lección 5 toma esta tabla, línea por línea, y le aplica las seis categorías de STRIDE.


Analogía: el auditor que camina el edificio antes de escribir la póliza

Antes de asegurar un edificio, un auditor de seguros no confía en el plano que el dueño le entrega — camina el edificio piso por piso: cuenta cuántas puertas tienen llave y cuáles no, revisa qué llaves abren qué puertas, y anota, sin opinar todavía, exactamente lo que encuentra. Recién con ese inventario completo en la mano empieza a preguntarse qué podría salir mal. Esta lección es esa caminata: cuatro preguntas concretas, cuatro respuestas reales, sin ningún juicio de "esto está mal" todavía —ese juicio llega en la lección 5—.


Paso 1 — awslocal iam list-roles: quién existe

awslocal iam list-roles --query 'Roles[].{RoleName:RoleName,Trust:AssumeRolePolicyDocument.Statement[0].Principal.Service}'

Qué esperar (representativo — mismo formato ya confirmado en terraform-and-iac-guide, Módulo 6, lección 5; el orden exacto puede variar entre ejecuciones, lo que no varía es que ambos roles estén presentes con el trust correcto):

[
    { "RoleName": "AppServerRole", "Trust": "ec2.amazonaws.com" },
    { "RoleName": "LambdaManifestProcessorRole", "Trust": "lambda.amazonaws.com" }
]

Dos identidades, dos servicios de confianza distintos: AppServerRole lo puede asumir cualquier instancia EC2 con el perfil correcto, LambdaManifestProcessorRole únicamente el servicio Lambda. Ninguna persona humana aparece en esta lista —Andes Cargo, en este ecosistema, no tiene usuarios IAM individuales, solo roles de servicio—. Este es el primer dato del inventario: dos identidades de servicio, cero identidades humanas con acceso directo documentado.


Paso 2 — awslocal iam list-attached-role-policies: la primera pregunta, que responde "ninguna"

awslocal iam list-attached-role-policies --role-name LambdaManifestProcessorRole
awslocal iam list-attached-role-policies --role-name AppServerRole

Qué esperar (representativo):

{
    "AttachedPolicies": []
}
{
    "AttachedPolicies": []
}

Este resultado vacío es, en sí mismo, un hallazgo real, no un error. list-attached-role-policies responde una pregunta específica: ¿qué políticas administradas (managed policies, con su propio ARN, reutilizables entre roles) tiene este rol adjuntas? La respuesta, para los dos roles de Andes Cargo, es cero — porque terraform-and-iac-guide construyó ambos roles con políticas inline (aws_iam_role_policy, definidas dentro del propio rol, sin ARN independiente), no con políticas administradas adjuntas. Un rol sin políticas administradas no es un rol sin permisos — es un rol cuyos permisos viven en otro lugar. La pregunta siguiente encuentra ese lugar.


Paso 3 — awslocal iam list-role-policies + get-role-policy: donde viven los permisos reales

awslocal iam list-role-policies --role-name LambdaManifestProcessorRole

Qué esperar (representativo):

{
    "PolicyNames": [
        "LambdaManifestProcessorRole-policy",
        "LambdaManifestProcessorRole-dynamodb-write-policy"
    ]
}

LambdaManifestProcessorRole tiene dos políticas inline, no una — el segundo dato del inventario que list-attached-role-policies por sí solo nunca iba a mostrar. Léelas una por una:

awslocal iam get-role-policy --role-name LambdaManifestProcessorRole --policy-name LambdaManifestProcessorRole-policy

Qué esperar (representativo — reconstruido del data "aws_iam_policy_document" "lambda_permissions" de terraform-and-iac-guide, Módulo 6, lección 4):

{
    "RoleName": "LambdaManifestProcessorRole",
    "PolicyName": "LambdaManifestProcessorRole-policy",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "LambdaReadManifests",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    "arn:aws:s3:::andes-cargo-shipment-docs",
                    "arn:aws:s3:::andes-cargo-shipment-docs/*"
                ]
            }
        ]
    }
}
awslocal iam get-role-policy --role-name LambdaManifestProcessorRole --policy-name LambdaManifestProcessorRole-dynamodb-write-policy

Qué esperar (representativo — reconstruido de terraform-and-iac-guide, Módulo 7, lección 5):

{
    "RoleName": "LambdaManifestProcessorRole",
    "PolicyName": "LambdaManifestProcessorRole-dynamodb-write-policy",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "WriteShipmentRecords",
                "Effect": "Allow",
                "Action": ["dynamodb:PutItem"],
                "Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/Shipments"
            }
        ]
    }
}

Ahora AppServerRole:

awslocal iam list-role-policies --role-name AppServerRole
awslocal iam get-role-policy --role-name AppServerRole --policy-name AppServerRole-policy

Qué esperar (representativo — reconstruido de terraform-and-iac-guide, Módulo 6, lección 5):

{
    "PolicyNames": ["AppServerRole-policy"]
}
{
    "RoleName": "AppServerRole",
    "PolicyName": "AppServerRole-policy",
    "PolicyDocument": {
        "Version": "2012-10-17",
        "Statement": [
            {
                "Sid": "ReadManifests",
                "Effect": "Allow",
                "Action": ["s3:GetObject", "s3:ListBucket"],
                "Resource": [
                    "arn:aws:s3:::andes-cargo-shipment-docs",
                    "arn:aws:s3:::andes-cargo-shipment-docs/*"
                ]
            }
        ]
    }
}

Anota este dato con cuidado, lo vas a necesitar en la lección 5: AppServerRole y LambdaManifestProcessorRole tienen, letra por letra, la misma política de lectura de S3 — mismas acciones, mismo bucket completo, mismo alcance de recurso (andes-cargo-shipment-docs y andes-cargo-shipment-docs/*, sin distinguir entre el prefijo manifests/ que solo la Lambda procesa y el prefijo photos/ que la app de tracking muestra). La única diferencia real entre los dos roles es de quién puede asumirlos (Paso 1) y que LambdaManifestProcessorRole tiene, además, el permiso de escritura en DynamoDB que AppServerRole nunca tiene.


Paso 4 — awslocal s3api get-bucket-policy: qué protege el bucket

awslocal s3api get-bucket-policy --bucket andes-cargo-shipment-docs --query Policy --output text

Qué esperar (representativo — reconstruido de terraform-and-iac-guide, Módulo 6, lección 6):

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "DenyInsecureTransport",
            "Effect": "Deny",
            "Principal": "*",
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::andes-cargo-shipment-docs",
                "arn:aws:s3:::andes-cargo-shipment-docs/*"
            ],
            "Condition": {
                "Bool": { "aws:SecureTransport": "false" }
            }
        }
    ]
}

Un único statement, Deny con Principal: "*" — recuerda, de terraform-and-iac-guide, que esto no es una política pública: es exactamente lo opuesto, una restricción universal que le niega tráfico sin HTTPS a absolutamente todo el mundo, incluida la propia cuenta. La política de bucket resuelve una sola pregunta —¿el tráfico va cifrado?—; no resuelve, ni lo intenta, la pregunta de si el bucket podría volverse público algún día. Anota también esto para la lección 5: en ninguna parte de andes-cargo-infra/ existe un recurso aws_s3_bucket_public_access_blockmodules/s3-bucket/ no lo declara, por decisión de alcance explícita de terraform-and-iac-guide, Módulo 6.


Paso 5 — awslocal lambda get-policy: quién puede invocar la función

awslocal lambda get-policy --function-name process-shipment-manifest --query Policy --output text

Qué esperar (representativo — reconstruido de terraform-and-iac-guide, Módulo 7, lección 6):

{
    "Version": "2012-10-17",
    "Id": "default",
    "Statement": [
        {
            "Sid": "AllowS3InvokeAndesCargoBucket",
            "Effect": "Allow",
            "Principal": {"Service": "s3.amazonaws.com"},
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:us-east-1:000000000000:function:process-shipment-manifest",
            "Condition": {
                "StringEquals": {"AWS:SourceAccount": "000000000000"},
                "ArnLike": {"AWS:SourceArn": "arn:aws:s3:::andes-cargo-shipment-docs"}
            }
        }
    ]
}

Esta es la política basada en recursos de la función —distinta del rol de ejecución del Paso 3—: responde "¿quién puede invocarme desde afuera?", no "¿qué puedo hacer una vez que corro?". Un único principal autorizado (s3.amazonaws.com), acotado además por SourceAccount y SourceArn a exactamente este bucket, en esta cuenta. Sin sorpresas aquí: solo el trigger de S3 que ya conoces puede invocar process-shipment-manifest.


El inventario completo, en un solo diagrama

              SUPERFICIE DE ATAQUE DE ANDES CARGO — M1.4 (inventario real)
              Cuenta 000000000000 · región us-east-1

  ┌─────────────────────────┐        ┌─────────────────────────┐
  │ GitHub Actions runner    │        │  IDENTIDADES DE SERVICIO   │
  │ (ci.yml, apply.yml)      │        │                             │
  │ credenciales: test/test  │        │  LambdaManifestProcessorRole │
  │ (.secrets, gitignoreado) │        │  trust: lambda.amazonaws.com │
  └───────────┬──────────────┘        │  ├─ s3:GetObject/ListBucket  │
              │                       │  │  (bucket COMPLETO)        │
              │ terraform apply       │  └─ dynamodb:PutItem          │
              ▼                       │     (solo tabla Shipments)    │
  ┌─────────────────────────┐        │                             │
  │  andes-cargo-shipment-docs│◀──────┤  AppServerRole                │
  │  Versionado: Enabled      │  lee  │  trust: ec2.amazonaws.com     │
  │  Política: exige HTTPS    │       │  └─ s3:GetObject/ListBucket   │
  │  SIN public-access-block  │       │     (bucket COMPLETO — IGUAL │
  │  (ningún recurso lo       │       │      que el rol de Lambda)   │
  │   declara todavía)        │       └─────────────────────────┘
  └───────────┬──────────────┘
              │ evento s3:ObjectCreated:*
              │ filter_prefix: "manifests/"
              ▼
  ┌─────────────────────────┐        ┌─────────────────────────┐
  │  process-shipment-manifest│──────▶│  Shipments (DynamoDB)     │
  │  handler: .zip (SIN firma) │write │  PK: shipmentId            │
  │  invocable solo por:       │      │  billing: PAY_PER_REQUEST  │
  │  s3.amazonaws.com          │      │  SIN protección de destroy │
  │  (SourceArn/SourceAccount  │      └─────────────────────────┘
  │   acotados)                │
  └─────────────────────────┘

Este diagrama —con nombres reales, permisos reales, y las tres ausencias que ya anotaste (public-access-block, firma del .zip, protección de destroy)— es la entrada exacta de la lección 5.


Errores comunes

Concluir, del resultado vacío de list-attached-role-policies, que un rol no tiene ningún permiso (de lectura apresurada). Qué pasa: alguien corre el Paso 2, ve "AttachedPolicies": [], y asume que ese rol no puede hacer nada. Cómo detectarlo: si tu inventario de un rol se detiene en un resultado vacío de list-attached-role-policies, sin correr también list-role-policies. Cómo corregirlo: un rol puede tener permisos por dos caminos independientes —políticas administradas (list-attached-role-policies) y políticas inline (list-role-policies)—; un inventario real de superficie de ataque tiene que revisar los dos, siempre, sin asumir que uno implica el otro. Los dos roles de Andes Cargo son, de hecho, el caso exacto donde asumir eso te haría perder toda la información real.

Leer Effect: Deny con Principal: "*" como si fuera una política pública (de patrón, ya advertido en terraform-and-iac-guide). Qué pasa: alguien ve "Principal": "*" en la política del bucket del Paso 4 y, por reflejo, lo marca como un hallazgo de exposición pública. Cómo detectarlo: si tu nota sobre la política del bucket dice "público" sin haber leído el Effect. Cómo corregirlo: Effect: Deny combinado con Principal: "*" es una restricción universal, no un permiso universal —le quita acceso inseguro a todo el mundo, sin excepción—. Lo que haría pública una política sería Effect: Allow con un alcance sin restricción, un patrón completamente distinto que no aparece en ningún lugar de esta política.

Confundir la política del rol de ejecución (Paso 3) con la política basada en recursos de la función (Paso 5) (de capas). Qué pasa: alguien, después de leer get-role-policy, asume que ya sabe todo lo que hay que saber sobre quién puede invocar process-shipment-manifest. Cómo detectarlo: si tu inventario de la función no incluye por separado la salida de lambda get-policy. Cómo corregirlo: son dos capas de permisos completamente independientes —"qué puede hacer la función una vez que corre" (el rol de ejecución) y "quién tiene permiso de invocarla desde afuera" (la política basada en recursos)—, y un inventario completo necesita las dos, exactamente la misma distinción que terraform-and-iac-guide ya marcó en su Módulo 7, lección 6.


Ejercicios

Ejercicio 1 — Reconstruye el inventario completo de memoria. Sin mirar hacia atrás en esta lección, lista los cinco componentes del diagrama final (dos roles, un bucket, una función, una tabla) y, para cada uno, el dato de seguridad más importante que encontraste sobre él.

Ver solución

LambdaManifestProcessorRole: confía en lambda.amazonaws.com, lee el bucket completo y escribe (solo PutItem) en Shipments. AppServerRole: confía en ec2.amazonaws.com, lee el bucket completo con exactamente el mismo alcance que el rol de Lambda. andes-cargo-shipment-docs: exige HTTPS, pero no tiene ningún bloqueo de acceso público declarado. process-shipment-manifest: solo s3.amazonaws.com (acotado por SourceArn/SourceAccount) puede invocarla, y su .zip de despliegue no tiene ninguna firma que confirme que no fue modificado. Shipments: sin ninguna protección contra destrucción accidental o intencional. Si reconstruiste los cinco sin ayuda, tienes el inventario completo internalizado.

Ejercicio 2 — Explica por qué este inventario no incluye, todavía, ningún juicio de "esto es un riesgo" (de disciplina metodológica). Un compañero, viendo el diagrama final de esta lección, pregunta por qué no está ya ordenado de "más grave" a "menos grave". ¿Por qué esta lección se detiene antes de esa clasificación?

Ver solución

Porque mezclar la recolección de evidencia con el juicio sobre esa evidencia es exactamente el tipo de atajo que produce inventarios sesgados —uno tiende a notar primero lo que ya sospecha que es grave, y a pasar por alto lo que no encaja con esa sospecha inicial—. Separar los dos pasos, como hace esta lección, garantiza que el inventario sea completo antes de que empiece cualquier priorización. La lección 5 aplica el juicio —STRIDE— sobre este mismo inventario, ya completo y sin sesgo de haber sido recolectado "buscando" confirmar una idea previa.

Ejercicio 3 — Diseña la quinta pregunta que este inventario todavía no contestó. Los cuatro comandos de esta lección responden "quién existe", "qué permisos tiene cada rol", "qué protege al bucket" y "quién puede invocar la función". Propone un quinto comando awslocal —de cualquier servicio de Andes Cargo— que agregaría una pieza real al inventario, y explica qué pregunta contestaría.

Ver solución

Un ejemplo sólido: awslocal s3api get-bucket-versioning --bucket andes-cargo-shipment-docs, que confirma si el versionado está activo —una pieza relevante para Tampering: si alguien modifica o borra un objeto del bucket, ¿queda una versión anterior recuperable, o la modificación es irreversible?—. Otro ejemplo igual de válido: awslocal dynamodb describe-table --table-name Shipments, para confirmar el estado y la clave primaria de la tabla antes de analizar el riesgo de Denial of service sobre ella en la lección 5. Cualquier comando que agregue un dato verificable —no una suposición— al inventario cuenta como respuesta correcta.


Resumen y siguiente paso

En esta lección construiste el inventario real de la superficie de ataque de Andes Cargo con cuatro comandos —iam list-roles, iam list-attached-role-policies/list-role-policies, s3api get-bucket-policy, lambda get-policy—, todos representativos por la ausencia de LOCALSTACK_AUTH_TOKEN en este entorno, cada uno reconstruido campo por campo de la infraestructura real que terraform-and-iac-guide ya aplicó y verificó. Encontraste tres datos que van a gobernar la lección siguiente: AppServerRole y LambdaManifestProcessorRole comparten el mismo alcance de lectura sobre el bucket completo; andes-cargo-shipment-docs no tiene ningún bloqueo de acceso público declarado; y el .zip de process-shipment-manifest no tiene ninguna firma.

Antes de avanzar deberías poder: correr de memoria los cuatro comandos de esta lección, en el orden correcto; explicar la diferencia entre una política administrada y una inline, y por qué un inventario real necesita revisar las dos; y nombrar los tres hallazgos que quedaron pendientes de esta lección para la siguiente.

La lección 5 toma este mismo inventario, sin agregar ningún dato nuevo, y le aplica las seis categorías de STRIDE —una por una, con evidencia, no con sospecha—.

Recursos

  1. AWS CLI — iam list-roles — referencia completa del comando del Paso 1.
  2. AWS CLI — iam list-attached-role-policies y iam list-role-policies — la distinción entre políticas administradas e inline, central en el Paso 2 y 3 de esta lección.
  3. AWS CLI — s3api get-bucket-policy — referencia completa del comando del Paso 4.
  4. AWS CLI — lambda get-policy — referencia completa del comando del Paso 5, incluida la distinción entre política basada en recursos y rol de ejecución.
  5. terraform-and-iac-guide, Módulos 6 y 7 — la fuente completa de cada valor reconstruido en esta lección: iam.tf, s3.tf, lambda.tf, dynamodb.tf de andes-cargo-infra/.