Módulo 7: Detective Vs Preventive Guardrails

7. Manos a la obra: una alarma sobre un evento de IAM sensible

Descripción

CloudTrail (lección 5) responde "¿quién hizo qué, cuándo?" — pero solo si alguien va a buscar esa respuesta, consultando lookup-events de forma manual. Esta lección cierra ese hueco: una regla de Amazon EventBridge que reacciona, en casi tiempo real, cuando el trail de la lección 5 registra exactamente dos eventos de alto riesgo —iam:AttachRolePolicy y iam:CreateAccessKey— y notifica a un tema de SNS. Es la diferencia entre un guardrail detectivo pasivo (el registro existe, si alguien lo consulta) y uno activo (el registro te avisa, sin que nadie tenga que preguntarle nada).

Conexión con el módulo

Esta lección depende directamente de la lección 5: sin include_global_service_events = true en el trail, ningún evento de IAM llegaría nunca a EventBridge, y esta alarma nunca tendría nada que evaluar. Es, además, la pieza que responde con más precisión al vector de riesgo que VALIDACION.md citó desde el Módulo 1 —"el vector dominante de 2026 ya no es el humano descuidado sino el agente con credenciales"—: iam:AttachRolePolicy y iam:CreateAccessKey son, exactamente, los dos tipos de llamada que un agente con credenciales comprometidas o mal alcanzadas usaría para escalar privilegios o crear una vía de acceso persistente.


Por qué estos dos eventos, específicamente

