A 04-02 vam xifrar les còpies de seguretat de mercadofresco-pedidos amb la clau alias/mercadofresco-datos, i vam dir que KMS registra escrupolosament cada operació criptogràfica. A 04-03 vam desar la contrasenya de mfadmin al secret mercadofresco/produccion/rds/mfadmin i vam dir que cada lectura queda anotada. A 04-01 vam crear el rol rol-mercadofresco-tienda i vam dir que cada AssumeRole deixa rastre.

Cap d'aquestes tres coses no és mentida. Totes estan registrades. I ningú no les ha mirat mai.

Aquest registre es diu AWS CloudTrail, i respon a una pregunta que ni CloudWatch ni X-Ray no poden contestar. CloudWatch sap el que la teva aplicació diu de si mateixa. X-Ray sap per on va passar una petició d'un client. CloudTrail sap qui va cridar l'API d'AWS, quan, des d'on, amb quins paràmetres i amb quin resultat. És la diferència entre saber que la botiga va lenta i saber que algú, a les 03:42 d'un dimarts, des d'una IP d'un país on no tens oficines, va cridar kms:Decrypt sobre la còpia de la base de dades.

Aquesta lliçó munta el trail trail-mercadofresco, ensenya a llegir un esdeveniment real, i contesta per fi la tercera pregunta que va deixar oberta el mòdul 4.

Avís de compliment. Els registres d'auditoria solen tenir requisits legals de conservació. Segons el sector i el país, poden anar de 6 mesos a 10 anys, i en alguns casos han de ser inalterables i estar en custòdia separada. Aquest material és didàctic: les decisions de retenció, immutabilitat i accés als registres d'auditoria d'un sistema real les ha de validar un professional de compliment o l'assessor legal de la teva organització. No les prenguis tu a partir d'un curs.

Contingut

  1. Què registra CloudTrail i què no
  2. CloudTrail enfront de CloudWatch Logs
  3. L'historial d'esdeveniments de 90 dies, gratuït
  4. Crear el trail trail-mercadofresco
  5. El bucket que ningú no ha de poder esborrar
  6. Validació d'integritat dels fitxers
  7. Anatomia d'un esdeveniment: el JSON comentat
  8. userIdentity: els sis tipus i com es llegeixen
  9. Qui va desxifrar l'última còpia
  10. Esdeveniments de gestió enfront d'esdeveniments de dades
  11. Esdeveniments d'Insights
  12. Trails multiregió i d'organització
  13. Enviar el trail a CloudWatch Logs
  14. Filtres de mètriques i alarmes de seguretat
  15. Consultar amb Athena
  16. Les consultes que responen preguntes reals
  17. CloudTrail Lake
  18. Investigació d'un incident, pas a pas
  19. Relació amb IAM Access Analyzer
  20. Retenció, cost i neteja

Què registra CloudTrail i què no

CloudTrail registra crides a l'API d'AWS. Totes. Tant se val que les faci una persona a la consola, la CLI, un SDK, un servei d'AWS actuant en nom teu o una funció Lambda: per sota tot són crides HTTPS signades a les API d'AWS, i CloudTrail les veu totes.

El que no registra:

No registra Exemple On és
El que fa la teva aplicació per dins comanda 48213 confirmada CloudWatch Logs (05-01)
El trànsit HTTP dels teus clients GET /buscar?q=tomate Registres de l'ALB / CloudFront
Consultes SQL a la teva base de dades SELECT * FROM pedidos Registres d'RDS
Trànsit de xarxa entre instàncies Paquets TCP VPC Flow Logs (03-01)
Peticions a objectes d'S3 GET productos/tomate.jpg Esdeveniments de dades (cal activar-los)
Contingut d'un secret El valor de la contrasenya Mai no es registra

Aquesta última fila importa: CloudTrail registra que algú va cridar GetSecretValue sobre mercadofresco/produccion/rds/mfadmin, però no registra la contrasenya. Els camps sensibles s'ometen o s'ofusquen per disseny. El mateix amb kms:Decrypt: registra la crida i l'identificador de clau, no el text clar.

I una propietat estructural que cal entendre: CloudTrail sempre està encès. No és una cosa que s'«activa»: els últims 90 dies d'esdeveniments de gestió són disponibles gratis al teu compte des del primer dia, encara que no hagis creat cap trail. El que s'activa és la persistència.

CloudTrail enfront de CloudWatch Logs

És la confusió número u d'aquest mòdul, i es mereix una taula:

CloudTrail CloudWatch Logs
Registra Crides a l'API d'AWS El que escriu la teva aplicació
Origen de la dada El pla de control d'AWS El teu codi, l'agent, els serveis
Pregunta que respon Qui ha fet què? Què ha passat a dins?
Exemple mercadofresco-admin va esborrar el bucket ERROR pagament rebutjat comanda 48213
Àmbit Compte (i organització) Regió, grup de registres
Actiu per defecte , 90 dies gratis Només si publiques
Format JSON estructurat i fix El que tu escriguis
Retenció 90 dies / el que duri el bucket El que configuris
Es manipula Molt difícil (amb les proteccions) Fàcil, si tens permisos
Ús principal Auditoria, forense, compliment Operació, depuració

Un cas concret per fixar-ho. Algú esborra el bucket mercadofresco-registros-web:

  • CloudWatch Logs: res. La teva aplicació no ha escrit res perquè no ha estat la teva aplicació.
  • CloudTrail: un esdeveniment DeleteBucket, amb la identitat que ho va fer, l'hora exacta, la IP d'origen, l'agent d'usuari (aws-cli/2.15.0 o console.amazonaws.com), i si es va fer amb MFA.

I el cas invers. La botiga retorna 500 en confirmar una comanda:

  • CloudTrail: res. Ningú no va cridar cap API d'AWS de manera anòmala.
  • CloudWatch Logs: la traça completa de l'error.

No competeixen: cobreixen universos diferents.

L'historial d'esdeveniments de 90 dies, gratuït

Abans de crear res, hi ha una cosa que ja funciona:

# Els ultims esdeveniments d'escriptura de la regio
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

# Tot el que ha fet un usuari concret
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

# Totes les crides a un esdeveniment concret
aws cloudtrail lookup-events \
  --lookup-attributes AttributeKey=EventName,AttributeValue=Decrypt \
  --profile mercadofresco-dev --region eu-west-1

Els atributs de cerca disponibles són limitats i cal conèixer-los, perquè és el que es pot fer sense infraestructura: EventId, EventName, EventSource, ReadOnly, ResourceName, ResourceType, Username, AccessKeyId.

Les cinc limitacions de l'historial gratuït, que són exactament les raons per crear un trail:

  1. Només 90 dies. Una investigació d'un incident detectat tard es queda sense dades.
  2. Només esdeveniments de gestió. No hi ha esdeveniments de dades (accessos a objectes d'S3, invocacions de Lambda).
  3. Un sol atribut de cerca per consulta. No pots creuar «aquest usuari» i «aquesta acció».
  4. No és exportable ni consultable amb SQL. No hi ha anàlisi seriosa possible.
  5. No és immutable ni verificable. No serveix com a prova en una auditoria formal.

Crear el trail trail-mercadofresco

Un trail persisteix els esdeveniments en un bucket d'S3, indefinidament, i permet tot l'anterior.

Pas 1: el bucket dedicat.

aws s3api create-bucket \
  --bucket mercadofresco-auditoria-cloudtrail \
  --region eu-west-1 \
  --create-bucket-configuration LocationConstraint=eu-west-1 \
  --profile mercadofresco-dev

# Bloqueig total d'acces public (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

# Versionat: imprescindible per a Object Lock i per recuperar esborrats
aws s3api put-bucket-versioning \
  --bucket mercadofresco-auditoria-cloudtrail \
  --versioning-configuration Status=Enabled \
  --profile mercadofresco-dev

# Xifratge amb la clau gestionada per 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-dev

Aquest BucketKeyEnabled: true no és cosmètic: redueix fins a un 99 % les crides a KMS —i el seu cost— quan s'escriuen milers d'objectes petits, que és exactament el que fa CloudTrail. Ho vam veure a 04-02.

Pas 2: la política del bucket. CloudTrail necessita permís per escriure:

{
  "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" } }
    }
  ]
}

Les condicions aws:SourceArn són importants i no sempre s'hi posen: sense elles, en teoria un trail d'un altre compte podria escriure al teu bucket (el confused deputy que vam veure a 04-01).

