En 04-02 ciframos las copias de seguridad de mercadofresco-pedidos con la clave
alias/mercadofresco-datos, y dijimos que KMS registra escrupulosamente cada operación criptográfica.
En 04-03 guardamos la contraseña de mfadmin en el secreto
mercadofresco/produccion/rds/mfadmin y dijimos que cada lectura queda anotada. En 04-01 creamos el
rol rol-mercadofresco-tienda y dijimos que cada AssumeRole deja rastro.
Ninguna de esas tres cosas es mentira. Todas están registradas. Y nadie las ha mirado nunca.
Ese registro se llama AWS CloudTrail, y responde a una pregunta que ni CloudWatch ni X-Ray pueden
contestar. CloudWatch sabe lo que tu aplicación dice de sí misma. X-Ray sabe por dónde pasó una
petición de un cliente. CloudTrail sabe quién llamó a la API de AWS, cuándo, desde dónde, con qué
parámetros y con qué resultado. Es la diferencia entre saber que la tienda va lenta y saber que
alguien, a las 03:42 de un martes, desde una IP de un país donde no tienes oficinas, llamó a
kms:Decrypt sobre la copia de la base de datos.
Esta lección monta el trail trail-mercadofresco, aprende a leer un evento real, y contesta por fin a
la tercera pregunta que dejó abierta el módulo 4.
Aviso de cumplimiento. Los registros de auditoría suelen tener requisitos legales de conservación. Según el sector y el país, pueden ir de 6 meses a 10 años, y en algunos casos deben ser inalterables y estar en custodia separada. Este material es didáctico: las decisiones de retención, inmutabilidad y acceso a los registros de auditoría de un sistema real las debe validar un profesional de cumplimiento o el asesor legal de tu organización. No las tomes tú a partir de un curso.
Contenido
- Qué registra CloudTrail y qué no
- CloudTrail frente a CloudWatch Logs
- El historial de eventos de 90 días, gratuito
- Crear el trail
trail-mercadofresco - El bucket que nadie debe poder borrar
- Validación de integridad de los ficheros
- Anatomía de un evento: el JSON comentado
userIdentity: los seis tipos y cómo se leen- Quién descifró la última copia
- Eventos de gestión frente a eventos de datos
- Eventos de Insights
- Trails multirregión y de organización
- Enviar el trail a CloudWatch Logs
- Filtros de métricas y alarmas de seguridad
- Consultar con Athena
- Las consultas que responden preguntas reales
- CloudTrail Lake
- Investigación de un incidente, paso a paso
- Relación con IAM Access Analyzer
- Retención, coste y limpieza
Qué registra CloudTrail y qué no
CloudTrail registra llamadas a la API de AWS. Todas. Da igual que las haga una persona en la consola, la CLI, un SDK, un servicio de AWS actuando en tu nombre o una función Lambda: por debajo todo son llamadas HTTPS firmadas a las API de AWS, y CloudTrail las ve todas.
Lo que no registra:
| No registra | Ejemplo | Dónde está |
|---|---|---|
| Lo que hace tu aplicación por dentro | pedido 48213 confirmado |
CloudWatch Logs (05-01) |
| El tráfico HTTP de tus clientes | GET /buscar?q=tomate |
Registros del ALB / CloudFront |
| Consultas SQL a tu base de datos | SELECT * FROM pedidos |
Registros de RDS |
| Tráfico de red entre instancias | Paquetes TCP | VPC Flow Logs (03-01) |
| Peticiones a objetos de S3 | GET productos/tomate.jpg |
Eventos de datos (hay que activarlos) |
| Contenido de un secreto | El valor de la contraseña | Nunca se registra |
Esa última fila importa: CloudTrail registra que alguien llamó a GetSecretValue sobre
mercadofresco/produccion/rds/mfadmin, pero no registra la contraseña. Los campos sensibles se
omiten o se ofuscan por diseño. Lo mismo con kms:Decrypt: registra la llamada y el identificador de
clave, no el texto claro.
Y una propiedad estructural que hay que entender: CloudTrail está siempre encendido. No es algo que se «activa»: los últimos 90 días de eventos de gestión están disponibles gratis en tu cuenta desde el primer día, incluso si no has creado ningún trail. Lo que se activa es la persistencia.
CloudTrail frente a CloudWatch Logs
Es la confusión número uno de este módulo, y merece una tabla:
| CloudTrail | CloudWatch Logs | |
|---|---|---|
| Registra | Llamadas a la API de AWS | Lo que escribe tu aplicación |
| Origen del dato | El plano de control de AWS | Tu código, el agente, los servicios |
| Pregunta que responde | ¿Quién hizo qué? | ¿Qué pasó dentro? |
| Ejemplo | mercadofresco-admin borró el bucket |
ERROR pago rechazado pedido 48213 |
| Ámbito | Cuenta (y organización) | Región, grupo de registros |
| Activo por defecto | Sí, 90 días gratis | Solo si publicas |
| Formato | JSON estructurado y fijo | Lo que tú escribas |
| Retención | 90 días / lo que dure el bucket | Lo que configures |
| Se manipula | Muy difícil (con las protecciones) | Fácil, si tienes permisos |
| Uso principal | Auditoría, forense, cumplimiento | Operación, depuración |
Un caso concreto para fijarlo. Alguien borra el bucket mercadofresco-registros-web:
- CloudWatch Logs: nada. Tu aplicación no ha escrito nada porque no ha sido tu aplicación.
- CloudTrail: un evento
DeleteBucket, con la identidad que lo hizo, la hora exacta, la IP de origen, el agente de usuario (aws-cli/2.15.0oconsole.amazonaws.com), y si se hizo con MFA.
Y el caso inverso. La tienda devuelve 500 al confirmar un pedido:
- CloudTrail: nada. Nadie llamó a ninguna API de AWS de forma anómala.
- CloudWatch Logs: la traza completa del error.
No compiten: cubren universos distintos.
El historial de eventos de 90 días, gratuito
Antes de crear nada, hay algo que ya funciona:
# Los ultimos eventos de escritura de la region
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=ReadOnly,AttributeValue=false \
--max-results 20 \
--query 'Events[].[EventTime,Username,EventName,EventSource]' \
--output table \
--profile mercadofresco-dev --region eu-west-1
# Todo lo que ha hecho un usuario concreto
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=Username,AttributeValue=mercadofresco-admin \
--start-time 2026-07-01T00:00:00Z \
--profile mercadofresco-dev --region eu-west-1
# Todas las llamadas a un evento concreto
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--profile mercadofresco-dev --region eu-west-1Los atributos de búsqueda disponibles son limitados y hay que conocerlos, porque es lo que se puede
hacer sin infraestructura: EventId, EventName, EventSource, ReadOnly, ResourceName,
ResourceType, Username, AccessKeyId.
Las cinco limitaciones del historial gratuito, que son exactamente las razones para crear un trail:
- Solo 90 días. Una investigación de un incidente detectado tarde se queda sin datos.
- Solo eventos de gestión. No hay eventos de datos (accesos a objetos de S3, invocaciones de Lambda).
- Un solo atributo de búsqueda por consulta. No puedes cruzar «este usuario» y «esta acción».
- No es exportable ni consultable con SQL. No hay análisis serio posible.
- No es inmutable ni verificable. No sirve como prueba en una auditoría formal.
Crear el trail trail-mercadofresco
Un trail persiste los eventos en un bucket de S3, indefinidamente, y permite todo lo anterior.
Paso 1: el bucket dedicado.
aws s3api create-bucket \
--bucket mercadofresco-auditoria-cloudtrail \
--region eu-west-1 \
--create-bucket-configuration LocationConstraint=eu-west-1 \
--profile mercadofresco-dev
# Bloqueo total de acceso publico (02-03)
aws s3api put-public-access-block \
--bucket mercadofresco-auditoria-cloudtrail \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true" \
--profile mercadofresco-dev
# Versionado: imprescindible para Object Lock y para recuperar borrados
aws s3api put-bucket-versioning \
--bucket mercadofresco-auditoria-cloudtrail \
--versioning-configuration Status=Enabled \
--profile mercadofresco-dev
# Cifrado con la clave gestionada por MercadoFresco (04-02)
aws s3api put-bucket-encryption \
--bucket mercadofresco-auditoria-cloudtrail \
--server-side-encryption-configuration '{
"Rules": [{
"ApplyServerSideEncryptionByDefault": {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos"
},
"BucketKeyEnabled": true
}]
}' \
--profile mercadofresco-devEse BucketKeyEnabled: true no es cosmético: reduce hasta un 99 % las llamadas a KMS —y su coste—
cuando se escriben miles de objetos pequeños, que es exactamente lo que hace CloudTrail. Lo vimos en
04-02.
Paso 2: la política del bucket. CloudTrail necesita permiso para escribir:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudTrailComprobarAcl",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:GetBucketAcl",
"Resource": "arn:aws:s3:::mercadofresco-auditoria-cloudtrail",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco"
}
}
},
{
"Sid": "CloudTrailEscribir",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:PutObject",
"Resource": "arn:aws:s3:::mercadofresco-auditoria-cloudtrail/AWSLogs/111122223333/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceArn": "arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco"
}
}
},
{
"Sid": "DenegarBorradoATodoElMundo",
"Effect": "Deny",
"Principal": "*",
"Action": [
"s3:DeleteObject",
"s3:DeleteObjectVersion",
"s3:PutBucketPolicy",
"s3:DeleteBucketPolicy",
"s3:PutLifecycleConfiguration"
],
"Resource": [
"arn:aws:s3:::mercadofresco-auditoria-cloudtrail",
"arn:aws:s3:::mercadofresco-auditoria-cloudtrail/*"
],
"Condition": {
"ArnNotEquals": {
"aws:PrincipalArn": "arn:aws:iam::111122223333:role/rol-custodia-auditoria"
}
}
},
{
"Sid": "DenegarTrafficoSinTLS",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"arn:aws:s3:::mercadofresco-auditoria-cloudtrail",
"arn:aws:s3:::mercadofresco-auditoria-cloudtrail/*"
],
"Condition": { "Bool": { "aws:SecureTransport": "false" } }
}
]
}Las condiciones aws:SourceArn son importantes y no siempre se ponen: sin ellas, en teoría un trail de
otra cuenta podría escribir en tu bucket (el problema del «diputado confuso» que vimos en 04-01).
Paso 3: el trail.
aws cloudtrail create-trail \
--name trail-mercadofresco \
--s3-bucket-name mercadofresco-auditoria-cloudtrail \
--is-multi-region-trail \
--include-global-service-events \
--enable-log-file-validation \
--kms-key-id arn:aws:kms:eu-west-1:111122223333:alias/mercadofresco-datos \
--cloud-watch-logs-log-group-arn arn:aws:logs:eu-west-1:111122223333:log-group:/aws/cloudtrail/mercadofresco:* \
--cloud-watch-logs-role-arn arn:aws:iam::111122223333:role/rol-cloudtrail-a-logs \
--tags-list Key=Proyecto,Value=mercadofresco Key=Entorno,Value=produccion \
Key=Componente,Value=auditoria Key=Propietario,Value=marta \
Key=CentroCoste,Value=plataforma \
--profile mercadofresco-dev --region eu-west-1
# Y ARRANCARLO: create-trail NO lo inicia.
aws cloudtrail start-logging \
--name trail-mercadofresco \
--profile mercadofresco-dev --region eu-west-1El error del año: create-trail crea el trail pero no empieza a registrar. Hay que llamar a
start-logging. Hay cuentas con trails creados hace años que nunca han grabado una línea. Compruébalo
siempre:
aws cloudtrail get-trail-status --name trail-mercadofresco \
--query '[IsLogging,LatestDeliveryTime,LatestDeliveryError]' \
--profile mercadofresco-dev --region eu-west-1Las cuatro banderas del comando, explicadas:
| Bandera | Qué hace | ¿Por qué? |
|---|---|---|
--is-multi-region-trail |
Registra las 19+ regiones | Un atacante crea recursos en ap-south-1, donde nadie mira |
--include-global-service-events |
Incluye IAM, STS, CloudFront, Route 53 | Son globales y se registran en us-east-1 |
--enable-log-file-validation |
Genera ficheros de resumen firmados | Detecta manipulación |
--kms-key-id |
Cifra los ficheros con tu clave | Separación de funciones (04-02) |
La primera es la más importante desde el punto de vista de seguridad. Un trail de una sola región es una cámara que solo enfoca la puerta principal.
El bucket que nadie debe poder borrar
Un registro de auditoría que el atacante puede borrar no es un registro de auditoría. Es la primera cosa que hace cualquiera que sabe lo que hace: entrar, borrar el rastro, seguir.
Hay cuatro niveles de protección, y conviene entender qué protege cada uno:
| Nivel | Qué impide | Debilidad |
|---|---|---|
1. Política de bucket con Deny |
Borrar objetos | Quien pueda cambiar la política, la quita |
| 2. Versionado + MFA Delete | Borrar versiones sin MFA física | Solo lo activa la cuenta raíz |
3. Object Lock en modo COMPLIANCE |
Borrar, incluso la cuenta raíz | Hay que activarlo al crear el bucket |
| 4. Cuenta separada | Que el atacante de producción llegue al bucket | Requiere Organizations (09-04) |
Nivel 3, Object Lock, es el que de verdad cierra la puerta:
# Solo se puede activar en un bucket con versionado.
# En buckets ya existentes hay que solicitarlo al soporte de AWS;
# lo habitual es crearlo con --object-lock-enabled-for-bucket.
aws s3api put-object-lock-configuration \
--bucket mercadofresco-auditoria-cloudtrail \
--object-lock-configuration '{
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": { "Mode": "COMPLIANCE", "Days": 2555 }
}
}' \
--profile mercadofresco-devLos dos modos, y la diferencia es enorme:
| Modo | Quién puede acortar la retención |
|---|---|
GOVERNANCE |
Quien tenga s3:BypassGovernanceRetention |
COMPLIANCE |
Nadie. Ni la cuenta raíz. Ni el soporte de AWS. |
Esos 2.555 días son 7 años. Y ahí está el peligro: con COMPLIANCE, cada objeto que CloudTrail
escriba será imborrable durante siete años, y lo pagarás durante siete años. Si te equivocas de valor,
no hay marcha atrás.
Esta es exactamente la decisión que debe validar un profesional de cumplimiento. El periodo de retención de los registros de auditoría depende de tu sector, tu país y tus obligaciones contractuales. No lo elijas por intuición, y no copies el 2555 de este curso.
Nivel 2, MFA Delete, solo lo puede activar la cuenta raíz con un dispositivo MFA, y por eso no encaja con el principio de 04-01 de no usar la raíz. MercadoFresco no lo usa; usa Object Lock.
Nivel 4, cuenta separada, es la respuesta profesional completa: el bucket de auditoría vive en una
cuenta de registro distinta, a la que la cuenta de producción solo puede escribir. Aunque alguien
comprometa por completo 111122223333, los registros están fuera de su alcance. Requiere AWS
Organizations, que es la lección 09-04. MercadoFresco lo tiene anotado como el siguiente paso.
Y una defensa complementaria que se monta hoy mismo: una alarma que avise si alguien detiene o borra el trail. Está en la sección de filtros de métricas.
Validación de integridad de los ficheros
Con --enable-log-file-validation, CloudTrail hace algo elegante: cada hora publica un fichero de
resumen (digest) que contiene el hash SHA-256 de cada fichero de registro de esa hora, más el
hash del resumen anterior. Es una cadena: cada resumen firma al anterior.
flowchart LR
D1["Resumen 10:00<br/>hash de los ficheros<br/>+ hash del resumen 09:00"] --> D2["Resumen 11:00<br/>hash de los ficheros<br/>+ hash del resumen 10:00"]
D2 --> D3["Resumen 12:00<br/>..."]
F1["registro-10-01.json.gz"] -.-> D1
F2["registro-10-02.json.gz"] -.-> D1
F3["registro-11-01.json.gz"] -.-> D2
Consecuencia práctica: no se puede alterar un fichero de registro, ni borrarlo, ni sustituir un resumen, sin romper la cadena. Los resúmenes están firmados con la clave privada de CloudTrail, así que tampoco se pueden regenerar.
La verificación:
aws cloudtrail validate-logs \
--trail-arn arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco \
--start-time 2026-07-01T00:00:00Z \
--end-time 2026-08-01T00:00:00Z \
--profile mercadofresco-dev --region eu-west-1Una salida sana termina con Results requested for ... Results found for N digest files ... All files verified. Cualquier mención a ficheros modificados o ausentes es un hallazgo de seguridad
inmediato.
Marta lo ejecuta el primer lunes de cada mes y guarda la salida. En una auditoría formal, esa salida es lo que demuestra que los registros no se han tocado.
Anatomía de un evento: el JSON comentado
Este es un evento real de kms:Decrypt, que es exactamente el que buscábamos. Comentado campo a
campo:
{
"eventVersion": "1.09",
"userIdentity": {
"type": "AssumedRole",
"principalId": "AROA1234567890ABCDEFG:sesion-marta-copias",
"arn": "arn:aws:sts::111122223333:assumed-role/rol-restauracion-copias/sesion-marta-copias",
"accountId": "111122223333",
"accessKeyId": "ASIA1234567890ABCDEF",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"principalId": "AROA1234567890ABCDEFG",
"arn": "arn:aws:iam::111122223333:role/rol-restauracion-copias",
"accountId": "111122223333",
"userName": "rol-restauracion-copias"
},
"attributes": {
"creationDate": "2026-07-28T03:41:52Z",
"mfaAuthenticated": "false"
}
}
},
"eventTime": "2026-07-28T03:42:17Z",
"eventSource": "kms.amazonaws.com",
"eventName": "Decrypt",
"awsRegion": "eu-west-1",
"sourceIPAddress": "198.51.100.77",
"userAgent": "aws-cli/2.15.30 Python/3.11.8 Linux/6.1.0 exe/x86_64",
"requestParameters": {
"encryptionContext": {
"aws:rds:db-id": "arn:aws:rds:eu-west-1:111122223333:db:mercadofresco-pedidos",
"aws:rds:backup-id": "snapshot-2026-07-27-03-00"
},
"keyId": "arn:aws:kms:eu-west-1:111122223333:key/8f2c1a9b-4d3e-4f6a-9c1b-2e5d7a8f3c04",
"encryptionAlgorithm": "SYMMETRIC_DEFAULT"
},
"responseElements": null,
"requestID": "d3a1f9c2-7b4e-4a8d-9f2c-1e5b3a7d9c04",
"eventID": "c7f2b81a-3d9e-4c5f-8a1b-2d6e4f7a9c31",
"readOnly": true,
"resources": [
{
"accountId": "111122223333",
"type": "AWS::KMS::Key",
"ARN": "arn:aws:kms:eu-west-1:111122223333:key/8f2c1a9b-4d3e-4f6a-9c1b-2e5d7a8f3c04"
}
],
"eventType": "AwsApiCall",
"managementEvent": true,
"recipientAccountId": "111122223333",
"eventCategory": "Management",
"tlsDetails": {
"tlsVersion": "TLSv1.3",
"cipherSuite": "TLS_AES_128_GCM_SHA256",
"clientProvidedHostHeader": "kms.eu-west-1.amazonaws.com"
}
}Lectura campo a campo de lo que este evento nos está diciendo:
| Campo | Valor | Qué significa aquí |
|---|---|---|
eventTime |
03:42:17Z |
Las 3:42 de la madrugada. Anómalo por sí solo |
userIdentity.type |
AssumedRole |
No fue un usuario directo: alguien asumió un rol |
sessionIssuer.userName |
rol-restauracion-copias |
Qué rol |
principalId tras los : |
sesion-marta-copias |
Quién lo asumió: el nombre de sesión |
mfaAuthenticated |
"false" |
Sin MFA. Segundo indicador |
sourceIPAddress |
198.51.100.77 |
Desde dónde. Hay que comprobar si es conocida |
userAgent |
aws-cli/2.15.30 |
Desde la CLI, no la consola. Fue un script o una persona en terminal |
eventName |
Decrypt |
La operación |
encryptionContext |
db-id, backup-id |
Qué se descifró: la copia del 27 de julio |
readOnly |
true |
No modificó nada |
errorCode |
(ausente) | Tuvo éxito |
tlsDetails |
TLS 1.3 | Metadatos de la conexión |
Tres campos merecen comentario aparte:
responseElementsesnullen las operaciones de solo lectura. En una operación de escritura —CreateBucket,RunInstances— contiene lo que devolvió la API: el ID del recurso creado. Es el campo que te dice qué se creó.errorCodeyerrorMessageaparecen solo cuando la llamada falló.AccessDenied,UnauthorizedOperation,Client.InvalidParameterValue. Que CloudTrail registre también las llamadas que fallan es una de sus propiedades más valiosas: un atacante probando puertas deja un reguero deAccessDeniedque es la señal más limpia de reconocimiento.encryptionContextes lo que en 04-02 explicamos como los «datos adicionales autenticados» del cifrado de sobre. Aquí se ve su verdadero valor: es lo que convierte un evento genérico deDecrypten «alguien descifró la copia del 27 de julio demercadofresco-pedidos». Sin él, el evento diría solo que se usó una clave.
userIdentity: los seis tipos y cómo se leen
El campo userIdentity es donde vive la respuesta a «¿quién?», y tiene seis formas distintas:
type |
Significa | Dónde está el nombre real |
|---|---|---|
Root |
La cuenta raíz. Alarma siempre | arn |
IAMUser |
Un usuario IAM con claves permanentes | userName |
AssumedRole |
Alguien asumió un rol con STS | sessionContext.sessionIssuer.userName + nombre de sesión |
AWSService |
Un servicio de AWS actuando solo | invokedBy |
AWSAccount |
Otra cuenta de AWS | accountId |
FederatedUser |
Federación con SAML / OIDC / Identity Center | sessionContext |
Unknown |
No determinado | — |
El más habitual, y el más confuso, es AssumedRole. La instancia EC2 de MercadoFresco no aparece
como «instancia»: aparece como el rol rol-mercadofresco-tienda con un nombre de sesión que es el ID
de la instancia. Y cuando Marta asume un rol desde su usuario, aparece como el rol con el nombre de
sesión que ella eligió.
De ahí sale una de las prácticas más útiles de todo este módulo: si en AssumeRole no pones un
nombre de sesión identificativo, CloudTrail te dirá «alguien con el rol de administración borró el
bucket» y no podrás saber quién. Con --role-session-name marta.costa sí:
aws sts assume-role \
--role-arn arn:aws:iam::111122223333:role/rol-restauracion-copias \
--role-session-name marta.costa \
--profile mercadofresco-devEn 04-01 introdujimos esto de pasada. Aquí se ve por qué importa: el nombre de sesión es lo que convierte un rol compartido en una persona identificable. Con AWS IAM Identity Center esto se hace solo, con el correo del usuario federado como nombre de sesión.
Y una nota sobre AWSService. Muchos eventos legítimos tienen invokedBy con valores como
autoscaling.amazonaws.com o dlm.amazonaws.com. Son AWS actuando en tu nombre: el ASG lanzando una
instancia, DLM creando una instantánea (02-02). No son alertas; son el sistema funcionando. Filtrarlos
correctamente es la mitad del trabajo de reducir el ruido en una investigación.
Quién descifró la última copia
Ahora sí, la respuesta a la pregunta del módulo 4. La consulta directa contra el historial de 90 días:
aws cloudtrail lookup-events \
--lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
--start-time 2026-07-25T00:00:00Z \
--end-time 2026-07-30T00:00:00Z \
--profile mercadofresco-dev --region eu-west-1 \
--output json \
| jq -r '.Events[]
| select(.CloudTrailEvent | fromjson | .requestParameters.encryptionContext["aws:rds:db-id"] // "" | test("mercadofresco-pedidos"))
| (.CloudTrailEvent | fromjson)
| [.eventTime,
.userIdentity.sessionContext.sessionIssuer.userName // .userIdentity.userName,
(.userIdentity.arn | split("/") | last),
.sourceIPAddress,
.userIdentity.sessionContext.attributes.mfaAuthenticated,
.requestParameters.encryptionContext["aws:rds:backup-id"]]
| @tsv'Salida real:
2026-07-27T03:00:14Z rds-backup-service AWSService rds.amazonaws.com - snapshot-2026-07-27 2026-07-27T09:15:41Z rol-mercadofresco-tienda i-0abc123def456 10.0.11.24 false - 2026-07-28T03:42:17Z rol-restauracion-copias sesion-marta-copias 198.51.100.77 false snapshot-2026-07-27
Tres líneas y tres lecturas completamente distintas:
rds-backup-servicea las 03:00: es el propio RDS cifrando la copia automática. Normal, es la ventana de copia que configuramos en 02-04.rol-mercadofresco-tiendadesde10.0.11.24: una instancia del ASG, IP privada de la subred de aplicación (03-01), descifrando datos de la aplicación. Normal.rol-restauracion-copiasa las 03:42, desde198.51.100.77, sin MFA, sobre la copiasnapshot-2026-07-27: esto hay que investigarlo.
Y las preguntas que se derivan, en este orden:
- ¿Es
198.51.100.77una IP conocida? ¿La oficina? ¿La VPN? ¿El domicilio de alguien? - ¿Quién asumió
rol-restauracion-copias? El nombre de sesión dicesesion-marta-copias, pero eso es una cadena que eligió quien hizo la llamada, no una identidad verificada. Hay que buscar el eventoAssumeRolecorrespondiente para ver quién lo asumió de verdad. - ¿Hay un evento
RestoreDBInstanceFromDBSnapshotcerca? ¿Se restauró la copia en algún sitio? - ¿Hubo un
AccessDeniedantes, señal de tanteo?
Esto no es una respuesta, es el principio de una investigación. Y esa es precisamente la lección: CloudTrail no dice si algo está mal. Dice qué pasó, con precisión suficiente para que una persona decida. La investigación completa está más abajo.
Eventos de gestión frente a eventos de datos
Es la distinción que decide la factura de CloudTrail:
| Eventos de gestión | Eventos de datos | |
|---|---|---|
| Qué registran | Operaciones sobre recursos | Operaciones sobre el contenido |
| Ejemplos | CreateBucket, RunInstances, AssumeRole, Decrypt |
GetObject, PutObject, Invoke, GetItem |
| Volumen | Bajo: cientos o miles al día | Altísimo: millones |
| Primera copia | Gratis | De pago siempre |
| Coste | 2 USD por 100.000 eventos (copias adicionales) | 0,10 USD por 100.000 eventos |
| Activado por defecto | Sí | No |
El cálculo que hay que hacer antes de activar eventos de datos en S3. MercadoFresco sirve unas
40 millones de peticiones al mes a mercadofresco-catalogo-fotos (fotos de producto, aunque
CloudFront cachea la mayoría). Registrar todas:
40.000.000 × 0,10 / 100.000 = 40 USD/mes, más el almacenamiento en S3 de un volumen enorme de JSON, más lo que cueste consultarlo con Athena.
Y no aporta casi nada: son lecturas públicas de fotos de tomates.
La regla es: activa eventos de datos con selectores de alcance mínimo, solo sobre lo que importa.
aws cloudtrail put-event-selectors \
--trail-name trail-mercadofresco \
--advanced-event-selectors '[
{
"Name": "Eventos de gestion completos",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Management"] }
]
},
{
"Name": "Solo las copias de la base de datos y los informes",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": [
"arn:aws:s3:::mercadofresco-copias-basedatos/",
"arn:aws:s3:::mercadofresco-informes-analitica/"
]}
]
},
{
"Name": "Escrituras en el catalogo, no lecturas",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": [
"arn:aws:s3:::mercadofresco-catalogo-fotos/"
]},
{ "Field": "readOnly", "Equals": ["false"] }
]
}
]' \
--profile mercadofresco-dev --region eu-west-1Lo que consigue esta configuración:
| Bucket | Se registra | Volumen/mes | Coste |
|---|---|---|---|
mercadofresco-copias-basedatos |
Todo: quién lee una copia es crítico | ~2.000 | 0,00 USD |
mercadofresco-informes-analitica |
Todo: datos de negocio de Sara | ~15.000 | 0,02 USD |
mercadofresco-catalogo-fotos |
Solo escrituras | ~40.000 | 0,04 USD |
mercadofresco-registros-web |
Nada | — | 0,00 USD |
| Total | ~0,06 USD/mes |
De 40 USD a 0,06 USD sin perder nada relevante. Ese readOnly: false sobre el catálogo es la decisión
clave: leer una foto de tomate no interesa a nadie; que alguien suba o borre una foto sí, porque
es una modificación del contenido del sitio.
Los tipos de recurso disponibles para eventos de datos incluyen AWS::S3::Object,
AWS::Lambda::Function, AWS::DynamoDB::Table (módulo 6), AWS::SQS::Queue (07-01) y varios más.
Eventos de Insights
CloudTrail Insights analiza el volumen de llamadas y detecta picos anómalos de actividad de escritura o de errores. No mira el contenido: mira el ritmo.
Casos que detecta bien:
- Un script mal escrito que llama a
DescribeInstances40.000 veces en una hora. - Un aumento súbito de
AccessDenied: alguien probando permisos. - Un pico de
TerminateInstancesa las 4 de la mañana.
aws cloudtrail put-insight-selectors \
--trail-name trail-mercadofresco \
--insight-selectors '[
{"InsightType": "ApiCallRateInsight"},
{"InsightType": "ApiErrorRateInsight"}
]' \
--profile mercadofresco-dev --region eu-west-1Coste: 0,35 USD por cada 100.000 eventos de gestión analizados. Con ~150.000 eventos mensuales, MercadoFresco paga unos 0,53 USD/mes. Es de lo más barato de este módulo y de lo que más pronto avisa.
Las dos limitaciones honestas: necesita al menos 7 días de línea base antes de servir para algo, y solo detecta anomalías de volumen, no de intención. Una sola llamada maliciosa perfectamente ejecutada no genera ningún insight. Para eso están las alarmas específicas de la siguiente sección.
Trails multirregión y de organización
Multirregión ya lo activamos con --is-multi-region-trail, y merece insistir en por qué. Un trail
de una sola región es como una cámara de seguridad que solo enfoca la puerta principal: un atacante
que consiga credenciales creará sus instancias de minado en ap-southeast-2, donde nadie mira nunca.
Con el trail multirregión, esas llamadas aparecen en el mismo bucket.
Coste de la multirregión: cero. La primera copia de los eventos de gestión es gratuita en todas las regiones. No hay ninguna razón para no activarla.
Trail de organización: si tienes varias cuentas bajo AWS Organizations, un solo trail creado en la cuenta de gestión registra todas las cuentas miembro, y estas no pueden desactivarlo ni verlo. Es la configuración correcta para cualquier organización con más de una cuenta, y es la lección 09-04. MercadoFresco tiene una sola cuenta hoy; la separación entre producción, preproducción y registro es un objetivo declarado.
Enviar el trail a CloudWatch Logs
El bucket de S3 es el archivo. Para reaccionar en caliente hace falta que los eventos lleguen también a CloudWatch Logs, donde pueden convertirse en métricas y alarmas (05-01).
# 1. Grupo de registros con retencion finita
aws logs create-log-group \
--log-group-name /aws/cloudtrail/mercadofresco \
--profile mercadofresco-dev --region eu-west-1
aws logs put-retention-policy \
--log-group-name /aws/cloudtrail/mercadofresco \
--retention-in-days 90 \
--profile mercadofresco-dev --region eu-west-1Fíjate: 90 días en CloudWatch Logs, 7 años en S3. Los dos destinos tienen propósitos distintos y retenciones distintas. Logs es para alarmar y consultar lo reciente; S3 es el archivo legal. Duplicar 7 años en CloudWatch Logs costaría una fortuna sin aportar nada.
El rol que CloudTrail necesita:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "sts:AssumeRole",
"Condition": {
"StringEquals": {
"aws:SourceArn": "arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco"
}
}
}]
}Con la política de permisos:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Action": ["logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": "arn:aws:logs:eu-west-1:111122223333:log-group:/aws/cloudtrail/mercadofresco:log-stream:*"
}]
}Coste: la ingesta de los eventos de gestión de MercadoFresco son unos 0,4 GB al mes: 0,25 USD. Si activaras eventos de datos de alto volumen, esto se dispararía; es otra razón para los selectores restrictivos.
Filtros de métricas y alarmas de seguridad
Aquí es donde CloudTrail y CloudWatch se juntan y se convierten en detección, no solo en registro. Estas son las cinco alarmas que monta MercadoFresco, y las cinco son estándar en cualquier auditoría de seguridad (aparecen en el CIS Benchmark de AWS, que veremos en 05-04).
1. Uso de la cuenta raíz. Después de 04-01, la raíz no debería usarse nunca:
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-uso-root \
--filter-pattern '{ $.userIdentity.type = "Root" && $.userIdentity.invokedBy NOT EXISTS && $.eventType != "AwsServiceEvent" }' \
--metric-transformations \
metricName=UsoDeRoot,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-uso-root \
--alarm-description "ALGUIEN HA USADO LA CUENTA RAIZ" \
--namespace MercadoFresco/Seguridad --metric-name UsoDeRoot \
--statistic Sum --period 300 --evaluation-periods 1 \
--threshold 0 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Ese $.userIdentity.invokedBy NOT EXISTS excluye los eventos en los que un servicio de AWS actúa en
nombre de la cuenta, que son legítimos y frecuentes. Sin él, la alarma sería puro ruido.
2. Detención o borrado de un trail. La primera acción de cualquiera que quiera ocultar su rastro:
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-cambio-trail \
--filter-pattern '{ ($.eventName = "StopLogging") || ($.eventName = "DeleteTrail") || ($.eventName = "UpdateTrail") || ($.eventName = "PutEventSelectors") }' \
--metric-transformations \
metricName=CambiosEnTrail,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Con su alarma en umbral 0, exactamente igual que la anterior. Esta es la alarma más importante de esta lección: es la que avisa de que el sistema de auditoría está siendo atacado.
3. Uso de KMS sobre las copias de la base de datos. La que responde directamente a la pregunta del módulo 4, pero en caliente:
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-descifrado-copias \
--filter-pattern '{ $.eventSource = "kms.amazonaws.com" && $.eventName = "Decrypt" && $.requestParameters.encryptionContext."aws:rds:db-id" = "*mercadofresco-pedidos*" && $.userIdentity.type != "AWSService" }' \
--metric-transformations \
metricName=DescifradoCopias,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Ese $.userIdentity.type != "AWSService" excluye al propio RDS cifrando sus copias automáticas cada
noche, que es la línea 1 de nuestra investigación. Lo que queda es actividad humana o de script
sobre una copia de la base de datos, y eso siempre merece un aviso.
4. Accesos denegados repetidos. El reguero que deja el reconocimiento:
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-accesos-denegados \
--filter-pattern '{ ($.errorCode = "AccessDenied*") || ($.errorCode = "UnauthorizedOperation") }' \
--metric-transformations \
metricName=AccesosDenegados,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-accesos-denegados \
--alarm-description "Pico de AccessDenied: posible reconocimiento" \
--namespace MercadoFresco/Seguridad --metric-name AccesosDenegados \
--statistic Sum --period 300 --evaluation-periods 2 --datapoints-to-alarm 2 \
--threshold 20 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Aquí el umbral no es 0, y la razón es honesta: los AccessDenied son constantes en cualquier
cuenta viva. Un desarrollador probando, una herramienta que consulta algo que no puede, un SDK que
sondea. 20 en cinco minutos sostenidos durante dos periodos sí es una señal.
5. Cambios en la política de un grupo de seguridad o en IAM.
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-cambios-seguridad \
--filter-pattern '{ ($.eventName = "AuthorizeSecurityGroupIngress") || ($.eventName = "CreateAccessKey") || ($.eventName = "AttachRolePolicy") || ($.eventName = "PutBucketPolicy") || ($.eventName = "DeleteBucketPolicy") || ($.eventName = "PutKeyPolicy") }' \
--metric-transformations \
metricName=CambiosDeSeguridad,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Con umbral 0 en horario nocturno sería demasiado ruidoso durante un despliegue diurno. MercadoFresco lo pone en umbral 0 con periodo de 5 minutos, aceptando el ruido: prefiere enterarse de todos los cambios de seguridad. Que la alarma sea informativa y no urgente es una decisión válida, siempre que esté documentada y no acabe siendo ignorada por costumbre.
Consultar con Athena
Para investigar en serio hay que consultar los ficheros de S3 con SQL. Amazon Athena ejecuta SQL directamente sobre los objetos de un bucket, sin cargar nada en ninguna base de datos.
Athena se ve con más profundidad en el módulo 6 al hablar de analítica. Aquí usamos lo justo para investigar CloudTrail, que es su caso de uso más común.
Paso 1: la tabla. La forma cómoda de crearla es desde la consola de CloudTrail («Create Athena table»), que genera este DDL. Conviene entenderlo:
CREATE DATABASE IF NOT EXISTS auditoria_mercadofresco;
CREATE EXTERNAL TABLE auditoria_mercadofresco.cloudtrail_mercadofresco (
eventVersion STRING,
userIdentity STRUCT<
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
userName: STRING,
invokedBy: STRING,
accessKeyId: STRING,
sessionContext: STRUCT<
attributes: STRUCT<
mfaAuthenticated: STRING,
creationDate: STRING>,
sessionIssuer: STRUCT<
type: STRING,
principalId: STRING,
arn: STRING,
accountId: STRING,
userName: STRING>>>,
eventTime STRING,
eventSource STRING,
eventName STRING,
awsRegion STRING,
sourceIPAddress STRING,
userAgent STRING,
errorCode STRING,
errorMessage STRING,
requestParameters STRING,
responseElements STRING,
requestID STRING,
eventID STRING,
readOnly STRING,
resources ARRAY<STRUCT<
arn: STRING,
accountId: STRING,
type: STRING>>,
eventType STRING,
recipientAccountId STRING,
vpcEndpointId STRING
)
PARTITIONED BY (region STRING, anio STRING, mes STRING, dia STRING)
ROW FORMAT SERDE 'com.amazon.emr.hive.serde.CloudTrailSerde'
STORED AS INPUTFORMAT 'com.amazon.emr.cloudtrail.CloudTrailInputFormat'
OUTPUTFORMAT 'org.apache.hadoop.hive.ql.io.HiveIgnoreKeyTextOutputFormat'
LOCATION 's3://mercadofresco-auditoria-cloudtrail/AWSLogs/111122223333/CloudTrail/';Dos cosas que hay que ver en ese DDL:
requestParametersyresponseElementssonSTRING, no estructuras. Su contenido varía según el evento, así que se consultan con funciones JSON:json_extract_scalar(requestParameters, '$.keyId').PARTITIONED BYes lo que decide el coste. Athena cobra 5 USD por TB escaneado. Sin particiones, cada consulta lee todos los ficheros del bucket. Con particiones y unWHEREsobre ellas, lee solo los días que pidas.
Paso 2: registrar las particiones. Con la proyección de particiones se automatiza:
ALTER TABLE auditoria_mercadofresco.cloudtrail_mercadofresco
SET TBLPROPERTIES (
'projection.enabled' = 'true',
'projection.region.type' = 'enum',
'projection.region.values' = 'eu-west-1,us-east-1,eu-central-1',
'projection.anio.type' = 'integer',
'projection.anio.range' = '2026,2030',
'projection.mes.type' = 'integer',
'projection.mes.range' = '1,12',
'projection.mes.digits' = '2',
'projection.dia.type' = 'integer',
'projection.dia.range' = '1,31',
'projection.dia.digits' = '2',
'storage.location.template' =
's3://mercadofresco-auditoria-cloudtrail/AWSLogs/111122223333/CloudTrail/${region}/${anio}/${mes}/${dia}'
);Sin esto habría que ejecutar MSCK REPAIR TABLE o ALTER TABLE ADD PARTITION cada día. Con la
proyección, Athena deduce las particiones del patrón de rutas.
Las consultas que responden preguntas reales
1. ¿Quién asumió rol-mercadofresco-tienda, y cuándo?
SELECT eventtime,
userIdentity.arn AS quien,
json_extract_scalar(requestParameters, '$.roleSessionName') AS sesion,
sourceipaddress,
useragent,
errorcode
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND eventname = 'AssumeRole'
AND json_extract_scalar(requestParameters, '$.roleArn')
LIKE '%rol-mercadofresco-tienda%'
ORDER BY eventtime DESC
LIMIT 100;2. ¿Quién leyó el secreto de mfadmin? La pregunta que dejamos pendiente en 04-03:
SELECT eventtime,
COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
userIdentity.userName) AS identidad,
split_part(userIdentity.arn, '/', 3) AS sesion,
sourceipaddress,
userIdentity.sessionContext.attributes.mfaAuthenticated AS con_mfa,
errorcode
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND eventsource = 'secretsmanager.amazonaws.com'
AND eventname = 'GetSecretValue'
AND json_extract_scalar(requestParameters, '$.secretId')
LIKE '%mercadofresco/produccion/rds/mfadmin%'
ORDER BY eventtime DESC;Lo esperable en el resultado: el rol de la tienda leyéndolo al arrancar cada instancia, y
rol-rotacion-mfadmin cada 30 días haciendo la rotación (04-03). Cualquier otra identidad es un
hallazgo.
3. Todas las llamadas denegadas, agrupadas. La consulta de reconocimiento:
SELECT COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
userIdentity.userName, userIdentity.type) AS identidad,
sourceipaddress,
eventsource,
eventname,
count(*) AS intentos,
min(eventtime) AS primero,
max(eventtime) AS ultimo
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND errorcode IN ('AccessDenied', 'AccessDeniedException', 'UnauthorizedOperation')
GROUP BY 1, 2, 3, 4
HAVING count(*) > 5
ORDER BY intentos DESC
LIMIT 50;Esta consulta tiene un uso doble muy valioso, y conviene subrayarlo:
- Seguridad: una identidad desconocida con 400
AccessDeniedsobre 30 servicios distintos en diez minutos es reconocimiento. Alguien está mapeando lo que puede hacer. - Operación:
rol-mercadofresco-tiendacon 3.000AccessDeniedsobres3:GetObjectsignifica que apol-mercadofresco-tiendale falta un permiso y hay una funcionalidad rota que nadie ha reportado. LosAccessDeniedson también un detector de errores de configuración.
4. Actividad fuera de horario, que es la que suele delatar.
SELECT eventtime, eventname, eventsource,
COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
userIdentity.userName) AS identidad,
sourceipaddress
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND readonly = 'false'
AND userIdentity.type != 'AWSService'
AND (hour(from_iso8601_timestamp(eventtime)) < 7
OR hour(from_iso8601_timestamp(eventtime)) > 21)
ORDER BY eventtime DESC;Filtrar userIdentity.type != 'AWSService' es imprescindible: si no, la salida se llena de copias
automáticas, instantáneas de DLM y actividad del ASG a las 3 de la mañana, que es completamente
normal.
5. Todas las IP de origen, para detectar las desconocidas.
SELECT sourceipaddress,
count(*) AS llamadas,
count(DISTINCT eventname) AS acciones_distintas,
array_agg(DISTINCT COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
userIdentity.userName)) AS identidades,
min(eventtime) AS primera, max(eventtime) AS ultima
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND userIdentity.type != 'AWSService'
AND sourceipaddress NOT LIKE '10.%'
GROUP BY sourceipaddress
ORDER BY llamadas DESC;Una IP que aparece por primera vez, hace 12 llamadas y desaparece es mucho más sospechosa que una que hace 40.000 desde hace meses.
6. La consulta de la copia descifrada, ahora en SQL.
SELECT eventtime,
COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
userIdentity.userName) AS identidad,
split_part(userIdentity.arn, '/', 3) AS sesion,
sourceipaddress,
json_extract_scalar(requestParameters, '$.encryptionContext."aws:rds:backup-id"') AS copia
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND eventsource = 'kms.amazonaws.com'
AND eventname = 'Decrypt'
AND userIdentity.type != 'AWSService'
AND requestParameters LIKE '%mercadofresco-pedidos%'
ORDER BY eventtime DESC;Control de coste en Athena: cada consulta cobra por TB escaneado. Poner siempre el WHERE de
partición (anio, mes, dia) es la diferencia entre 0,01 USD y varios euros por consulta. Y se
puede fijar un límite duro por grupo de trabajo:
aws athena update-work-group --work-group primary \
--configuration-updates 'BytesScannedCutoffPerQuery=10737418240' \
--profile mercadofresco-dev --region eu-west-1Diez GiB máximo por consulta. Si alguien escribe un SELECT * sin WHERE, la consulta se cancela sola
en lugar de generar una factura.
CloudTrail Lake
CloudTrail Lake es la alternativa gestionada a montar Athena a mano: un almacén de datos de eventos gestionado por AWS, con SQL directo desde la consola de CloudTrail y sin tablas que crear.
| S3 + Athena | CloudTrail Lake | |
|---|---|---|
| Configuración | Bucket, política, tabla, particiones | Un comando |
| Consulta | SQL (Presto) desde Athena | SQL desde CloudTrail |
| Retención | La que quieras en S3 | Hasta 10 años |
| Coste de ingesta | Solo S3 (céntimos) | ~2,50 USD/GB |
| Coste de consulta | 5 USD/TB escaneado | 0,005 USD/GB escaneado |
| Coste total | Mucho menor | Mayor |
| Esfuerzo | Medio | Mínimo |
| Integración | Tú la montas | Nativa con Organizations |
aws cloudtrail create-event-data-store \
--name lake-mercadofresco \
--retention-period 2555 \
--multi-region-enabled \
--advanced-event-selectors '[{
"Name": "Gestion",
"FieldSelectors": [{ "Field": "eventCategory", "Equals": ["Management"] }]
}]' \
--profile mercadofresco-dev --region eu-west-1La decisión de MercadoFresco: se queda con S3 + Athena. Con 0,4 GB mensuales de eventos, Lake costaría 1 USD al mes de ingesta —no es dramático— pero el bucket de S3 ya existe, ya está protegido con Object Lock y ya alimenta el archivo legal de 7 años. Lake se anota como opción a reconsiderar si la cuenta crece y montar particiones a mano empieza a doler.
Con muchas cuentas y equipos que no son de plataforma, Lake gana claramente: nadie tiene que aprender a crear tablas externas en Athena para investigar un incidente.
Investigación de un incidente, paso a paso
Volvemos al hallazgo: rol-restauracion-copias descifró la copia snapshot-2026-07-27 a las 03:42
desde 198.51.100.77, sin MFA. Este es el procedimiento completo, y sirve como plantilla para
cualquier investigación.
Paso 0. Congelar la evidencia. Antes de nada.
# Verificar que los registros no han sido manipulados
aws cloudtrail validate-logs \
--trail-arn arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco \
--start-time 2026-07-27T00:00:00Z --end-time 2026-07-29T00:00:00Z \
--profile mercadofresco-dev --region eu-west-1
# Copiar los ficheros del periodo a un prefijo de investigacion
aws s3 sync \
s3://mercadofresco-auditoria-cloudtrail/AWSLogs/111122223333/CloudTrail/eu-west-1/2026/07/28/ \
s3://mercadofresco-auditoria-cloudtrail/investigaciones/inc-2026-07-28/ \
--profile mercadofresco-devEsto se hace primero y siempre. Si el incidente acaba en un procedimiento formal, lo que importa no es lo que descubriste, sino que puedas demostrar que los datos no cambiaron mientras investigabas.
Paso 1. ¿Quién asumió el rol? El nombre de sesión sesion-marta-copias es texto libre elegido por
quien llamó. Hay que buscar el AssumeRole:
SELECT eventtime, userIdentity.arn AS quien_asumio,
userIdentity.type, sourceipaddress, useragent,
userIdentity.sessionContext.attributes.mfaAuthenticated AS con_mfa,
json_extract_scalar(requestParameters, '$.roleSessionName') AS nombre_sesion
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio='2026' AND mes='07' AND dia='28'
AND eventname = 'AssumeRole'
AND json_extract_scalar(requestParameters, '$.roleArn') LIKE '%rol-restauracion-copias%'
ORDER BY eventtime;Resultado: arn:aws:iam::111122223333:user/luis.dev, a las 03:41:52, desde 198.51.100.77,
mfaAuthenticated: false, agente aws-cli/2.15.30.
Paso 2. Reconstruir toda la sesión. El accessKeyId temporal (ASIA...) identifica la sesión
completa:
SELECT eventtime, eventsource, eventname, errorcode,
json_extract_scalar(requestParameters, '$.dBInstanceIdentifier') AS instancia
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio='2026' AND mes='07' AND dia='28'
AND userIdentity.accessKeyId = 'ASIA1234567890ABCDEF'
ORDER BY eventtime;Resultado:
| Hora | Evento | Resultado |
|---|---|---|
| 03:41:52 | AssumeRole |
OK |
| 03:42:03 | DescribeDBSnapshots |
OK |
| 03:42:17 | kms:Decrypt |
OK |
| 03:42:19 | RestoreDBInstanceFromDBSnapshot |
OK — instancia pedidos-copia-luis |
| 03:58:44 | CreateDBSnapshot |
AccessDenied |
| 04:30:12 | DeleteDBInstance |
OK |
Paso 3. Interpretar. El patrón es coherente y bastante legible: alguien restauró una copia,
intentó crear una nueva instantánea (denegado), y a los 45 minutos borró la instancia restaurada. No
hay exfiltración visible —ninguna llamada a S3, ningún CreateDBSnapshot con éxito, ninguna
instancia dejada corriendo—. Parece trabajo legítimo hecho mal: restaurar una copia para
comprobar algo, de madrugada, sin avisar y sin MFA.
Paso 4. Confirmar con la persona. Y aquí una advertencia importante, porque es donde más se equivoca la gente: CloudTrail dice qué pasó, no por qué. Nunca se acusa a nadie con un registro en la mano sin hablar primero. La conversación con Luis aclara que estaba comprobando si una copia era restaurable tras un aviso de Sara, y que lo hizo de madrugada para no cargar la base de datos en horas de producción. Intención correcta, procedimiento incorrecto.
Paso 5. Comprobar que no hay nada más. Aunque la explicación cuadre, se verifica:
SELECT eventtime, eventname, sourceipaddress
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio='2026' AND mes='07'
AND sourceipaddress = '198.51.100.77'
ORDER BY eventtime;Si esa IP solo aparece en horario de trabajo de Luis durante meses, es su casa o su oficina. Si aparece una vez, a las 3 de la mañana, y nunca más, la conversación es otra.
Paso 6. Acciones correctivas. Sin culpables y con fechas, igual que el post-mortem de 04-04:
| Acción | Responsable | Plazo |
|---|---|---|
Exigir MFA para asumir rol-restauracion-copias (condición aws:MultiFactorAuthPresent, 04-01) |
Marta | 3 días |
Alarma mercadofresco-descifrado-copias, ya creada, verificada con simulacro |
Marta | 1 día |
| Procedimiento escrito: restaurar copias se hace en preproducción y se avisa | Marta | 1 semana |
| Prueba de restauración programada y automatizada trimestral | Luis | 1 mes |
Etiquetar Entorno=temporal las instancias restauradas y borrarlas solas a las 8 h |
Luis | 1 mes |
Fíjate en la última fila y en la anterior: la respuesta correcta a «Luis restauró una copia a escondidas» no es prohibirlo. Es hacer que probar restauraciones sea fácil, visible y rutinario. Una prueba de restauración es una práctica excelente que en 02-04 recomendamos explícitamente; el problema era el procedimiento, no la actividad.
Relación con IAM Access Analyzer
CloudTrail no solo sirve para investigar: sirve para construir permisos correctos. IAM Access Analyzer puede leer tu historial de CloudTrail y generar una política de mínimo privilegio con exactamente las acciones que una identidad ha usado de verdad en un periodo.
aws accessanalyzer start-policy-generation \
--policy-generation-details '{
"principalArn": "arn:aws:iam::111122223333:role/rol-mercadofresco-tienda"
}' \
--cloud-trail-details '{
"trails": [{
"cloudTrailArn": "arn:aws:cloudtrail:eu-west-1:111122223333:trail/trail-mercadofresco",
"regions": ["eu-west-1"],
"allRegions": false
}],
"accessRole": "arn:aws:iam::111122223333:role/rol-access-analyzer",
"startTime": "2026-06-01T00:00:00Z",
"endTime": "2026-07-31T00:00:00Z"
}' \
--profile mercadofresco-dev --region eu-west-1Esto cierra el círculo con 04-01. Allí escribimos pol-mercadofresco-tienda a mano, razonando sobre
qué debía poder hacer la tienda. Con dos meses de CloudTrail, Access Analyzer dice qué hizo de
verdad, y la diferencia entre ambas listas es un regalo:
- Acciones que están en la política y nunca se usan: candidatas a quitar.
- Acciones que la aplicación intenta y le son denegadas: funcionalidad rota que nadie reportó.
Access Analyzer hace más cosas —detectar recursos accesibles desde fuera de la cuenta, validar políticas— pero esa es su relación directa con CloudTrail y con lo aprendido en 04-01.
Retención, coste y limpieza
| Concepto | Precio | MercadoFresco |
|---|---|---|
| Historial de eventos, 90 días | Gratis | — |
| Primera copia de eventos de gestión | Gratis | 0,00 USD |
| Copias adicionales de eventos de gestión | 2,00 USD / 100.000 | 0,00 USD (solo un trail) |
| Eventos de datos | 0,10 USD / 100.000 | 0,06 USD (con selectores) |
| Insights | 0,35 USD / 100.000 analizados | 0,53 USD |
| Almacenamiento en S3 | 0,023 USD/GB/mes | ~0,05 USD |
| Ingesta a CloudWatch Logs | ~0,63 USD/GB | 0,25 USD |
| Alarmas (5) | 0,10 USD cada una | 0,50 USD |
| Athena | 5 USD/TB escaneado | ~0,20 USD |
| Total | ~1,59 USD/mes |
Es de lo más barato que se puede montar en AWS, y el argumento definitivo: la primera copia de los eventos de gestión multirregión es gratuita. No hay ninguna excusa técnica ni económica para no tener un trail. Que una cuenta de producción no lo tenga es siempre un hallazgo de auditoría.
Ciclo de vida en S3, para que los 7 años no se paguen a precio de almacenamiento estándar:
{
"Rules": [{
"ID": "auditoria-a-frio",
"Status": "Enabled",
"Filter": { "Prefix": "AWSLogs/" },
"Transitions": [
{ "Days": 90, "StorageClass": "STANDARD_IA" },
{ "Days": 365, "StorageClass": "GLACIER_IR" },
{ "Days": 730, "StorageClass": "DEEP_ARCHIVE" }
]
}]
}Cuidado con dos cosas: DEEP_ARCHIVE tarda hasta 12 horas en restaurar, lo que puede ser
inaceptable en una investigación urgente —por eso los dos primeros años se quedan en clases de acceso
rápido—, y si has activado Object Lock en modo COMPLIANCE, la regla de ciclo de vida no puede
expirar objetos antes de que venza la retención. Puede cambiarlos de clase, no borrarlos.
Limpieza, si has montado esto solo para practicar:
# Detener y borrar el trail
aws cloudtrail stop-logging --name trail-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
aws cloudtrail delete-trail --name trail-mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# Alarmas y filtros
aws cloudwatch delete-alarms --alarm-names \
mercadofresco-uso-root mercadofresco-cambio-trail \
mercadofresco-accesos-denegados mercadofresco-descifrado-copias \
mercadofresco-cambios-seguridad \
--profile mercadofresco-dev --region eu-west-1
# Grupo de registros
aws logs delete-log-group --log-group-name /aws/cloudtrail/mercadofresco \
--profile mercadofresco-dev --region eu-west-1
# Lake, si lo creaste: primero quitar la proteccion de borrado
aws cloudtrail update-event-data-store \
--event-data-store <ARN> --no-termination-protection-enabled \
--profile mercadofresco-dev --region eu-west-1
aws cloudtrail delete-event-data-store --event-data-store <ARN> \
--profile mercadofresco-dev --region eu-west-1El bucket no se borra a la ligera. Si activaste Object Lock en modo
COMPLIANCE, los objetos no se pueden borrar hasta que venza la retención —ni tú, ni la cuenta raíz, ni el soporte de AWS—, y el bucket no se puede eliminar mientras contenga objetos. Es una propiedad deseada, no un fallo. Practica Object Lock en una cuenta de pruebas y con retenciones de días, nunca de años.
Errores Comunes y Consejos
1. Crear el trail y no llamar a start-logging. create-trail no arranca el registro. Comprueba
siempre con get-trail-status que IsLogging es true.
2. Trail de una sola región. Un atacante crea recursos donde nadie mira. La multirregión es gratis y no tiene contrapartida.
3. Un bucket de auditoría que se puede borrar. Sin política de Deny, sin versionado y sin Object
Lock, el registro dura lo que tarde alguien en ejecutar un comando. Y el nivel definitivo es una cuenta
separada (09-04).
4. Activar eventos de datos sin selectores. Registrar todos los GetObject de un bucket de fotos
son decenas de USD al mes por datos inútiles. Selectores por prefijo y readOnly: false.
5. No poner nombre de sesión al asumir un rol. CloudTrail dirá «alguien con este rol», y no habrá
forma de saber quién. --role-session-name con el nombre de la persona.
6. Buscar en CloudTrail lo que dice tu aplicación. No está ahí. CloudTrail registra llamadas a la API de AWS; los errores de tu código están en CloudWatch Logs (05-01).
7. Consultas de Athena sin WHERE de partición. Escanean el bucket entero a 5 USD por TB. Pon
siempre anio, mes y dia, y fija BytesScannedCutoffPerQuery en el grupo de trabajo.
8. Ignorar los AccessDenied. Son a la vez el detector de reconocimiento y el detector de
permisos mal configurados. Revisarlos semanalmente encuentra cosas rotas que nadie reportó.
9. No verificar la integridad nunca. validate-logs una vez al mes, con la salida guardada. Sin
eso, la validación está activada pero no sirve de nada.
10. Retención infinita en CloudWatch Logs para los eventos de CloudTrail. El archivo largo va en S3, que cuesta 20 veces menos. En Logs, 90 días.
11. Acusar a alguien con un registro en la mano. CloudTrail dice qué pasó, no por qué. Se investiga, se contrasta y luego se habla con la persona. Un incidente mal gestionado hace más daño que el incidente.
12. Activar Object Lock en modo COMPLIANCE con 7 años «por probar». No hay marcha atrás. Practica
con GOVERNANCE y con retenciones de días.
13. Suponer que el nombre de sesión identifica a alguien. Es una cadena libre. La identidad real
está en el evento AssumeRole correspondiente.
Consejo final: revisa CloudTrail cuando no hay incidentes. Media hora al mes ejecutando las
consultas de IP desconocidas, AccessDenied agrupados y actividad fuera de horario encuentra cosas
—permisos rotos, procesos zombis, claves olvidadas de un antiguo proveedor— que de otro modo solo
aparecen el día que hay un problema de verdad.
Ejercicios
Los tres ejercicios trabajan sobre la tabla auditoria_mercadofresco.cloudtrail_mercadofresco y el
trail trail-mercadofresco definidos en esta lección.
Ejercicio 1: diseñar la estrategia de eventos de datos
MercadoFresco quiere activar eventos de datos, pero con criterio y con un presupuesto de 5 USD al mes. Volumen mensual real:
| Recurso | Operaciones/mes | Reparto |
|---|---|---|
mercadofresco-catalogo-fotos |
40.000.000 | 99,9 % lecturas |
mercadofresco-copias-basedatos |
2.400 | 60 % escrituras |
mercadofresco-informes-analitica |
180.000 | 70 % lecturas (Sara) |
mercadofresco-registros-web |
8.000.000 | 99 % escrituras (el ALB) |
Lambda mercadofresco-generar-miniaturas |
620.000 invocaciones | — |
Lambda mercadofresco-estado-pedido |
660.000 invocaciones | — |
Requisitos: hay que poder saber siempre quién ha leído o tocado una copia de la base de datos; hay que detectar si alguien descarga masivamente los informes de negocio; hay que detectar si alguien altera las fotos del catálogo; el presupuesto es de 5 USD.
Escribe los selectores avanzados completos en JSON, calcula el coste de cada uno, justifica qué dejas fuera y por qué, y propón qué alarma montarías sobre los eventos de datos que sí registras.
Ejercicio 2: la investigación completa
Un lunes por la mañana, la alarma mercadofresco-accesos-denegados ha saltado dos veces durante el
fin de semana. Estos son los datos iniciales:
- Sábado 02:14 a 02:31: 340 eventos con
errorCode = AccessDenied. - Todos desde
sourceIPAddress = 203.0.113.201. - Todos con
userIdentity.type = "IAMUser",userName = "integracion-proveedor". userAgent:aws-cli/2.9.19 Python/3.9.11 Windows/10.- Los servicios afectados:
iam,s3,ec2,rds,secretsmanager,kms,organizations. - Domingo 22:40: 6 eventos sin
errorCodedesde la misma IP y el mismo usuario:ListBuckets,GetBucketLocation,ListObjectsV2sobremercadofresco-informes-analitica,GetObject× 3.
Escribe: las consultas SQL exactas que ejecutarías y en qué orden; qué conclusión sacas de cada una; qué acciones de contención tomarías y en qué orden exacto; y qué te dice el hecho de que las 340 llamadas fallaran pero 6 tuvieran éxito. Indica también qué información no vas a poder obtener de CloudTrail y dónde la buscarías.
Ejercicio 3: convertir CloudTrail en detección proactiva
Marta quiere pasar de investigar después a detectar en el momento. Define cinco detecciones adicionales a las cinco alarmas de la lección, orientadas al riesgo real de MercadoFresco:
- Alguien crea una clave de acceso permanente para un usuario IAM (después de 04-01, nadie debería).
- Alguien modifica la política de la clave
alias/mercadofresco-datos. - Un rol de aplicación se usa desde una IP fuera de la VPC.
- Alguien desactiva el cifrado de un bucket o activa el acceso público.
- Se crea un recurso en una región que MercadoFresco no usa.
Para cada una: escribe el patrón del filtro de métricas, decide el umbral y los periodos de la alarma, di si debe despertar a alguien de madrugada o solo generar un correo, y explica qué falso positivo esperas y cómo lo evitarías. Para las que no se puedan resolver con un filtro de métricas, indica qué herramienta usarías (y adelanta cuál será la lección que la cubra).
Soluciones
Solución 1
Cálculo de partida. Registrarlo todo:
| Recurso | Eventos | Coste |
|---|---|---|
| Catálogo | 40.000.000 | 40,00 USD |
| Registros web | 8.000.000 | 8,00 USD |
| Informes | 180.000 | 0,18 USD |
| Lambdas | 1.280.000 | 1,28 USD |
| Copias | 2.400 | 0,00 USD |
| Total | 49,5 M | 49,46 USD |
Diez veces el presupuesto, y con el 97 % del gasto en lecturas de fotos de tomates.
Selectores propuestos:
[
{
"Name": "Gestion completa",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Management"] }
]
},
{
"Name": "Copias de base de datos: TODO",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::mercadofresco-copias-basedatos/"] }
]
},
{
"Name": "Informes de negocio: TODO",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::mercadofresco-informes-analitica/"] }
]
},
{
"Name": "Catalogo: SOLO escrituras",
"FieldSelectors": [
{ "Field": "eventCategory", "Equals": ["Data"] },
{ "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
{ "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::mercadofresco-catalogo-fotos/"] },
{ "Field": "readOnly", "Equals": ["false"] }
]
}
]Coste resultante:
| Selector | Eventos/mes | Coste |
|---|---|---|
| Gestión | ~150.000 | 0,00 USD (primera copia gratis) |
| Copias de BD | 2.400 | 0,002 USD |
| Informes | 180.000 | 0,18 USD |
| Catálogo, solo escrituras (0,1 %) | ~40.000 | 0,04 USD |
| Total | 222.400 | ~0,22 USD/mes |
Muy por debajo de los 5 USD y cumpliendo los tres requisitos.
Qué queda fuera y por qué:
- Lecturas del catálogo (40 M): son fotos públicas de producto servidas por CloudFront. Su lectura no tiene ningún valor de seguridad y cuesta 40 USD. Si algún día hiciera falta analizar patrones de acceso, están los registros de acceso de S3 y los de CloudFront, mucho más baratos.
mercadofresco-registros-web(8 M): es el ALB escribiendo sus propios registros. Registrar que el ALB escribe sus registros es recursivo e inútil: 8 USD por confirmar lo que ya sabemos.- Invocaciones de Lambda (1,28 M): 1,28 USD, y ya tenemos mejor visibilidad de esas funciones con
las métricas de
AWS/Lambda, sus registros y las trazas de X-Ray (05-02). CloudTrail solo diría «se invocó», que es lo menos informativo de todo.
Alarmas sobre los eventos de datos que sí registramos:
# 1. Cualquier lectura humana de una copia de la base de datos
aws logs put-metric-filter \
--log-group-name /aws/cloudtrail/mercadofresco \
--filter-name filtro-lectura-copias \
--filter-pattern '{ $.eventName = "GetObject" && $.requestParameters.bucketName = "mercadofresco-copias-basedatos" && $.userIdentity.type != "AWSService" }' \
--metric-transformations \
metricName=LecturaCopiasBD,metricNamespace=MercadoFresco/Seguridad,\
metricValue=1,defaultValue=0 \
--profile mercadofresco-dev --region eu-west-1Con umbral 0: nadie tiene por qué descargar una copia de la base de datos sin que alguien se entere.
# 2. Descarga masiva de informes: umbral, no cero
aws cloudwatch put-metric-alarm \
--alarm-name mercadofresco-descarga-masiva-informes \
--namespace MercadoFresco/Seguridad --metric-name DescargaInformes \
--statistic Sum --period 300 --evaluation-periods 1 \
--threshold 500 --comparison-operator GreaterThanThreshold \
--treat-missing-data notBreaching \
--alarm-actions arn:aws:sns:eu-west-1:111122223333:alertas-mercadofresco \
--profile mercadofresco-dev --region eu-west-1Aquí el umbral no puede ser 0: Sara lee informes todos los días, es su trabajo. 500 objetos en cinco minutos no es leer un informe, es descargarse el almacén entero.
# 3. Escrituras en el catalogo fuera del proceso normal
# Solo rol-mercadofresco-tienda y rol-lambda-miniaturas deben escribir ahi.
--filter-pattern '{ ($.eventName = "PutObject" || $.eventName = "DeleteObject") && $.requestParameters.bucketName = "mercadofresco-catalogo-fotos" && $.userIdentity.sessionContext.sessionIssuer.userName != "rol-mercadofresco-tienda" && $.userIdentity.sessionContext.sessionIssuer.userName != "rol-lambda-miniaturas" }'Este último es el patrón más valioso de los tres: lista blanca de identidades esperadas, alarma sobre todo lo demás. Es más robusto que enumerar lo malo, porque cubre también lo que no has previsto.
Solución 2
Lectura inicial de los datos. Tres señales muy claras, antes de ejecutar ninguna consulta:
- 340
AccessDenieden 17 minutos sobre 7 servicios distintos, incluyendoiamyorganizations. Eso no es una aplicación con un permiso mal puesto: una aplicación falla siempre en la misma llamada. Esto es enumeración sistemática de permisos. IAMUsercon clave permanente, exactamente lo que 04-01 dice que no debe existir. Un usuario de «integración con proveedor» es el vector clásico: clave que se creó una vez, se puso en un fichero de configuración, y nadie ha rotado en tres años.- 20 horas de silencio y luego 6 llamadas con éxito, quirúrgicas, sobre los informes de negocio. Ese espaciado es el patrón: primero se mapea qué se puede hacer, luego se vuelve a por lo que funciona.
Consulta 1: toda la actividad de ese usuario, no solo la del fin de semana.
SELECT eventtime, eventsource, eventname, errorcode,
sourceipaddress, useragent, awsregion
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026'
AND userIdentity.userName = 'integracion-proveedor'
ORDER BY eventtime DESC;Qué busco: cuándo empezó todo, si esa IP había aparecido antes, y qué hacía este usuario
normalmente. Si su actividad legítima es un PutObject diario a las 06:00 desde otra IP, todo lo del
fin de semana es intrusión.
Consulta 2: qué tuvo éxito. Es lo único que importa de verdad.
SELECT eventtime, eventsource, eventname,
json_extract_scalar(requestParameters, '$.bucketName') AS bucket,
json_extract_scalar(requestParameters, '$.key') AS objeto,
sourceipaddress
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026' AND mes = '07'
AND userIdentity.userName = 'integracion-proveedor'
AND errorcode IS NULL
ORDER BY eventtime;Qué busco: el alcance exacto de la brecha. Los 340 fallos son ruido; los 6 éxitos son el
incidente. Aquí sale qué tres objetos concretos se descargaron de
mercadofresco-informes-analitica, y eso determina si hay datos personales implicados y si hay
obligación de notificar.
Consulta 3: la clave de acceso, para ver si se usó desde más sitios.
SELECT sourceipaddress, useragent, awsregion,
count(*) AS llamadas,
min(eventtime) AS primera, max(eventtime) AS ultima
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026'
AND userIdentity.accessKeyId = 'AKIA...'
GROUP BY 1, 2, 3
ORDER BY primera;Qué busco: si la clave se usa también desde la IP legítima del proveedor, está comprometida pero
sigue en uso legítimo, y desactivarla romperá una integración. Si solo se usa desde 203.0.113.201
desde hace un mes, el proveedor ya no la usa y desactivarla no rompe nada.
Consulta 4: de dónde salió esa clave y cuándo.
SELECT eventtime, userIdentity.arn AS quien_la_creo, sourceipaddress
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE eventname IN ('CreateAccessKey', 'CreateUser')
AND json_extract_scalar(responseElements, '$.accessKey.userName') = 'integracion-proveedor';Qué busco: si la clave se creó hace dos años y nunca se rotó, o si se creó el viernes pasado, en cuyo caso el compromiso es más profundo: alguien ya tenía acceso y se creó una puerta trasera.
Consulta 5: correlación por IP, más allá de este usuario.
SELECT eventtime, userIdentity.arn, eventname, errorcode
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE anio = '2026'
AND sourceipaddress = '203.0.113.201'
ORDER BY eventtime;Qué busco: si esa IP ha usado otras identidades, el alcance es mucho mayor.
Contención, en este orden exacto:
| # | Acción | Por qué en este orden |
|---|---|---|
| 1 | Desactivar la clave, no borrarla: aws iam update-access-key --status Inactive |
Corta el acceso conservando la evidencia. Borrarla destruye información |
| 2 | Congelar la evidencia: validate-logs + copiar los ficheros del periodo |
Antes de tocar nada más |
| 3 | Revisar qué permisos tenía el usuario y qué pudo haber hecho | Define el peor caso posible |
| 4 | Determinar qué se descargó exactamente (consulta 2) | Decide si hay notificación obligatoria |
| 5 | Avisar a dirección y a cumplimiento | Si hay datos personales, hay plazos legales |
| 6 | Buscar persistencia: CreateUser, CreateAccessKey, CreateRole, PutUserPolicy en el periodo |
Un atacante deja puertas traseras |
| 7 | Rotar credenciales relacionadas y revisar el secreto de mfadmin |
Por si hubo movimiento lateral |
| 8 | Solo entonces, contactar con el proveedor | Puede que el comprometido sea él |
Qué significa que 340 fallaran y 6 tuvieran éxito. Es la mejor noticia posible: el mínimo privilegio funcionó. El trabajo de 04-01 es lo que impidió que esas 340 llamadas —a IAM, a KMS, a Secrets Manager, a RDS, a Organizations— hicieran daño. La brecha se limitó a los permisos que ese usuario tenía legítimamente, que era leer informes.
Y a la vez es la peor noticia sobre el diseño: ese usuario nunca debió tener una clave permanente.
El acceso de un proveedor debe hacerse con un rol asumible con condición de identidad externa
(sts:ExternalId, 04-01), con credenciales temporales que caducan solas. Ese es el arreglo de fondo.
Lo que CloudTrail NO puede decirte:
| Pregunta | Dónde buscar |
|---|---|
| ¿Qué contenían exactamente los objetos descargados? | En el propio bucket, mirando esas claves |
| ¿Cómo se filtró la clave? | Repositorios de código, portátiles, el proveedor |
| ¿Hubo acceso a la base de datos con credenciales robadas? | Registros de PostgreSQL (05-01) |
| ¿Se exfiltró por red desde una instancia? | VPC Flow Logs (03-01) |
| ¿Quién es la persona detrás de esa IP? | Nadie en AWS. Es trabajo policial |
| ¿Se modificó algo dentro de la aplicación? | Registros de aplicación (05-01) |
Esa tabla es la lección más importante del ejercicio: CloudTrail es una pieza de la investigación, no la investigación entera. Da el «quién, qué, cuándo, desde dónde» del plano de control de AWS. Todo lo demás está en otras fuentes, y por eso este módulo entero tiene sentido como conjunto.
Solución 3
Detección 1: creación de clave de acceso permanente.
{ ($.eventName = "CreateAccessKey") || ($.eventName = "CreateUser") || ($.eventName = "CreateLoginProfile") }| Parámetro | Valor | Justificación |
|---|---|---|
| Umbral | 0 | Después de 04-01, no debería crearse ninguna |
| Periodo / evaluación | 300 s / 1 | Inmediato |
| ¿Despierta? | Sí | Es el paso clásico de persistencia tras un compromiso |
| Falso positivo esperado | Alta legítima de un usuario nuevo | Se avisa antes por el canal del equipo; el aviso se marca como esperado |
Detección 2: cambio en la política de la clave KMS.
{ ($.eventSource = "kms.amazonaws.com") && (($.eventName = "PutKeyPolicy") || ($.eventName = "ScheduleKeyDeletion") || ($.eventName = "DisableKey") || ($.eventName = "DisableKeyRotation")) }| Parámetro | Valor | Justificación |
|---|---|---|
| Umbral | 0 | Cambiar la política de alias/mercadofresco-datos es un evento de altísimo impacto |
| ¿Despierta? | Sí, siempre | ScheduleKeyDeletion sobre esa clave dejaría ilegibles las copias y la base de datos. Es el evento más destructivo de toda la cuenta |
| Falso positivo | Cambio planificado | Se hace con aviso previo y se anota |
Merece subrayarse: en 04-02 vimos que borrar una clave KMS tiene un periodo de espera de 7 a 30 días precisamente para dar tiempo a reaccionar. Esta alarma es lo que convierte ese periodo de espera en una defensa real: sin ella, la espera pasa sin que nadie se entere.
Detección 3: rol de aplicación usado desde fuera de la VPC.
Un filtro de métricas no puede hacer esto bien. La sintaxis de patrones de CloudWatch Logs no soporta comparaciones de rango de IP ni negación de prefijos de forma fiable. Se puede aproximar:
{ ($.userIdentity.sessionContext.sessionIssuer.userName = "rol-mercadofresco-tienda") && ($.sourceIPAddress != "10.0.*") }…pero es frágil y generará falsos positivos, porque las llamadas a través de un endpoint de VPC
(vpce-mercadofresco-s3) o desde ciertos servicios aparecen con otras direcciones.
La herramienta correcta es otra, y aquí hay que ser honesto:
- Amazon GuardDuty tiene un hallazgo específico para esto:
UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration, que detecta credenciales de rol de instancia usadas desde fuera de AWS. Es exactamente este caso y funciona sin configuración. - Alternativamente, una condición en la política de confianza del rol con
aws:SourceVpc, que directamente impide el uso desde fuera en lugar de detectarlo. Prevenir es mejor que detectar (04-01).
GuardDuty y Security Hub se ven en visión general en 05-04.
Detección 4: cifrado desactivado o acceso público en un bucket.
{ ($.eventSource = "s3.amazonaws.com") && (($.eventName = "DeleteBucketEncryption") || ($.eventName = "PutBucketAcl") || ($.eventName = "DeletePublicAccessBlock") || ($.eventName = "PutBucketPolicy")) }| Parámetro | Valor | Justificación |
|---|---|---|
| Umbral | 0 | Ninguna de esas cuatro debería ocurrir en producción |
| ¿Despierta? | Correo inmediato; SMS solo si es sobre buckets críticos | PutBucketPolicy ocurre legítimamente al desplegar |
| Falso positivo | Despliegues de infraestructura como código | Se filtra por identidad: si viene del rol del pipeline (módulo 8), no alarma |
Pero esta detección tiene un límite serio: solo detecta el cambio, no el estado. Si el cifrado ya estaba desactivado desde antes de crear la alarma, nunca saltará. Y no puede corregirlo.
Para esto existe una herramienta específica, que es la lección siguiente: AWS Config. Config evalúa el estado actual de todos los recursos contra reglas, detecta las desviaciones existentes —no solo las nuevas— y puede corregirlas automáticamente. Es exactamente la quinta pregunta que dejó abierta el módulo 4.
Detección 5: recurso creado en una región no usada.
{ ($.awsRegion != "eu-west-1") && ($.awsRegion != "us-east-1") && ($.readOnly = "false") && ($.userIdentity.type != "AWSService") }| Parámetro | Valor | Justificación |
|---|---|---|
| Umbral | 0 | MercadoFresco solo usa eu-west-1 y us-east-1 (CloudFront, WAF, ACM) |
| ¿Despierta? | Sí | Crear recursos en regiones no usadas es la firma clásica del minado de criptomonedas con credenciales robadas |
| Falso positivo | Alguien probando algo | Muy raro, y merece la conversación |
Esta detección depende por completo de que el trail sea multirregión. Sin --is-multi-region-trail
no verías absolutamente nada, y es el argumento definitivo para activarlo.
Y la prevención, mejor que la detección: una política de control de servicios (SCP) que deniegue todo fuera de las regiones permitidas. Requiere Organizations: 09-04.
Resumen de las cinco:
| # | Detección | ¿Filtro de métricas? | Herramienta correcta |
|---|---|---|---|
| 1 | Clave de acceso creada | Sí | CloudTrail + alarma |
| 2 | Política de KMS modificada | Sí | CloudTrail + alarma |
| 3 | Rol usado fuera de la VPC | No | GuardDuty, o condición en la política de confianza |
| 4 | Cifrado / acceso público de bucket | Parcial | AWS Config (05-04) |
| 5 | Recurso en región no usada | Sí | CloudTrail multirregión + alarma; mejor SCP (09-04) |
La conclusión del ejercicio: CloudTrail detecta eventos, no estados. Es perfecto para «alguien ha hecho X», e insuficiente para «el bucket Y está mal configurado desde hace tres meses». Esa segunda pregunta necesita otra herramienta, y es la siguiente lección.
Conclusión
MercadoFresco ya sabe quién hizo qué. Tienes clara la diferencia esencial que define este servicio: CloudTrail registra llamadas a la API de AWS; CloudWatch Logs registra lo que dice tu aplicación. Uno responde «¿quién borró el bucket?», el otro «¿por qué falló el pago?». No compiten: cubren universos distintos, y buscar en el equivocado cuesta horas.
Sabes que el historial de eventos de 90 días está siempre ahí, gratis, y conoces sus cinco
limitaciones —90 días, solo gestión, un atributo por búsqueda, sin SQL, sin inmutabilidad— que son
exactamente las razones para crear un trail. Has creado trail-mercadofresco con las cuatro
banderas que importan: multirregión (gratis, y sin ella un atacante crea recursos donde nadie
mira), eventos globales, validación de integridad y cifrado con alias/mercadofresco-datos.
Y sabes que create-trail no arranca el registro: hay que llamar a start-logging y comprobarlo
con get-trail-status.
Has montado el bucket mercadofresco-auditoria-cloudtrail con las cuatro capas de protección —política
con Deny de borrado, versionado, Object Lock en modo COMPLIANCE que ni la cuenta raíz puede
saltarse, y la cuenta separada como paso pendiente de 09-04— porque un registro de auditoría que el
atacante puede borrar no es un registro de auditoría. Y conoces la cadena de resúmenes firmados que
hace imposible alterar un fichero sin romperla, con validate-logs ejecutado el primer lunes de cada
mes.
Sabes leer un evento completo: eventTime, eventSource, eventName, sourceIPAddress,
userAgent, requestParameters, responseElements (nulo en las lecturas) y errorCode, que solo
aparece cuando la llamada falla —y que es una de las propiedades más valiosas de CloudTrail, porque un
atacante probando puertas deja un reguero de AccessDenied—. Dominas los seis tipos de
userIdentity, con AssumedRole como el más frecuente y el más confuso, y sabes que el nombre
de sesión es lo único que convierte un rol compartido en una persona identificable, y que es texto
libre: la identidad real está en el AssumeRole correspondiente.
Y has respondido a la tercera pregunta del módulo 4. Quién descifró la última copia: tres eventos
de Decrypt, dos normales —RDS cifrando su copia automática y una instancia del ASG desde
10.0.11.24— y uno que no lo era: rol-restauracion-copias, a las 03:42, desde
198.51.100.77, sin MFA, sobre snapshot-2026-07-27. Gracias al encryptionContext de 04-02,
que es lo que convierte un evento genérico de KMS en «alguien descifró la copia del 27 de julio de
mercadofresco-pedidos».
Sabes distinguir eventos de gestión (primera copia gratis) de eventos de datos (0,10 USD por 100.000, y volumen millonario), y has escrito selectores avanzados que bajan la factura de 40 USD a 0,06 USD sin perder nada: todo sobre las copias y los informes, solo escrituras sobre el catálogo, nada sobre los registros web. Conoces Insights por 0,53 USD al mes, y sus dos limitaciones: 7 días de línea base y solo detecta anomalías de volumen.
Has enviado el trail también a CloudWatch Logs —90 días ahí, 7 años en S3, cada destino con su
propósito— y has montado las cinco alarmas de seguridad estándar: uso de la cuenta raíz, cambios
en el trail (la más importante de todas, porque avisa de que la auditoría está siendo atacada),
descifrado de copias excluyendo al propio RDS, picos de AccessDenied con umbral realista, y cambios
de configuración de seguridad. Y sabes consultar con Athena —con el WHERE de partición siempre
puesto y BytesScannedCutoffPerQuery como red de seguridad— para responder quién asumió un rol, quién
leyó el secreto de mfadmin, qué llamadas fallaron y qué IP desconocidas han aparecido; con
CloudTrail Lake como alternativa gestionada, evaluada y descartada con criterio.
Has recorrido una investigación completa: congelar la evidencia antes de nada, encontrar el
AssumeRole real, reconstruir la sesión entera por accessKeyId, interpretar el patrón, hablar con
la persona antes de concluir nada —CloudTrail dice qué pasó, no por qué— y cerrar con acciones
correctivas con responsable y fecha, entre ellas la más importante: hacer que probar restauraciones
sea fácil y visible en lugar de prohibirlo. Y conoces IAM Access Analyzer generando políticas de
mínimo privilegio a partir del uso real, que cierra el círculo con 04-01. Todo por 1,59 USD al mes.
Pero fíjate en lo que este servicio no puede hacer, porque el ejercicio 3 lo dejó claro:
CloudTrail detecta eventos, no estados. Sabe que alguien llamó a DeleteBucketEncryption esta
mañana. No sabe que el bucket mercadofresco-registros-web lleva cinco meses sin cifrado porque nunca
se configuró. No sabe que hay un grupo de seguridad con 0.0.0.0/0 en el puerto 22 desde una prueba de
marzo. No puede decir cuántos recursos incumplen la política de etiquetado obligatorio de
MercadoFresco. Y desde luego no puede arreglarlo solo.
Esa es la quinta y última pregunta que dejó abierta el módulo 4: nada avisa si alguien desactiva el
cifrado de un bucket o abre un grupo de seguridad al mundo. En la lección 05-04, «AWS Config»,
veremos qué es el elemento de configuración de un recurso y su línea temporal, en qué se diferencia
exactamente de CloudTrail —quién hizo la llamada frente a cómo quedó el recurso—, cómo se activa el
grabador de configuración con su canal de entrega, las reglas gestionadas que MercadoFresco necesita
(s3-bucket-server-side-encryption-enabled, restricted-ssh, required-tags y compañía), las reglas
propias con Lambda y con Guard, la remediación automática con documentos de Systems Manager que
vuelve a cifrar un bucket o cierra un grupo de seguridad sin que nadie intervenga, los paquetes de
conformidad alineados con CIS y PCI DSS, y el coste real por elemento de configuración, que es la
partida que más fácilmente se dispara de todo este módulo.
Curso de AWS
Módulo 1: Introducción a AWS
- ¿Qué es AWS?
- Configuración de tu cuenta de AWS
- Infraestructura global de AWS
- Consola de administración de AWS
- AWS CLI y SDKs
Módulo 2: Servicios principales de AWS
Módulo 3: Redes y entrega de contenido
- Amazon VPC
- Grupos de seguridad y listas de control de acceso
- Elastic Load Balancing
- Amazon CloudFront
- Route 53
Módulo 4: Seguridad e identidad
- AWS Identity and Access Management (IAM)
- AWS Key Management Service (KMS)
- Secrets Manager y Parameter Store
- AWS Shield
- AWS WAF
Módulo 5: Monitorización y gestión
- Amazon CloudWatch
- AWS X-Ray y trazabilidad distribuida
- AWS CloudTrail
- AWS Config
- AWS Trusted Advisor
Módulo 6: Bases de datos
- Cómo elegir la base de datos adecuada
- Amazon DynamoDB
- Amazon Aurora
- Amazon Redshift
- Amazon ElastiCache
Módulo 7: Integración de aplicaciones
- Amazon SQS
- Amazon SNS
- Amazon EventBridge
- AWS Step Functions
- Patrones de integración: idempotencia, reintentos y colas de mensajes fallidos
