Saltar al contenido
Agustina Fassina
Volver a todos los posts
Postmortem3 min de lectura

La access key que tenía AdministratorAccess

Una IAM access key terminó en un gist público con permisos de admin completos. La rotamos en once minutos. Lo que asustó fue cuánto tiempo estuvo ahí.

Una cinta transportadora de cajas etiquetadas pasando por tres arcos de control hacia una lámpara verde de verificación

Un contratista pegó un ejemplo de curl en un gist público para pedir ayuda con un upload a S3. El ejemplo incluía AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY y un one-liner que funcionaba. Lo que el gist no decía era que la key pertenecía a una cuenta de servicio llamada deploy-bot, y que tenía AdministratorAccess porque alguien había estado “solo probando algo” tres meses antes.

GitGuardian nos mandó un mail a las 02:14 un martes. A las 02:25 la key estaba muerta. A las 06:00 sabíamos el daño: nada. A las 10:00 sabíamos por qué eso fue en gran parte suerte.

Cómo llegó ahí

El contratista no exfiltró nada. Copió un snippet que funcionaba desde una página interna del wiki escrita seis meses atrás, cuando crearon deploy-bot. La página del wiki estaba mal el día que la escribieron y nadie la volvió a abrir.

La key en sí estaba bien al crearla: scope a push en ECR y deploy en ECS en un cluster. Después un ingeniero de plataforma le attachó AdministratorAccess durante un deploy de viernes que iba mal, juró que lo sacaría, abrió un ticket, cerró el ticket cuando el deploy salió verde, y nunca más tocó IAM.

El gist estuvo público cuatro días antes de que el scanner lo encontrara. No sabemos si alguien más lo vio.

Cómo se vieron esos once minutos de respuesta

Este es el orden que importó, no el que se sintió satisfactorio:

  1. Desactivar la key (no borrar todavía; querés el trail).
  2. CloudTrail lookup sobre AccessKeyId de los últimos 90 días. Filtrar primero sin errorCode, después leer todo.
  3. Asumir compromiso en todo lo que el principal tocó: buckets S3, env vars de Lambda, paths de Secrets Manager. Rotar lo que era legible, no solo lo que parece sospechoso.
  4. Emitir una key nueva solo cuando entendés el blast radius. Un reemplazo apurado con la misma policy repite el problema.

La ventana de CloudTrail mostró llamadas Describe* desde un ISP residencial en otro país, después nada. Pudo ser un scanner. Pudo ser alguien cuidadoso. Tratamos ambos igual.

Qué deberíamos haber tenido antes de las 02:14

Control Qué habría cambiado
Sin keys de larga duración en humanos o bots OIDC desde CI a AWS. El gist hubiera tenido un token que expira en minutos
Permission boundary en deploy-bot El attach de admin hubiera fallado en la API
Alarma por edad de access key La key tenía 94 días; no teníamos alerta
Secret scanning en wiki interno El wiki fue el leak real; el gist fue el amplificador

La policy IAM que quedó:

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Deny",
      "Action": "iam:*",
      "Resource": "*",
      "Condition": {
        "StringNotEquals": {
          "aws:PrincipalTag/role": "iam-admin"
        }
      }
    }
  ]
}

Un permission boundary en cada principal no-admin. deploy-bot no puede darse admin aunque alguien le attaché una policy más amplia por error.

La parte que me sigue dando vueltas

Celebramos la rotación en once minutos. Seguridad amó el runbook. Dirección amó la slide del timeline.

La falla real tenía tres meses y era invisible: una policy de admin que nadie recordaba haber attachado, en una key que nadie recordaba haber creado, copiada desde una página que nadie recordaba haber escrito. El gist fue solo el momento en que la deuda se hizo visible.

Rotá rápido. Pero el trabajo que previene el próximo es higiene aburrida de IAM: boundaries, sin keys de larga duración, y tratar AdministratorAccess como un hacha de bomberos. Detrás del vidrio, logueada, y devuelta al instante.