Pas 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

# I ARRENCAR-LO: create-trail NO l'inicia.
aws cloudtrail start-logging \
  --name trail-mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

L'error de l'any: create-trail crea el trail però no comença a registrar. Cal cridar start-logging. Hi ha comptes amb trails creats fa anys que mai no han gravat una línia. Comprova-ho sempre:

aws cloudtrail get-trail-status --name trail-mercadofresco \
  --query '[IsLogging,LatestDeliveryTime,LatestDeliveryError]' \
  --profile mercadofresco-dev --region eu-west-1

Les quatre banderes de la comanda, explicades:

Bandera Què fa Per què?
--is-multi-region-trail Registra les 19+ regions Un atacant crea recursos a ap-south-1, on ningú no mira
--include-global-service-events Inclou IAM, STS, CloudFront, Route 53 Són globals i es registren a us-east-1
--enable-log-file-validation Genera fitxers de resum signats Detecta manipulació
--kms-key-id Xifra els fitxers amb la teva clau Separació de funcions (04-02)

La primera és la més important des del punt de vista de seguretat. Un trail d'una sola regió és una càmera que només enfoca la porta principal.

El bucket que ningú no ha de poder esborrar

Un registre d'auditoria que l'atacant pot esborrar no és un registre d'auditoria. És la primera cosa que fa qualsevol que sap el que fa: entrar, esborrar el rastre, seguir.

Hi ha quatre nivells de protecció, i convé entendre què protegeix cadascun:

Nivell Què impedeix Debilitat
1. Política de bucket amb Deny Esborrar objectes Qui pugui canviar la política, la treu
2. Versionat + MFA Delete Esborrar versions sense MFA física Només ho activa el compte arrel
3. Object Lock en mode COMPLIANCE Esborrar, fins i tot el compte arrel Cal activar-lo en crear el bucket
4. Compte separat Que l'atacant de producció arribi al bucket Requereix Organizations (09-04)

Nivell 3, Object Lock, és el que de debò tanca la porta:

# Nomes es pot activar en un bucket amb versionat.
# En buckets ja existents cal demanar-ho al suport d'AWS;
# l'habitual es crear-lo amb --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-dev

Els dos modes, i la diferència és enorme:

Mode Qui pot escurçar la retenció
GOVERNANCE Qui tingui s3:BypassGovernanceRetention
COMPLIANCE Ningú. Ni el compte arrel. Ni el suport d'AWS.

Aquests 2.555 dies són 7 anys. I aquí hi ha el perill: amb COMPLIANCE, cada objecte que CloudTrail escrigui serà inesborrable durant set anys, i el pagaràs durant set anys. Si t'equivoques de valor, no hi ha marxa enrere.

Aquesta és exactament la decisió que ha de validar un professional de compliment. El període de retenció dels registres d'auditoria depèn del teu sector, el teu país i les teves obligacions contractuals. No el triïs per intuïció, i no copiïs el 2555 d'aquest curs.

Nivell 2, MFA Delete, només el pot activar el compte arrel amb un dispositiu MFA, i per això no encaixa amb el principi de 04-01 de no usar l'arrel. MercadoFresco no el fa servir; usa Object Lock.

Nivell 4, compte separat, és la resposta professional completa: el bucket d'auditoria viu en un compte de registre diferent, al qual el compte de producció només pot escriure. Encara que algú comprometi del tot 111122223333, els registres queden fora del seu abast. Requereix AWS Organizations, que és la lliçó 09-04. MercadoFresco ho té anotat com el pas següent.

I una defensa complementària que es munta avui mateix: una alarma que avisi si algú atura o esborra el trail. És a la secció de filtres de mètriques.

Validació d'integritat dels fitxers

Amb --enable-log-file-validation, CloudTrail fa una cosa elegant: cada hora publica un fitxer de resum (digest) que conté el hash SHA-256 de cada fitxer de registre d'aquella hora, més el hash del resum anterior. És una cadena: cada resum signa l'anterior.

flowchart LR
    D1["Resum 10:00<br/>hash dels fitxers<br/>+ hash del resum 09:00"] --> D2["Resum 11:00<br/>hash dels fitxers<br/>+ hash del resum 10:00"]
    D2 --> D3["Resum 12:00<br/>..."]
    F1["registro-10-01.json.gz"] -.-> D1
    F2["registro-10-02.json.gz"] -.-> D1
    F3["registro-11-01.json.gz"] -.-> D2

Conseqüència pràctica: no es pot alterar un fitxer de registre, ni esborrar-lo, ni substituir un resum, sense trencar la cadena. Els resums estan signats amb la clau privada de CloudTrail, així que tampoc no es poden regenerar.

La verificació:

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-1

Una sortida sana acaba amb Results requested for ... Results found for N digest files ... All files verified. Qualsevol menció a fitxers modificats o absents és una troballa de seguretat immediata.

La Marta l'executa el primer dilluns de cada mes i desa la sortida. En una auditoria formal, aquesta sortida és el que demostra que els registres no s'han tocat.

Anatomia d'un esdeveniment: el JSON comentat

Aquest és un esdeveniment real de kms:Decrypt, que és exactament el que buscàvem. Comentat camp a camp:

{
  "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 camp a camp del que aquest esdeveniment ens està dient:

Camp Valor Què significa aquí
eventTime 03:42:17Z Les 3:42 de la matinada. Anòmal per si sol
userIdentity.type AssumedRole No va ser un usuari directe: algú va assumir un rol
sessionIssuer.userName rol-restauracion-copias Quin rol
principalId després dels : sesion-marta-copias Qui el va assumir: el nom de sessió
mfaAuthenticated "false" Sense MFA. Segon indicador
sourceIPAddress 198.51.100.77 Des d'on. Cal comprovar si és coneguda
userAgent aws-cli/2.15.30 Des de la CLI, no la consola. Va ser un script o una persona en terminal
eventName Decrypt L'operació
encryptionContext db-id, backup-id Què es va desxifrar: la còpia del 27 de juliol
readOnly true No va modificar res
errorCode (absent) Va tenir èxit
tlsDetails TLS 1.3 Metadades de la connexió

Tres camps mereixen comentari a part:

  • responseElements és null en les operacions de només lectura. En una operació d'escriptura —CreateBucket, RunInstances— conté el que va retornar l'API: l'ID del recurs creat. És el camp que et diu què es va crear.
  • errorCode i errorMessage apareixen només quan la crida va fallar. AccessDenied, UnauthorizedOperation, Client.InvalidParameterValue. Que CloudTrail registri també les crides que fallen és una de les seves propietats més valuoses: un atacant provant portes deixa un reguer d'AccessDenied que és el senyal més net de reconeixement.
  • encryptionContext és el que a 04-02 vam explicar com les «dades addicionals autenticades» del xifratge de sobre. Aquí es veu el seu valor de debò: és el que converteix un esdeveniment genèric de Decrypt en «algú va desxifrar la còpia del 27 de juliol de mercadofresco-pedidos». Sense ell, l'esdeveniment només diria que es va fer servir una clau.

userIdentity: els sis tipus i com es llegeixen

El camp userIdentity és on viu la resposta a «qui?», i té sis formes diferents:

type Significa On és el nom real
Root El compte arrel. Alarma sempre arn
IAMUser Un usuari IAM amb claus permanents userName
AssumedRole Algú va assumir un rol amb STS sessionContext.sessionIssuer.userName + nom de sessió
AWSService Un servei d'AWS actuant sol invokedBy
AWSAccount Un altre compte d'AWS accountId
FederatedUser Federació amb SAML / OIDC / Identity Center sessionContext
Unknown No determinat

El més habitual, i el més confús, és AssumedRole. La instància EC2 de MercadoFresco no apareix com a «instància»: apareix com el rol rol-mercadofresco-tienda amb un nom de sessió que és l'ID de la instància. I quan la Marta assumeix un rol des del seu usuari, apareix com el rol amb el nom de sessió que ella va triar.

D'aquí surt una de les pràctiques més útils de tot aquest mòdul: si a AssumeRole no hi poses un nom de sessió identificatiu, CloudTrail et dirà «algú amb el rol d'administració va esborrar el bucket» i no podràs saber qui. Amb --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-dev

A 04-01 vam introduir això de passada. Aquí es veu per què importa: el nom de sessió és el que converteix un rol compartit en una persona identificable. Amb AWS IAM Identity Center això es fa sol, amb el correu de l'usuari federat com a nom de sessió.

I una nota sobre AWSService. Molts esdeveniments legítims tenen invokedBy amb valors com autoscaling.amazonaws.com o dlm.amazonaws.com. Són AWS actuant en nom teu: l'ASG llançant una instància, DLM creant una instantània (02-02). No són alertes; són el sistema funcionant. Filtrar-los correctament és la meitat de la feina de reduir el soroll en una investigació.

Qui va desxifrar l'última còpia

Ara sí, la resposta a la pregunta del mòdul 4. La consulta directa contra l'historial de 90 dies:

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'

Sortida 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ínies i tres lectures completament diferents:

  1. rds-backup-service a les 03:00: és el mateix RDS xifrant la còpia automàtica. Normal, és la finestra de còpia que vam configurar a 02-04.
  2. rol-mercadofresco-tienda des de 10.0.11.24: una instància de l'ASG, IP privada de la subxarxa d'aplicació (03-01), desxifrant dades de l'aplicació. Normal.
  3. rol-restauracion-copias a les 03:42, des de 198.51.100.77, sense MFA, sobre la còpia snapshot-2026-07-27: això s'ha d'investigar.

I les preguntes que se'n deriven, en aquest ordre:

  • És 198.51.100.77 una IP coneguda? L'oficina? La VPN? El domicili d'algú?
  • Qui va assumir rol-restauracion-copias? El nom de sessió diu sesion-marta-copias, però això és una cadena que va triar qui va fer la crida, no una identitat verificada. Cal buscar l'esdeveniment AssumeRole corresponent per veure qui el va assumir de debò.
  • Hi ha un esdeveniment RestoreDBInstanceFromDBSnapshot a prop? Es va restaurar la còpia en algun lloc?
  • Hi va haver un AccessDenied abans, senyal de temptejar?

Això no és una resposta, és el principi d'una investigació. I aquesta és precisament la lliçó: CloudTrail no diu si alguna cosa va malament. Diu què va passar, amb prou precisió perquè una persona decideixi. La investigació completa és més avall.

Esdeveniments de gestió enfront d'esdeveniments de dades

És la distinció que decideix la factura de CloudTrail:

Esdeveniments de gestió Esdeveniments de dades
Què registren Operacions sobre recursos Operacions sobre el contingut
Exemples CreateBucket, RunInstances, AssumeRole, Decrypt GetObject, PutObject, Invoke, GetItem
Volum Baix: centenars o milers al dia Altíssim: milions
Primera còpia Gratis De pagament sempre
Cost 2 USD per 100.000 esdeveniments (còpies addicionals) 0,10 USD per 100.000 esdeveniments
Activat per defecte No

El càlcul que cal fer abans d'activar esdeveniments de dades a S3. MercadoFresco serveix unes 40 milions de peticions al mes a mercadofresco-catalogo-fotos (fotos de producte, encara que CloudFront en guarda la majoria a la memòria cau). Registrar-les totes:

40.000.000 × 0,10 / 100.000 = 40 USD/mes, més l'emmagatzematge a S3 d'un volum enorme de JSON, més el que costi consultar-ho amb Athena.

I no aporta gairebé res: són lectures públiques de fotos de tomàquets.

La regla és: activa esdeveniments de dades amb selectors d'abast mínim, només sobre el que importa.

aws cloudtrail put-event-selectors \
  --trail-name trail-mercadofresco \
  --advanced-event-selectors '[
    {
      "Name": "Esdeveniments de gestio complets",
      "FieldSelectors": [
        { "Field": "eventCategory", "Equals": ["Management"] }
      ]
    },
    {
      "Name": "Nomes les copies de la base de dades i els 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": "Escriptures al cataleg, no lectures",
      "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-1

El que aconsegueix aquesta configuració:

Bucket Es registra Volum/mes Cost
mercadofresco-copias-basedatos Tot: qui llegeix una còpia és crític ~2.000 0,00 USD
mercadofresco-informes-analitica Tot: dades de negoci de la Sara ~15.000 0,02 USD
mercadofresco-catalogo-fotos Només escriptures ~40.000 0,04 USD
mercadofresco-registros-web Res 0,00 USD
Total ~0,06 USD/mes

De 40 USD a 0,06 USD sense perdre res rellevant. Aquest readOnly: false sobre el catàleg és la decisió clau: llegir una foto de tomàquet no interessa a ningú; que algú pugi o esborri una foto sí, perquè és una modificació del contingut del lloc.

Els tipus de recurs disponibles per a esdeveniments de dades inclouen AWS::S3::Object, AWS::Lambda::Function, AWS::DynamoDB::Table (mòdul 6), AWS::SQS::Queue (07-01) i uns quants més.

Esdeveniments d'Insights

CloudTrail Insights analitza el volum de crides i detecta pics anòmals d'activitat d'escriptura o d'errors. No mira el contingut: mira el ritme.

Casos que detecta bé:

  • Un script mal escrit que crida DescribeInstances 40.000 vegades en una hora.
  • Un augment sobtat d'AccessDenied: algú provant permisos.
  • Un pic de TerminateInstances a les 4 de la matinada.
aws cloudtrail put-insight-selectors \
  --trail-name trail-mercadofresco \
  --insight-selectors '[
    {"InsightType": "ApiCallRateInsight"},
    {"InsightType": "ApiErrorRateInsight"}
  ]' \
  --profile mercadofresco-dev --region eu-west-1

Cost: 0,35 USD per cada 100.000 esdeveniments de gestió analitzats. Amb ~150.000 esdeveniments mensuals, MercadoFresco paga uns 0,53 USD/mes. És de les coses més barates d'aquest mòdul i de les que avisa més aviat.

Les dues limitacions honestes: necessita almenys 7 dies de línia base abans de servir per a res, i només detecta anomalies de volum, no d'intenció. Una sola crida maliciosa perfectament executada no genera cap insight. Per a això hi ha les alarmes específiques de la secció següent.

Trails multiregió i d'organització

Multiregió ja ho vam activar amb --is-multi-region-trail, i val la pena insistir en per què. Un trail d'una sola regió és com una càmera de seguretat que només enfoca la porta principal: un atacant que aconsegueixi credencials crearà les seves instàncies de mineria a ap-southeast-2, on ningú no mira mai. Amb el trail multiregió, aquestes crides apareixen al mateix bucket.

Cost de la multiregió: zero. La primera còpia dels esdeveniments de gestió és gratuïta a totes les regions. No hi ha cap raó per no activar-la.

Trail d'organització: si tens diversos comptes sota AWS Organizations, un sol trail creat al compte de gestió registra tots els comptes membre, i aquests no el poden desactivar ni veure. És la configuració correcta per a qualsevol organització amb més d'un compte, i és la lliçó 09-04. MercadoFresco té un sol compte avui; la separació entre producció, preproducció i registre és un objectiu declarat.

Enviar el trail a CloudWatch Logs

El bucket d'S3 és l'arxiu. Per reaccionar en calent cal que els esdeveniments arribin també a CloudWatch Logs, on poden convertir-se en mètriques i alarmes (05-01).

# 1. Grup de registres amb retencio 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-1

Fixa-t'hi: 90 dies a CloudWatch Logs, 7 anys a S3. Els dos destins tenen propòsits diferents i retencions diferents. Logs és per alarmar i consultar el que és recent; S3 és l'arxiu legal. Duplicar 7 anys a CloudWatch Logs costaria una fortuna sense aportar res.

El rol que CloudTrail necessita:

{
  "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"
      }
    }
  }]
}

Amb 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:*"
  }]
}

Cost: la ingesta dels esdeveniments de gestió de MercadoFresco són uns 0,4 GB al mes: 0,25 USD. Si activessis esdeveniments de dades d'alt volum, això es dispararia; és una altra raó per als selectors restrictius.

Filtres de mètriques i alarmes de seguretat