De todos los eventos posibles de IAM, esta lección elige dos, no al azar:

  • iam:AttachRolePolicy — adjunta una política administrada a un rol existente. Es, en los hechos, exactamente el mecanismo que convertiría a AppServerRole —hoy acotado a photos/* (Módulo 2, lección 7) y protegido por el boundary de la lección 3— en un rol con permisos administrativos completos, con una sola llamada. Ningún control preventivo de esta guía impide, por sí solo, que esta llamada suceda contra una cuenta real fuera del alcance del boundary; una alarma que la detecte de inmediato es el complemento necesario.
  • iam:CreateAccessKey — genera un par de credenciales de acceso de larga vida para un usuario IAM. Es, con precisión, el antipatrón que el Módulo 2 de esta guía dedicó un módulo entero a reemplazar con OIDC — una llamada exitosa a esta acción, fuera de un flujo esperado, es exactamente el momento en que una credencial de larga vida nueva y no documentada empieza a existir.

Ambos eventos comparten una propiedad que los hace ideales para una alarma: son poco frecuentes en operación normal. A diferencia de s3:GetObject, que sucede cientos de veces por hora en cualquier sistema activo, AttachRolePolicy y CreateAccessKey deberían suceder, en un sistema bien gobernado, casi nunca fuera de un cambio deliberado de infraestructura vía Terraform — lo que hace que cualquier ocurrencia inesperada sea, por definición, una señal de alto valor, no ruido.


Paso 1 — El tema de SNS que recibe la notificación

En un archivo nuevo, alarm.tf:

resource "aws_sns_topic" "security_alerts" {
  name = "andes-cargo-security-alerts"
  tags = local.common_tags
}

data "aws_iam_policy_document" "security_alerts_topic_policy" {
  statement {
    sid    = "AllowEventBridgePublish"
    effect = "Allow"

    principals {
      type        = "Service"
      identifiers = ["events.amazonaws.com"]
    }

    actions   = ["sns:Publish"]
    resources = [aws_sns_topic.security_alerts.arn]
  }
}

resource "aws_sns_topic_policy" "security_alerts" {
  arn    = aws_sns_topic.security_alerts.arn
  policy = data.aws_iam_policy_document.security_alerts_topic_policy.json
}

El mismo patrón exacto que ya viste en cloudtrail.tf (lección 5): un recurso, más una política que le da permiso explícito a otro servicio de AWSevents.amazonaws.com, no un usuario ni un rol humano— de interactuar con él. Sin esta política, EventBridge no tendría permiso de publicar en este tema, sin importar qué tan bien escrita estuviera la regla del Paso 2.


Paso 2 — La regla de EventBridge: el patrón que decide qué es "sensible"

resource "aws_cloudwatch_event_rule" "sensitive_iam_events" {
  name        = "andes-cargo-sensitive-iam-events"
  description = "Fires when CloudTrail records iam:AttachRolePolicy or iam:CreateAccessKey."

  event_pattern = jsonencode({
    source      = ["aws.iam"]
    detail-type = ["AWS API Call via CloudTrail"]
    detail = {
      eventSource = ["iam.amazonaws.com"]
      eventName   = ["AttachRolePolicy", "CreateAccessKey"]
    }
  })

  tags = local.common_tags
}

resource "aws_cloudwatch_event_target" "sensitive_iam_events_to_sns" {
  rule      = aws_cloudwatch_event_rule.sensitive_iam_events.name
  target_id = "andes-cargo-security-alerts"
  arn       = aws_sns_topic.security_alerts.arn
}

El recurso vive bajo el nombre aws_cloudwatch_event_rule —un resabio histórico del nombre original del servicio (CloudWatch Events), que hoy se llama Amazon EventBridge—, pero el event_pattern sigue exactamente la forma documentada oficialmente por AWS para reaccionar a llamadas de API registradas por CloudTrail:

  • source = ["aws.iam"] — el mismo patrón que el tutorial oficial de AWS usa para EC2 (aws.ec2), aquí aplicado al servicio que esta alarma vigila.
  • detail-type = ["AWS API Call via CloudTrail"] — el valor literal, documentado oficialmente, que EventBridge asigna a cualquier evento que provenga de una llamada de API capturada por un trail activo — sin este detail-type exacto, la regla nunca coincidiría con nada.
  • detail.eventSource = ["iam.amazonaws.com"] y detail.eventName = ["AttachRolePolicy", "CreateAccessKey"] — el filtro fino: de todos los eventos de IAM posibles, solo estos dos nombres específicos de operación disparan la regla. Una lista, no un solo valor: EventBridge evalúa cada eventName de la lista como una alternativa válida (comportamiento OR), el mismo patrón que ya viste con values en una condición StringLike de una trust policy (Módulo 2, lección 5).

Un prerequisito que ya construiste, no uno nuevo: esta regla depende de que el trail de la lección 5 esté activo y entregando eventos al bus de eventos por defecto — la propia documentación de EventBridge lo confirma: "When an AWS service in your account emits an event, it always goes to your account's default event bus", y CloudTrail entrega a EventBridge exactamente como una de sus opciones de destino de trail, ya declarada en la lección 5.


Paso 3 — fmt, validate: el motor real

terraform fmt -recursive -check
terraform validate

Qué esperar (literal, ejecutado para escribir esta lección):

Success! The configuration is valid.

Paso 4 — El plan: los cuatro recursos nuevos

terraform plan

Qué esperar (literal, ejecutado para escribir esta lección — se muestra la parte de este plan que corresponde a alarm.tf; el plan completo de este punto de la guía suma, además, el boundary de la lección 3 y el trail de la lección 5, doce recursos en total):

  # aws_cloudwatch_event_rule.sensitive_iam_events will be created
  + resource "aws_cloudwatch_event_rule" "sensitive_iam_events" {
      + arn            = (known after apply)
      + description    = "Fires when CloudTrail records iam:AttachRolePolicy or iam:CreateAccessKey."
      + event_bus_name = "default"
      + event_pattern  = jsonencode(
            {
              + detail      = {
                  + eventName   = [
                      + "AttachRolePolicy",
                      + "CreateAccessKey",
                    ]
                  + eventSource = [
                      + "iam.amazonaws.com",
                    ]
                }
              + detail-type = [
                  + "AWS API Call via CloudTrail",
                ]
              + source      = [
                  + "aws.iam",
                ]
            }
        )
      + id             = (known after apply)
      + name           = "andes-cargo-sensitive-iam-events"
      + region         = "us-east-1"
      + tags           = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
      + tags_all       = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
    }

  # aws_cloudwatch_event_target.sensitive_iam_events_to_sns will be created
  + resource "aws_cloudwatch_event_target" "sensitive_iam_events_to_sns" {
      + arn            = (known after apply)
      + event_bus_name = "default"
      + id             = (known after apply)
      + region         = "us-east-1"
      + rule           = "andes-cargo-sensitive-iam-events"
      + target_id      = "andes-cargo-security-alerts"
    }

  # aws_sns_topic.security_alerts will be created
  + resource "aws_sns_topic" "security_alerts" {
      + arn                         = (known after apply)
      + id                          = (known after apply)
      + name                        = "andes-cargo-security-alerts"
      + region                      = "us-east-1"
      + tags                        = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
      + tags_all                    = {
          + "Environment" = "dev"
          + "ManagedBy"   = "terraform"
          + "Project"     = "andes-cargo"
        }
      ... (otros atributos "known after apply", omitidos por brevedad)
    }

  # aws_sns_topic_policy.security_alerts will be created
  + resource "aws_sns_topic_policy" "security_alerts" {
      + arn    = (known after apply)
      + id     = (known after apply)
      + owner  = (known after apply)
      + policy = (known after apply)
      + region = "us-east-1"
    }

Fíjate en event_bus_name = "default", literal — la regla se adjunta al mismo bus por defecto donde CloudTrail entrega sus eventos automáticamente, sin necesitar declarar ningún bus propio. Y fíjate también en policy = (known after apply) dentro de aws_sns_topic_policy.security_alerts — depende de data.aws_iam_policy_document.security_alerts_topic_policy, que a su vez depende del ARN del tema de SNS, todavía no creado en el momento del plan: la tercera vez, en este mismo módulo, que ves exactamente este mecanismo de dependencia entre recursos nuevos del mismo apply.


Paso 5 — El intento real de confirmar el patrón (representativo)

Igual que en la lección 5, esto no es una descripción de antemano — es lo que produjo, literalmente, intentar estos comandos en este entorno de escritura, sin LOCALSTACK_AUTH_TOKEN exportado:

awslocal events describe-rule --name andes-cargo-sensitive-iam-events

Qué pasó, literal, al intentarlo:

aws: [ERROR]: Could not connect to the endpoint URL: "http://localhost:4566/"
awslocal iam attach-role-policy \
  --role-name AppServerRole \
  --policy-arn arn:aws:iam::aws:policy/ReadOnlyAccess

Qué pasó, literal, al intentarlo:

aws: [ERROR]: Could not connect to the endpoint URL: "http://localhost:4566/"

La misma causa raíz de la lección 5, confirmada una vez más: sin ningún contenedor de LocalStack corriendo, cualquier awslocal, sin importar el servicio, falla con el mismo error de conexión. Lo que este intento sí confirma es que el patrón está declarado correctamente —los cuatro recursos del Paso 2 se aplicarían, contra una cuenta AWS real o contra LocalStack con el contenedor corriendo, exactamente como el plan del Paso 4 lo describió—; lo que no puede confirmar, en este entorno específico, es que el evento AttachRolePolicy de esta última llamada efectivamente dispararía la regla y produciría una notificación real en el tema de SNS.


Errores comunes

Escribir detail-type = ["AWS API Call via CloudTrail"] con un guion bajo (detail_type) en vez de un guion medio, dentro del jsonencode (de sintaxis JSON, no HCL). Qué pasa: alguien, acostumbrado a que HCL usa snake_case para casi todos sus argumentos, escribe detail_type dentro del objeto que pasa a jsonencode. Cómo detectarlo: terraform validate no detecta este error —dentro de jsonencode({...}), las claves son strings arbitrarios desde la perspectiva de Terraform, no argumentos con nombre fijo—, pero la regla, aplicada contra una cuenta real, nunca coincidiría con ningún evento, porque detail-type (con guion medio) es el nombre de campo exacto que EventBridge espera en el patrón, documentado así oficialmente. Cómo corregirlo: dentro de un event_pattern, las claves siguen la convención de nombres de AWS, no la de HCL — copiar el patrón exacto del Paso 2, con el guion medio preservado tal cual, es la forma correcta.

Olvidar la política del tema de SNS (Paso 1), y encontrarse con eventos que "se pierden" sin ningún error visible (de permisos silenciosos). Qué pasa: alguien declara aws_sns_topic y el aws_cloudwatch_event_target que apunta a él, pero omite aws_sns_topic_policy. Cómo detectarlo: terraform plan/apply no muestran ningún error —ambos recursos son sintácticamente válidos e independientes—, pero contra una cuenta real, EventBridge no tendría permiso de publicar en un tema sin política que se lo autorice explícitamente; los eventos que la regla capture nunca llegarían a generar ninguna notificación, sin ningún mensaje de error que lo señale en el momento. Cómo corregirlo: siempre que un servicio de AWS (events.amazonaws.com, en este caso) necesite interactuar con un recurso de otro servicio, ese recurso necesita una política de recurso que se lo permita explícitamente — el mismo patrón, ya visto tres veces en este módulo (el boundary de IAM, la política del bucket de CloudTrail, y esta política de SNS).

Confundir esta alarma con un guardrail preventivo, porque "reacciona rápido" (de categoría, retomado de la lección 1). Qué pasa: alguien, viendo que esta alarma puede notificar en segundos después de que CloudTrail registre el evento, la clasifica como preventiva. Cómo detectarlo: si tu clasificación de esta pieza, en el proyecto de la lección 8, dice "preventivo". Cómo corregirlo: la velocidad de reacción no cambia la categoría — iam:AttachRolePolicy ya sucedió en el momento en que esta alarma dispara; nada en esta cadena (trail → EventBridge → SNS) impidió que la llamada se ejecutara. Es, con precisión, un guardrail detectivo rápido, no uno preventivo — la diferencia entre "detectar en segundos" y "prevenir" sigue siendo la misma que gobierna este módulo entero desde la lección 1.


Ejercicios

Ejercicio 1 — Predice qué pasaría si alguien llamara iam:DetachRolePolicy en vez de iam:AttachRolePolicy. Usando el event_pattern exacto del Paso 2, ¿dispararía esta regla si alguien remueve una política de un rol, en vez de agregarla?

Ver solución

No dispararía. El event_pattern del Paso 2 lista, explícitamente, solo dos valores en eventName: "AttachRolePolicy" y "CreateAccessKey". DetachRolePolicy es un nombre de evento distinto, que no aparece en esa lista — EventBridge evalúa coincidencia exacta contra los valores declarados, no una categoría genérica de "cambios a políticas de rol". Si Andes Cargo quisiera capturar también remociones de política, tendría que agregar "DetachRolePolicy" explícitamente a la lista de eventName, el mismo mecanismo de lista que ya usaste para agregar un segundo valor (CreateAccessKey) junto al primero.

Ejercicio 2 — Explica por qué esta regla depende de la lección 5, sin repetir la palabra "prerequisito". En dos o tres frases, explica la cadena técnica exacta que hace que esta alarma no funcione sin el trail de la lección 5.

Ver solución

Una explicación completa suena, más o menos, así: "Esta regla de EventBridge evalúa eventos con detail-type: 'AWS API Call via CloudTrail' — ese tipo de evento específico solo existe si hay un trail activo que capture llamadas de API y las entregue a EventBridge. Sin el aws_cloudtrail de la lección 5, con include_global_service_events = true, ninguna llamada a iam:AttachRolePolicy generaría jamás un evento de ese tipo en el bus por defecto — la regla existiría, sintácticamente correcta, pero nunca tendría absolutamente nada que evaluar, porque la fuente de eventos que la alimenta nunca llegaría a producirlos."

Ejercicio 3 — Diseña un tercer evento sensible que agregarías a esta regla, con su justificación. Basándote en el criterio de esta lección (eventos de alto riesgo, poco frecuentes en operación normal), propone un tercer valor de eventName que agregarías a la lista del Paso 2, de cualquier servicio de AWS, y justifica por qué calificaría con el mismo criterio.

Ver solución

Una respuesta sólida, entre varias válidas: "PutBucketPolicy" (de S3) — un cambio a la política de un bucket es, exactamente como AttachRolePolicy, una llamada poco frecuente en operación normal (los buckets de Andes Cargo no cambian su política todos los días) y de alto riesgo (es, literalmente, el mecanismo que podría hacer público el bucket andes-cargo-shipment-docs, el hallazgo TM-04 de THREAT-MODEL.md). Para agregarlo, haría falta cambiar source a una lista que incluya también "aws.s3" (no solo "aws.iam"), y agregar el eventSource/eventName correspondientes dentro de detail — el mismo patrón de lista OR que ya usa esta lección para dos eventos de IAM, extendido a un servicio adicional. Cualquier evento que cumpla el mismo criterio —poco frecuente, alto impacto si sale mal— es una respuesta válida.


Resumen y siguiente paso

En esta lección construiste la última pieza nueva de HCL de este módulo: un tema de SNS con su política, y una regla de EventBridge que reacciona a exactamente dos eventos de IAM de alto riesgo —iam:AttachRolePolicy y iam:CreateAccessKey—, notificando en casi tiempo real en vez de esperar a que alguien consulte lookup-events manualmente. Corriste fmt/validate/plan de verdad sobre los cuatro recursos, confirmaste la dependencia con el trail de la lección 5, e intentaste, sin red de seguridad, confirmar el patrón contra este entorno — con el mismo resultado honesto de siempre: el HCL es real, el disparo queda representativo por la ausencia de un token en este entorno específico.

Antes de avanzar deberías poder: explicar por qué estos dos eventos específicos, y no cualquier evento de IAM, justifican una alarma; escribir de memoria la forma del event_pattern (source, detail-type, detail.eventSource, detail.eventName); y explicar, sin dudar, por qué esta pieza es detectiva y no preventiva, sin importar qué tan rápido reaccione.

La lección 8, el proyecto que cierra este módulo, reúne las ocho piezas —dos preventivas construidas, tres detectivas (una construida parcialmente, dos nombradas), y una nombrada de escala mayor— en una única matriz que las clasifica sin ninguna ambigüedad.

Recursos

  1. AWS Docs — Tutorial: Create an EventBridge rule that reacts to AWS API calls via CloudTrail — el tutorial oficial cuyo patrón de evento (source, "AWS API Call via CloudTrail") sigue esta lección, incluida la confirmación de que los eventos van siempre al bus por defecto de la cuenta.
  2. Terraform Registry — aws_cloudwatch_event_rule — referencia completa del recurso del Paso 2.
  3. Terraform Registry — aws_sns_topic_policy — referencia completa del recurso del Paso 1.
  4. AWS Docs — AWS CloudTrail User Guide: Trails — confirma la entrega opcional de un trail a Amazon EventBridge, el prerequisito de esta lección.
  5. Este curso, Módulo 7, lección 5 — el trail del que depende, técnicamente, la regla de esta lección.
  6. src/paths/aws-cloud-ecosystem/VALIDACION.md — la cita sobre el vector de riesgo de 2026 (el agente con credenciales) que justifica, con evidencia de mercado, por qué estos dos eventos específicos merecen una alarma dedicada.