Aquí és on CloudTrail i CloudWatch es troben i es converteixen en detecció, no només en registre. Aquestes són les cinc alarmes que munta MercadoFresco, i les cinc són estàndard en qualsevol auditoria de seguretat (apareixen al CIS Benchmark d'AWS, que veurem a 05-04).

1. Ús del compte arrel. Després de 04-01, l'arrel no s'hauria de fer servir mai:

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 "ALGU HA FET SERVIR EL COMPTE ARREL" \
  --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-1

Aquest $.userIdentity.invokedBy NOT EXISTS exclou els esdeveniments en què un servei d'AWS actua en nom del compte, que són legítims i freqüents. Sense ell, l'alarma seria pur soroll.

2. Aturada o esborrat d'un trail. La primera acció de qualsevol que vulgui amagar el seu rastre:

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-1

Amb la seva alarma amb llindar 0, exactament igual que l'anterior. Aquesta és l'alarma més important d'aquesta lliçó: és la que avisa que el sistema d'auditoria està sent atacat.

3. Ús de KMS sobre les còpies de la base de dades. La que respon directament a la pregunta del mòdul 4, però en calent:

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-1

Aquest $.userIdentity.type != "AWSService" exclou el mateix RDS xifrant les seves còpies automàtiques cada nit, que és la línia 1 de la nostra investigació. El que queda és activitat humana o d'script sobre una còpia de la base de dades, i això sempre mereix un avís.

4. Accessos denegats repetits. El reguer que deixa el reconeixement:

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 "Pic d'AccessDenied: possible reconeixement" \
  --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-1

Aquí el llindar no és 0, i la raó és honesta: els AccessDenied són constants en qualsevol compte viu. Un desenvolupador provant, una eina que consulta alguna cosa que no pot, un SDK que sondeja. 20 en cinc minuts sostinguts durant dos períodes sí que és un senyal.

5. Canvis en la política d'un grup de seguretat o a 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-1

Amb llindar 0 en horari nocturn seria massa sorollós durant un desplegament diürn. MercadoFresco el posa a llindar 0 amb període de 5 minuts, acceptant el soroll: prefereix assabentar-se de tots els canvis de seguretat. Que l'alarma sigui informativa i no urgent és una decisió vàlida, sempre que estigui documentada i no acabi sent ignorada per costum.

Consultar amb Athena

Per investigar de debò cal consultar els fitxers d'S3 amb SQL. Amazon Athena executa SQL directament sobre els objectes d'un bucket, sense carregar res en cap base de dades.

Athena es veu amb més profunditat al mòdul 6 quan es parla d'analítica. Aquí en fem servir el just per investigar CloudTrail, que és el seu cas d'ús més comú.

Pas 1: la taula. La manera còmoda de crear-la és des de la consola de CloudTrail («Create Athena table»), que genera aquest DDL. Convé entendre'l:

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/';

Dues coses que cal veure en aquest DDL:

  • requestParameters i responseElements són STRING, no estructures. El seu contingut varia segons l'esdeveniment, així que es consulten amb funcions JSON: json_extract_scalar(requestParameters, '$.keyId').
  • PARTITIONED BY és el que decideix el cost. Athena cobra 5 USD per TB escanejat. Sense particions, cada consulta llegeix tots els fitxers del bucket. Amb particions i un WHERE sobre elles, llegeix només els dies que demanis.

Pas 2: registrar les particions. Amb la projecció de particions s'automatitza:

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}'
);

Sense això caldria executar MSCK REPAIR TABLE o ALTER TABLE ADD PARTITION cada dia. Amb la projecció, Athena dedueix les particions del patró de rutes.

Les consultes que responen preguntes reals

1. Qui va assumir rol-mercadofresco-tienda, i quan?

SELECT eventtime,
       userIdentity.arn                         AS qui,
       json_extract_scalar(requestParameters, '$.roleSessionName') AS sessio,
       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 va llegir el secret de mfadmin? La pregunta que vam deixar pendent a 04-03:

SELECT eventtime,
       COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
                userIdentity.userName)          AS identitat,
       split_part(userIdentity.arn, '/', 3)     AS sessio,
       sourceipaddress,
       userIdentity.sessionContext.attributes.mfaAuthenticated AS amb_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;

El que s'espera en el resultat: el rol de la botiga llegint-lo en arrencar cada instància, i rol-rotacion-mfadmin cada 30 dies fent la rotació (04-03). Qualsevol altra identitat és una troballa.

3. Totes les crides denegades, agrupades. La consulta de reconeixement:

SELECT COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
                userIdentity.userName, userIdentity.type)  AS identitat,
       sourceipaddress,
       eventsource,
       eventname,
       count(*)                                            AS intents,
       min(eventtime)                                      AS primer,
       max(eventtime)                                      AS ultim
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 intents DESC
LIMIT 50;

Aquesta consulta té un ús doble molt valuós, i convé subratllar-ho:

  • Seguretat: una identitat desconeguda amb 400 AccessDenied sobre 30 serveis diferents en deu minuts és reconeixement. Algú està mapant el que pot fer.
  • Operació: rol-mercadofresco-tienda amb 3.000 AccessDenied sobre s3:GetObject significa que a pol-mercadofresco-tienda li falta un permís i hi ha una funcionalitat trencada que ningú no ha reportat. Els AccessDenied són també un detector d'errors de configuració.

4. Activitat fora d'horari, que és la que sol delatar.

SELECT eventtime, eventname, eventsource,
       COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
                userIdentity.userName) AS identitat,
       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' és imprescindible: si no, la sortida s'omple de còpies automàtiques, instantànies de DLM i activitat de l'ASG a les 3 de la matinada, que és completament normal.

5. Totes les IP d'origen, per detectar les desconegudes.

SELECT sourceipaddress,
       count(*)                                   AS crides,
       count(DISTINCT eventname)                  AS accions_diferents,
       array_agg(DISTINCT COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
                                   userIdentity.userName)) AS identitats,
       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 crides DESC;

Una IP que apareix per primer cop, fa 12 crides i desapareix és molt més sospitosa que una que en fa 40.000 des de fa mesos.

6. La consulta de la còpia desxifrada, ara en SQL.

SELECT eventtime,
       COALESCE(userIdentity.sessionContext.sessionIssuer.userName,
                userIdentity.userName) AS identitat,
       split_part(userIdentity.arn, '/', 3) AS sessio,
       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 cost a Athena: cada consulta cobra per TB escanejat. Posar sempre el WHERE de partició (anio, mes, dia) és la diferència entre 0,01 USD i uns quants euros per consulta. I es pot fixar un límit dur per grup de treball:

aws athena update-work-group --work-group primary \
  --configuration-updates 'BytesScannedCutoffPerQuery=10737418240' \
  --profile mercadofresco-dev --region eu-west-1

Deu GiB màxim per consulta. Si algú escriu un SELECT * sense WHERE, la consulta es cancel·la sola en lloc de generar una factura.

CloudTrail Lake

CloudTrail Lake és l'alternativa gestionada a muntar Athena a mà: un magatzem d'esdeveniments gestionat per AWS, amb SQL directe des de la consola de CloudTrail i sense taules per crear.

S3 + Athena CloudTrail Lake
Configuració Bucket, política, taula, particions Una comanda
Consulta SQL (Presto) des d'Athena SQL des de CloudTrail
Retenció La que vulguis a S3 Fins a 10 anys
Cost d'ingesta Només S3 (cèntims) ~2,50 USD/GB
Cost de consulta 5 USD/TB escanejat 0,005 USD/GB escanejat
Cost total Molt menor Major
Esforç Mitjà Mínim
Integració Te la muntes tu Nativa amb Organizations
aws cloudtrail create-event-data-store \
  --name lake-mercadofresco \
  --retention-period 2555 \
  --multi-region-enabled \
  --advanced-event-selectors '[{
    "Name": "Gestio",
    "FieldSelectors": [{ "Field": "eventCategory", "Equals": ["Management"] }]
  }]' \
  --profile mercadofresco-dev --region eu-west-1

La decisió de MercadoFresco: es queda amb S3 + Athena. Amb 0,4 GB mensuals d'esdeveniments, Lake costaria 1 USD al mes d'ingesta —no és dramàtic— però el bucket d'S3 ja existeix, ja està protegit amb Object Lock i ja alimenta l'arxiu legal de 7 anys. Lake s'anota com a opció a reconsiderar si el compte creix i muntar particions a mà comença a fer mal.

Amb molts comptes i equips que no són de plataforma, Lake guanya clarament: ningú no ha d'aprendre a crear taules externes a Athena per investigar un incident.

Investigació d'un incident, pas a pas

Tornem a la troballa: rol-restauracion-copias va desxifrar la còpia snapshot-2026-07-27 a les 03:42 des de 198.51.100.77, sense MFA. Aquest és el procediment complet, i serveix com a plantilla per a qualsevol investigació.

Pas 0. Congelar l'evidència. Abans de res.

# Verificar que els registres no han estat manipulats
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 els fitxers del periode a un prefix d'investigacio
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-dev

Això es fa primer i sempre. Si l'incident acaba en un procediment formal, el que importa no és el que vas descobrir, sinó que puguis demostrar que les dades no van canviar mentre investigaves.

Pas 1. Qui va assumir el rol? El nom de sessió sesion-marta-copias és text lliure triat per qui va fer la crida. Cal buscar l'AssumeRole:

SELECT eventtime, userIdentity.arn AS qui_el_va_assumir,
       userIdentity.type, sourceipaddress, useragent,
       userIdentity.sessionContext.attributes.mfaAuthenticated AS amb_mfa,
       json_extract_scalar(requestParameters, '$.roleSessionName') AS nom_sessio
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;

Resultat: arn:aws:iam::111122223333:user/luis.dev, a les 03:41:52, des de 198.51.100.77, mfaAuthenticated: false, agent aws-cli/2.15.30.

Pas 2. Reconstruir tota la sessió. L'accessKeyId temporal (ASIA...) identifica la sessió 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;

Resultat:

Hora Esdeveniment Resultat
03:41:52 AssumeRole OK
03:42:03 DescribeDBSnapshots OK
03:42:17 kms:Decrypt OK
03:42:19 RestoreDBInstanceFromDBSnapshot OK — instància pedidos-copia-luis
03:58:44 CreateDBSnapshot AccessDenied
04:30:12 DeleteDBInstance OK

Pas 3. Interpretar. El patró és coherent i força llegible: algú va restaurar una còpia, va intentar crear una instantània nova (denegat), i al cap de 45 minuts va esborrar la instància restaurada. No hi ha exfiltració visible —cap crida a S3, cap CreateDBSnapshot amb èxit, cap instància deixada en marxa—. Sembla feina legítima feta malament: restaurar una còpia per comprovar alguna cosa, de matinada, sense avisar i sense MFA.

Pas 4. Confirmar amb la persona. I aquí una advertència important, perquè és on més s'equivoca la gent: CloudTrail diu què va passar, no per què. No s'acusa mai ningú amb un registre a la mà sense parlar-hi primer. La conversa amb el Luis aclareix que estava comprovant si una còpia era restaurable després d'un avís de la Sara, i que ho va fer de matinada per no carregar la base de dades en hores de producció. Intenció correcta, procediment incorrecte.

Pas 5. Comprovar que no hi ha res més. Encara que l'explicació quadri, es 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 aquesta IP només apareix en horari de feina del Luis durant mesos, és casa seva o la seva oficina. Si apareix una vegada, a les 3 de la matinada, i mai més, la conversa és una altra.

Pas 6. Accions correctives. Sense culpables i amb dates, igual que el post-mortem de 04-04:

Acció Responsable Termini
Exigir MFA per assumir rol-restauracion-copias (condició aws:MultiFactorAuthPresent, 04-01) Marta 3 dies
Alarma mercadofresco-descifrado-copias, ja creada, verificada amb simulacre Marta 1 dia
Procediment escrit: restaurar còpies es fa en preproducció i s'avisa Marta 1 setmana
Prova de restauració programada i automatitzada trimestral Luis 1 mes
Etiquetar Entorno=temporal les instàncies restaurades i esborrar-les soles a les 8 h Luis 1 mes

Fixa't en l'última fila i en l'anterior: la resposta correcta a «el Luis va restaurar una còpia d'amagat» no és prohibir-ho. És fer que provar restauracions sigui fàcil, visible i rutinari. Una prova de restauració és una pràctica excel·lent que a 02-04 vam recomanar explícitament; el problema era el procediment, no l'activitat.

Relació amb IAM Access Analyzer

CloudTrail no només serveix per investigar: serveix per construir permisos correctes. IAM Access Analyzer pot llegir el teu historial de CloudTrail i generar una política de privilegi mínim amb exactament les accions que una identitat ha fet servir de debò en un període.

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-1

Això tanca el cercle amb 04-01. Allà vam escriure pol-mercadofresco-tienda a mà, raonant sobre què havia de poder fer la botiga. Amb dos mesos de CloudTrail, Access Analyzer diu què va fer de debò, i la diferència entre les dues llistes és un regal:

  • Accions que són a la política i no es fan servir mai: candidates a treure.
  • Accions que l'aplicació intenta i li són denegades: funcionalitat trencada que ningú no va reportar.

Access Analyzer fa més coses —detectar recursos accessibles des de fora del compte, validar polítiques— però aquesta és la seva relació directa amb CloudTrail i amb el que vam aprendre a 04-01.

Retenció, cost i neteja

Concepte Preu MercadoFresco
Historial d'esdeveniments, 90 dies Gratis
Primera còpia d'esdeveniments de gestió Gratis 0,00 USD
Còpies addicionals d'esdeveniments de gestió 2,00 USD / 100.000 0,00 USD (només un trail)
Esdeveniments de dades 0,10 USD / 100.000 0,06 USD (amb selectors)
Insights 0,35 USD / 100.000 analitzats 0,53 USD
Emmagatzematge a S3 0,023 USD/GB/mes ~0,05 USD
Ingesta a CloudWatch Logs ~0,63 USD/GB 0,25 USD
Alarmes (5) 0,10 USD cadascuna 0,50 USD
Athena 5 USD/TB escanejat ~0,20 USD
Total ~1,59 USD/mes

És de les coses més barates que es poden muntar a AWS, i l'argument definitiu: la primera còpia dels esdeveniments de gestió multiregió és gratuïta. No hi ha cap excusa tècnica ni econòmica per no tenir un trail. Que un compte de producció no en tingui és sempre una troballa d'auditoria.

Cicle de vida a S3, perquè els 7 anys no es paguin a preu d'emmagatzematge estàndard:

{
  "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" }
    ]
  }]
}

Compte amb dues coses: DEEP_ARCHIVE triga fins a 12 hores a restaurar, cosa que pot ser inacceptable en una investigació urgent —per això els dos primers anys es queden en classes d'accés ràpid—, i si has activat Object Lock en mode COMPLIANCE, la regla de cicle de vida no pot expirar objectes abans que venci la retenció. Els pot canviar de classe, no esborrar-los.

Neteja, si has muntat això només per practicar:

# Aturar i esborrar 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

# Alarmes i filtres
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

# Grup de registres
aws logs delete-log-group --log-group-name /aws/cloudtrail/mercadofresco \
  --profile mercadofresco-dev --region eu-west-1

# Lake, si el vas crear: primer treure la proteccio d'esborrat
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-1

El bucket no s'esborra a la lleugera. Si vas activar Object Lock en mode COMPLIANCE, els objectes no es poden esborrar fins que venci la retenció —ni tu, ni el compte arrel, ni el suport d'AWS—, i el bucket no es pot eliminar mentre contingui objectes. És una propietat desitjada, no una fallada. Practica Object Lock en un compte de proves i amb retencions de dies, mai d'anys.

Errors Habituals i Consells

1. Crear el trail i no cridar start-logging. create-trail no arrenca el registre. Comprova sempre amb get-trail-status que IsLogging és true.

2. Trail d'una sola regió. Un atacant crea recursos on ningú no mira. La multiregió és gratis i no té contrapartida.

3. Un bucket d'auditoria que es pot esborrar. Sense política de Deny, sense versionat i sense Object Lock, el registre dura el que trigui algú a executar una comanda. I el nivell definitiu és un compte separat (09-04).

4. Activar esdeveniments de dades sense selectors. Registrar tots els GetObject d'un bucket de fotos són desenes d'USD al mes per dades inútils. Selectors per prefix i readOnly: false.

5. No posar nom de sessió en assumir un rol. CloudTrail dirà «algú amb aquest rol», i no hi haurà manera de saber qui. --role-session-name amb el nom de la persona.

6. Buscar a CloudTrail el que diu la teva aplicació. No hi és. CloudTrail registra crides a l'API d'AWS; els errors del teu codi són a CloudWatch Logs (05-01).

7. Consultes d'Athena sense WHERE de partició. Escanegen el bucket sencer a 5 USD per TB. Posa sempre anio, mes i dia, i fixa BytesScannedCutoffPerQuery al grup de treball.

8. Ignorar els AccessDenied. Són alhora el detector de reconeixement i el detector de permisos mal configurats. Revisar-los setmanalment troba coses trencades que ningú no va reportar.

9. No verificar mai la integritat. validate-logs un cop al mes, amb la sortida desada. Sense això, la validació està activada però no serveix de res.

10. Retenció infinita a CloudWatch Logs per als esdeveniments de CloudTrail. L'arxiu llarg va a S3, que costa 20 vegades menys. A Logs, 90 dies.

11. Acusar algú amb un registre a la mà. CloudTrail diu què va passar, no per què. Cal investigar, contrastar i després parlar amb la persona. Un incident mal gestionat fa més mal que l'incident.

12. Activar Object Lock en mode COMPLIANCE amb 7 anys «per provar». No hi ha marxa enrere. Practica amb GOVERNANCE i amb retencions de dies.

13. Suposar que el nom de sessió identifica algú. És una cadena lliure. La identitat real és a l'esdeveniment AssumeRole corresponent.

Consell final: revisa CloudTrail quan no hi ha incidents. Mitja hora al mes executant les consultes d'IP desconegudes, AccessDenied agrupats i activitat fora d'horari troba coses —permisos trencats, processos zombis, claus oblidades d'un antic proveïdor— que d'altra manera només apareixen el dia que hi ha un problema de debò.

Exercicis

Els tres exercicis treballen sobre la taula auditoria_mercadofresco.cloudtrail_mercadofresco i el trail trail-mercadofresco definits en aquesta lliçó.

Exercici 1: dissenyar l'estratègia d'esdeveniments de dades

MercadoFresco vol activar esdeveniments de dades, però amb criteri i amb un pressupost de 5 USD al mes. Volum mensual real:

Recurs Operacions/mes Repartiment
mercadofresco-catalogo-fotos 40.000.000 99,9 % lectures
mercadofresco-copias-basedatos 2.400 60 % escriptures
mercadofresco-informes-analitica 180.000 70 % lectures (Sara)
mercadofresco-registros-web 8.000.000 99 % escriptures (l'ALB)
Lambda mercadofresco-generar-miniaturas 620.000 invocacions
Lambda mercadofresco-estado-pedido 660.000 invocacions

Requisits: cal poder saber sempre qui ha llegit o tocat una còpia de la base de dades; cal detectar si algú descarrega massivament els informes de negoci; cal detectar si algú altera les fotos del catàleg; el pressupost és de 5 USD.

Escriu els selectors avançats complets en JSON, calcula el cost de cadascun, justifica què deixes fora i per què, i proposa quina alarma muntaries sobre els esdeveniments de dades que sí que registres.

Exercici 2: la investigació completa

Un dilluns al matí, l'alarma mercadofresco-accesos-denegados ha saltat dues vegades durant el cap de setmana. Aquestes són les dades inicials:

  • Dissabte 02:14 a 02:31: 340 esdeveniments amb errorCode = AccessDenied.
  • Tots des de sourceIPAddress = 203.0.113.201.
  • Tots amb userIdentity.type = "IAMUser", userName = "integracion-proveedor".
  • userAgent: aws-cli/2.9.19 Python/3.9.11 Windows/10.
  • Els serveis afectats: iam, s3, ec2, rds, secretsmanager, kms, organizations.
  • Diumenge 22:40: 6 esdeveniments sense errorCode des de la mateixa IP i el mateix usuari: ListBuckets, GetBucketLocation, ListObjectsV2 sobre mercadofresco-informes-analitica, GetObject × 3.

Escriu: les consultes SQL exactes que executaries i en quin ordre; quina conclusió treus de cadascuna; quines accions de contenció prendries i en quin ordre exacte; i què et diu el fet que les 340 crides fallessin però 6 tinguessin èxit. Indica també quina informació no podràs obtenir de CloudTrail i on la buscaries.

Exercici 3: convertir CloudTrail en detecció proactiva

La Marta vol passar d'investigar després a detectar en el moment. Defineix cinc deteccions addicionals a les cinc alarmes de la lliçó, orientades al risc real de MercadoFresco:

  1. Algú crea una clau d'accés permanent per a un usuari IAM (després de 04-01, ningú no ho hauria de fer).
  2. Algú modifica la política de la clau alias/mercadofresco-datos.
  3. Un rol d'aplicació es fa servir des d'una IP fora de la VPC.
  4. Algú desactiva el xifratge d'un bucket o activa l'accés públic.
  5. Es crea un recurs en una regió que MercadoFresco no fa servir.

Per a cadascuna: escriu el patró del filtre de mètriques, decideix el llindar i els períodes de l'alarma, digues si ha de despertar algú de matinada o només generar un correu, i explica quin fals positiu esperes i com l'evitaries. Per a les que no es puguin resoldre amb un filtre de mètriques, indica quina eina faries servir (i avança quina serà la lliçó que la cobreixi).

Solucions

Solució 1

Càlcul de partida. Registrar-ho tot:

Recurs Esdeveniments Cost
Catàleg 40.000.000 40,00 USD
Registres web 8.000.000 8,00 USD
Informes 180.000 0,18 USD
Lambdes 1.280.000 1,28 USD
Còpies 2.400 0,00 USD
Total 49,5 M 49,46 USD

Deu vegades el pressupost, i amb el 97 % de la despesa en lectures de fotos de tomàquets.

Selectors proposats:

[
  {
    "Name": "Gestio completa",
    "FieldSelectors": [
      { "Field": "eventCategory", "Equals": ["Management"] }
    ]
  },
  {
    "Name": "Copies de base de dades: TOT",
    "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 negoci: TOT",
    "FieldSelectors": [
      { "Field": "eventCategory", "Equals": ["Data"] },
      { "Field": "resources.type", "Equals": ["AWS::S3::Object"] },
      { "Field": "resources.ARN", "StartsWith": ["arn:aws:s3:::mercadofresco-informes-analitica/"] }
    ]
  },
  {
    "Name": "Cataleg: NOMES escriptures",
    "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"] }
    ]
  }
]

Cost resultant:

Selector Esdeveniments/mes Cost
Gestió ~150.000 0,00 USD (primera còpia gratis)
Còpies de BD 2.400 0,002 USD
Informes 180.000 0,18 USD
Catàleg, només escriptures (0,1 %) ~40.000 0,04 USD
Total 222.400 ~0,22 USD/mes

Molt per sota dels 5 USD i complint els tres requisits.

Què queda fora i per què:

  • Lectures del catàleg (40 M): són fotos públiques de producte servides per CloudFront. Llegir-les no té cap valor de seguretat i costa 40 USD. Si algun dia calgués analitzar patrons d'accés, hi ha els registres d'accés d'S3 i els de CloudFront, molt més barats.
  • mercadofresco-registros-web (8 M): és l'ALB escrivint els seus propis registres. Registrar que l'ALB escriu els seus registres és recursiu i inútil: 8 USD per confirmar el que ja sabem.
  • Invocacions de Lambda (1,28 M): 1,28 USD, i ja tenim millor visibilitat d'aquestes funcions amb les mètriques d'AWS/Lambda, els seus registres i les traces d'X-Ray (05-02). CloudTrail només diria «es va invocar», que és el menys informatiu de tot.

Alarmes sobre els esdeveniments de dades que sí que registrem:

# 1. Qualsevol lectura humana d'una copia de la base de dades
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-1

Amb llindar 0: ningú no té per què descarregar una còpia de la base de dades sense que algú se n'assabenti.

# 2. Descarrega massiva d'informes: llindar, no zero
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-1

Aquí el llindar no pot ser 0: la Sara llegeix informes cada dia, és la seva feina. 500 objectes en cinc minuts no és llegir un informe, és descarregar-se el magatzem sencer.

# 3. Escriptures al cataleg fora del proces normal
# Nomes rol-mercadofresco-tienda i rol-lambda-miniaturas hi han d'escriure.
--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" }'

Aquest últim és el patró més valuós dels tres: llista blanca d'identitats esperades, alarma sobre tota la resta. És més robust que enumerar el que és dolent, perquè cobreix també el que no has previst.

Solució 2

Lectura inicial de les dades. Tres senyals molt clars, abans d'executar cap consulta:

  1. 340 AccessDenied en 17 minuts sobre 7 serveis diferents, incloent-hi iam i organizations. Això no és una aplicació amb un permís mal posat: una aplicació falla sempre a la mateixa crida. Això és enumeració sistemàtica de permisos.
  2. IAMUser amb clau permanent, exactament el que 04-01 diu que no ha d'existir. Un usuari d'«integració amb proveïdor» és el vector clàssic: clau que es va crear una vegada, es va posar en un fitxer de configuració, i que ningú no ha rotat en tres anys.
  3. 20 hores de silenci i després 6 crides amb èxit, quirúrgiques, sobre els informes de negoci. Aquest espaiat és el patró: primer es mapa què es pot fer, després es torna a buscar el que funciona.

Consulta 1: tota l'activitat d'aquest usuari, no només la del cap de setmana.

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: quan va començar tot, si aquella IP havia aparegut abans, i què feia aquest usuari normalment. Si la seva activitat legítima és un PutObject diari a les 06:00 des d'una altra IP, tot el del cap de setmana és intrusió.

Consulta 2: què va tenir èxit. És l'únic que importa de debò.

SELECT eventtime, eventsource, eventname,
       json_extract_scalar(requestParameters, '$.bucketName') AS bucket,
       json_extract_scalar(requestParameters, '$.key')        AS objecte,
       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: l'abast exacte de la bretxa. Els 340 errors són soroll; els 6 èxits són l'incident. Aquí surt quins tres objectes concrets es van descarregar de mercadofresco-informes-analitica, i això determina si hi ha dades personals implicades i si hi ha obligació de notificar.

Consulta 3: la clau d'accés, per veure si es va fer servir des de més llocs.

SELECT sourceipaddress, useragent, awsregion,
       count(*) AS crides,
       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 clau es fa servir també des de la IP legítima del proveïdor, està compromesa però segueix en ús legítim, i desactivar-la trencarà una integració. Si només es fa servir des de 203.0.113.201 des de fa un mes, el proveïdor ja no la fa servir i desactivar-la no trenca res.

Consulta 4: d'on va sortir aquesta clau i quan.

SELECT eventtime, userIdentity.arn AS qui_la_va_crear, sourceipaddress
FROM auditoria_mercadofresco.cloudtrail_mercadofresco
WHERE eventname IN ('CreateAccessKey', 'CreateUser')
  AND json_extract_scalar(responseElements, '$.accessKey.userName') = 'integracion-proveedor';

Què busco: si la clau es va crear fa dos anys i mai no es va rotar, o si es va crear el divendres passat, cas en què el compromís és més profund: algú ja tenia accés i es va obrir una porta del darrere.

Consulta 5: correlació per IP, més enllà d'aquest usuari.

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 aquesta IP ha fet servir altres identitats, l'abast és molt més gran.

Contenció, en aquest ordre exacte:

# Acció Per què en aquest ordre
1 Desactivar la clau, no esborrar-la: aws iam update-access-key --status Inactive Talla l'accés conservant l'evidència. Esborrar-la destrueix informació
2 Congelar l'evidència: validate-logs + copiar els fitxers del període Abans de tocar res més
3 Revisar quins permisos tenia l'usuari i què hauria pogut fer Defineix el pitjor cas possible
4 Determinar què es va descarregar exactament (consulta 2) Decideix si hi ha notificació obligatòria
5 Avisar la direcció i compliment Si hi ha dades personals, hi ha terminis legals
6 Buscar persistència: CreateUser, CreateAccessKey, CreateRole, PutUserPolicy en el període Un atacant deixa portes del darrere
7 Rotar credencials relacionades i revisar el secret de mfadmin Per si hi va haver moviment lateral
8 Només llavors, contactar amb el proveïdor Pot ser que el compromès sigui ell

Què significa que 340 fallessin i 6 tinguessin èxit. És la millor notícia possible: el privilegi mínim va funcionar. La feina de 04-01 és el que va impedir que aquelles 340 crides —a IAM, a KMS, a Secrets Manager, a RDS, a Organizations— fessin mal. La bretxa es va limitar als permisos que aquell usuari tenia legítimament, que era llegir informes.

I alhora és la pitjor notícia sobre el disseny: aquell usuari mai no hauria d'haver tingut una clau permanent. L'accés d'un proveïdor s'ha de fer amb un rol assumible amb condició d'identitat externa (sts:ExternalId, 04-01), amb credencials temporals que caduquen soles. Aquest és l'arranjament de fons.

El que CloudTrail NO et pot dir:

Pregunta On buscar
Què contenien exactament els objectes descarregats? Al mateix bucket, mirant aquelles claus
Com es va filtrar la clau? Repositoris de codi, portàtils, el proveïdor
Hi va haver accés a la base de dades amb credencials robades? Registres de PostgreSQL (05-01)
Es va exfiltrar per xarxa des d'una instància? VPC Flow Logs (03-01)
Qui és la persona darrere d'aquella IP? Ningú a AWS. És feina policial
Es va modificar alguna cosa dins de l'aplicació? Registres d'aplicació (05-01)

Aquesta taula és la lliçó més important de l'exercici: CloudTrail és una peça de la investigació, no la investigació sencera. Dona el «qui, què, quan, des d'on» del pla de control d'AWS. Tota la resta és en altres fonts, i per això aquest mòdul sencer té sentit com a conjunt.

Solució 3

Detecció 1: creació de clau d'accés permanent.

{ ($.eventName = "CreateAccessKey") || ($.eventName = "CreateUser") || ($.eventName = "CreateLoginProfile") }
Paràmetre Valor Justificació
Llindar 0 Després de 04-01, no se n'hauria de crear cap
Període / avaluació 300 s / 1 Immediat
Desperta? És el pas clàssic de persistència després d'un compromís
Fals positiu esperat Alta legítima d'un usuari nou S'avisa abans pel canal de l'equip; l'avís es marca com a esperat

Detecció 2: canvi en la política de la clau KMS.

{ ($.eventSource = "kms.amazonaws.com") && (($.eventName = "PutKeyPolicy") || ($.eventName = "ScheduleKeyDeletion") || ($.eventName = "DisableKey") || ($.eventName = "DisableKeyRotation")) }
Paràmetre Valor Justificació
Llindar 0 Canviar la política d'alias/mercadofresco-datos és un esdeveniment d'altíssim impacte
Desperta? Sí, sempre ScheduleKeyDeletion sobre aquesta clau deixaria illegibles les còpies i la base de dades. És l'esdeveniment més destructiu de tot el compte
Fals positiu Canvi planificat Es fa amb avís previ i s'anota

Val la pena subratllar-ho: a 04-02 vam veure que esborrar una clau KMS té un període d'espera de 7 a 30 dies precisament per donar temps a reaccionar. Aquesta alarma és el que converteix aquell període d'espera en una defensa real: sense ella, l'espera passa sense que ningú se n'assabenti.

Detecció 3: rol d'aplicació fet servir des de fora de la VPC.

Un filtre de mètriques no pot fer això bé. La sintaxi de patrons de CloudWatch Logs no admet comparacions de rang d'IP ni negació de prefixos de manera fiable. Es pot aproximar:

{ ($.userIdentity.sessionContext.sessionIssuer.userName = "rol-mercadofresco-tienda") && ($.sourceIPAddress != "10.0.*") }

…però és fràgil i generarà falsos positius, perquè les crides a través d'un punt d'enllaç de VPC (vpce-mercadofresco-s3) o des de certs serveis apareixen amb altres adreces.

L'eina correcta és una altra, i aquí cal ser honest:

  • Amazon GuardDuty té una troballa específica per a això: UnauthorizedAccess:IAMUser/InstanceCredentialExfiltration, que detecta credencials de rol d'instància usades des de fora d'AWS. És exactament aquest cas i funciona sense configuració.
  • Alternativament, una condició a la política de confiança del rol amb aws:SourceVpc, que directament impedeix l'ús des de fora en lloc de detectar-lo. Prevenir és millor que detectar (04-01).

GuardDuty i Security Hub es veuen en visió general a 05-04.

Detecció 4: xifratge desactivat o accés públic en un bucket.

{ ($.eventSource = "s3.amazonaws.com") && (($.eventName = "DeleteBucketEncryption") || ($.eventName = "PutBucketAcl") || ($.eventName = "DeletePublicAccessBlock") || ($.eventName = "PutBucketPolicy")) }
Paràmetre Valor Justificació
Llindar 0 Cap d'aquestes quatre no hauria de passar en producció
Desperta? Correu immediat; SMS només si és sobre buckets crítics PutBucketPolicy passa legítimament en desplegar
Fals positiu Desplegaments d'infraestructura com a codi Es filtra per identitat: si ve del rol del pipeline (mòdul 8), no alarma

Però aquesta detecció té un límit seriós: només detecta el canvi, no l'estat. Si el xifratge ja estava desactivat des d'abans de crear l'alarma, mai no saltarà. I no ho pot corregir.

Per a això existeix una eina específica, que és la lliçó següent: AWS Config. Config avalua l'estat actual de tots els recursos contra regles, detecta les desviacions existents —no només les noves— i les pot corregir automàticament. És exactament la cinquena pregunta que va deixar oberta el mòdul 4.

Detecció 5: recurs creat en una regió no utilitzada.

{ ($.awsRegion != "eu-west-1") && ($.awsRegion != "us-east-1") && ($.readOnly = "false") && ($.userIdentity.type != "AWSService") }
Paràmetre Valor Justificació
Llindar 0 MercadoFresco només fa servir eu-west-1 i us-east-1 (CloudFront, WAF, ACM)
Desperta? Crear recursos en regions no utilitzades és la firma clàssica de la mineria de criptomonedes amb credencials robades
Fals positiu Algú provant alguna cosa Molt rar, i mereix la conversa

Aquesta detecció depèn completament que el trail sigui multiregió. Sense --is-multi-region-trail no veuries absolutament res, i és l'argument definitiu per activar-lo.

I la prevenció, millor que la detecció: una política de control de serveis (SCP) que denegui tot fora de les regions permeses. Requereix Organizations: 09-04.

Resum de les cinc:

# Detecció Filtre de mètriques? Eina correcta
1 Clau d'accés creada CloudTrail + alarma
2 Política de KMS modificada CloudTrail + alarma
3 Rol usat fora de la VPC No GuardDuty, o condició a la política de confiança
4 Xifratge / accés públic de bucket Parcial AWS Config (05-04)
5 Recurs en regió no utilitzada CloudTrail multiregió + alarma; millor SCP (09-04)

La conclusió de l'exercici: CloudTrail detecta esdeveniments, no estats. És perfecte per a «algú ha fet X», i insuficient per a «el bucket Y està mal configurat des de fa tres mesos». Aquesta segona pregunta necessita una altra eina, i és la lliçó següent.

Conclusió

MercadoFresco ja sap qui va fer què. Tens clara la diferència essencial que defineix aquest servei: CloudTrail registra crides a l'API d'AWS; CloudWatch Logs registra el que diu la teva aplicació. L'un respon «qui va esborrar el bucket?», l'altre «per què va fallar el pagament?». No competeixen: cobreixen universos diferents, i buscar en el que no toca costa hores.

Saps que l'historial d'esdeveniments de 90 dies sempre hi és, gratis, i coneixes les seves cinc limitacions —90 dies, només gestió, un atribut per cerca, sense SQL, sense immutabilitat— que són exactament les raons per crear un trail. Has creat trail-mercadofresco amb les quatre banderes que importen: multiregió (gratis, i sense ella un atacant crea recursos on ningú no mira), esdeveniments globals, validació d'integritat i xifratge amb alias/mercadofresco-datos. I saps que create-trail no arrenca el registre: cal cridar start-logging i comprovar-ho amb get-trail-status.

Has muntat el bucket mercadofresco-auditoria-cloudtrail amb les quatre capes de protecció —política amb Deny d'esborrat, versionat, Object Lock en mode COMPLIANCE que ni el compte arrel no pot saltar-se, i el compte separat com a pas pendent de 09-04— perquè un registre d'auditoria que l'atacant pot esborrar no és un registre d'auditoria. I coneixes la cadena de resums signats que fa impossible alterar un fitxer sense trencar-la, amb validate-logs executat el primer dilluns de cada mes.

Saps llegir un esdeveniment complet: eventTime, eventSource, eventName, sourceIPAddress, userAgent, requestParameters, responseElements (nul en les lectures) i errorCode, que només apareix quan la crida falla —i que és una de les propietats més valuoses de CloudTrail, perquè un atacant provant portes deixa un reguer d'AccessDenied—. Domines els sis tipus de userIdentity, amb AssumedRole com el més freqüent i el més confús, i saps que el nom de sessió és l'única cosa que converteix un rol compartit en una persona identificable, i que és text lliure: la identitat real és a l'AssumeRole corresponent.

I has respost la tercera pregunta del mòdul 4. Qui va desxifrar l'última còpia: tres esdeveniments de Decrypt, dos de normals —RDS xifrant la seva còpia automàtica i una instància de l'ASG des de 10.0.11.24— i un que no ho era: rol-restauracion-copias, a les 03:42, des de 198.51.100.77, sense MFA, sobre snapshot-2026-07-27. Gràcies a l'encryptionContext de 04-02, que és el que converteix un esdeveniment genèric de KMS en «algú va desxifrar la còpia del 27 de juliol de mercadofresco-pedidos».

Saps distingir esdeveniments de gestió (primera còpia gratis) d'esdeveniments de dades (0,10 USD per 100.000, i volum milionari), i has escrit selectors avançats que baixen la factura de 40 USD a 0,06 USD sense perdre res: tot sobre les còpies i els informes, només escriptures sobre el catàleg, res sobre els registres web. Coneixes Insights per 0,53 USD al mes, i les seves dues limitacions: 7 dies de línia base i que només detecta anomalies de volum.

Has enviat el trail també a CloudWatch Logs —90 dies allà, 7 anys a S3, cada destí amb el seu propòsit— i has muntat les cinc alarmes de seguretat estàndard: ús del compte arrel, canvis al trail (la més important de totes, perquè avisa que l'auditoria està sent atacada), desxifratge de còpies excloent-hi el mateix RDS, pics d'AccessDenied amb llindar realista, i canvis de configuració de seguretat. I saps consultar amb Athena —amb el WHERE de partició sempre posat i BytesScannedCutoffPerQuery com a xarxa de seguretat— per respondre qui va assumir un rol, qui va llegir el secret de mfadmin, quines crides van fallar i quines IP desconegudes han aparegut; amb CloudTrail Lake com a alternativa gestionada, avaluada i descartada amb criteri.

Has recorregut una investigació completa: congelar l'evidència abans de res, trobar l'AssumeRole real, reconstruir la sessió sencera per accessKeyId, interpretar el patró, parlar amb la persona abans de concloure res —CloudTrail diu què va passar, no per què— i tancar amb accions correctives amb responsable i data, entre elles la més important: fer que provar restauracions sigui fàcil i visible en lloc de prohibir-ho. I coneixes IAM Access Analyzer generant polítiques de privilegi mínim a partir de l'ús real, que tanca el cercle amb 04-01. Tot per 1,59 USD al mes.

Però fixa't en el que aquest servei no pot fer, perquè l'exercici 3 ho va deixar clar: CloudTrail detecta esdeveniments, no estats. Sap que algú va cridar DeleteBucketEncryption aquest matí. No sap que el bucket mercadofresco-registros-web fa cinc mesos que està sense xifrar perquè mai no es va configurar. No sap que hi ha un grup de seguretat amb 0.0.0.0/0 al port 22 des d'una prova de març. No pot dir quants recursos incompleixen la política d'etiquetatge obligatori de MercadoFresco. I no cal dir que no ho pot arreglar sol.

Aquesta és la cinquena i última pregunta que va deixar oberta el mòdul 4: res no avisa si algú desactiva el xifratge d'un bucket o obre un grup de seguretat al món. A la lliçó 05-04, «AWS Config», veurem què és l'element de configuració d'un recurs i la seva línia temporal, en què es diferencia exactament de CloudTrail —qui va fer la crida enfront de com va quedar el recurs—, com s'activa l'enregistrador de configuració amb el seu canal de lliurament, les regles gestionades que MercadoFresco necessita (s3-bucket-server-side-encryption-enabled, restricted-ssh, required-tags i companyia), les regles pròpies amb Lambda i amb Guard, la remediació automàtica amb documents de Systems Manager que torna a xifrar un bucket o tanca un grup de seguretat sense que ningú intervingui, els paquets de conformitat alineats amb CIS i PCI DSS, i el cost real per element de configuració, que és la partida que més fàcilment es dispara de tot aquest mòdul.

Curs d'AWS

Mòdul 1: Introducció a AWS

Mòdul 2: Serveis principals d'AWS

Mòdul 3: Xarxes i lliurament de contingut

Mòdul 4: Seguretat i identitat

Mòdul 5: Monitoratge i gestió

Mòdul 6: Bases de dades

Mòdul 7: Integració d'aplicacions

Mòdul 8: Eines per a desenvolupadors

Mòdul 9: Infraestructura com a codi i govern de comptes

Mòdul 10: Contenidors a AWS

Mòdul 11: Millors pràctiques i gestió de costos

© Copyright 2026. Tots els drets reservats