Siguiendo con la implementación del AWS Foundational Security Best Practices, hoy le toca el turno a CloudTrail. Desde la consola de SecurityHub CSPM tenemos a nuestra disposición diversos “Security Standards”. Estos paquetes implementan múltiples reglas con recomendaciones para nuestro entorno. Ya hemos visto algún pequeño ejemplo con el servicio de CloudFront.
Tabla de contenidos
Controles
Dentro del AWS Foundational Security Best Practices sobre CloudTrail encontraremos los siguientes:
Y en el post de hoy veremos como se puede implementar esto usando Terraform.
CloudTrail.1 ( y 4)
Muy rápidamente, ¿qué es CloudTrail? Bueno, es realmente uno de los servicios core en AWS.
AWS CloudTrail es un servicio que registra y audita todas las acciones realizadas en tu cuenta AWS, incluyendo quién las hizo, desde dónde y cuándo, guardando los eventos para análisis y cumplimiento.
Sigamos.
En cada control, tenemos una guía detallada y cuál es la remediación que se espera. ¿Qué se espera en este control?
This control checks whether there is at least one multi-Region AWS CloudTrail trail that captures read and write management events. The control fails if CloudTrail is disabled or if there isn’t at least one CloudTrail trail that captures read and write management events.

En la siguiente pieza veremos como de paso también afectamos al control 4 cuando habilitamos el enable_log_file_validation.

Arreglar CloudTrail.1 (y 4)
Desplegaremos estos recursos usando Terraform. Aquí estaría un ejemplo práctico:
resource "aws_s3_bucket" "cloudtrail_logs" {
bucket = "mi-cloudtrail-logs"
}
resource "aws_s3_bucket_public_access_block" "bucket_cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
block_public_acls = true
block_public_policy = true
ignore_public_acls = true
restrict_public_buckets = true
}
resource "aws_s3_bucket_versioning" "bucket_cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
versioning_configuration {
status = "Enabled"
}
}
resource "aws_s3_bucket_server_side_encryption_configuration" "bucket_cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
rule {
apply_server_side_encryption_by_default {
sse_algorithm = "AES256"
}
}
}
resource "aws_s3_bucket_policy" "cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
policy = <<POLICY
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DenyInsecureTransport",
"Effect": "Deny",
"Principal": "*",
"Action": "s3:*",
"Resource": [
"${aws_s3_bucket.cloudtrail_logs.arn}",
"${aws_s3_bucket.cloudtrail_logs.arn}/*"
],
"Condition": {
"Bool": { "aws:SecureTransport": "false" }
}
},
{
"Sid": "AWSCloudTrailAclCheck",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:GetBucketAcl",
"Resource": "${aws_s3_bucket.cloudtrail_logs.arn}"
},
{
"Sid": "AWSCloudTrailWrite",
"Effect": "Allow",
"Principal": { "Service": "cloudtrail.amazonaws.com" },
"Action": "s3:PutObject",
"Resource": "${aws_s3_bucket.cloudtrail_logs.arn}/AWSLogs/${var.aws_account_id}/*",
"Condition": {
"StringEquals": {
"s3:x-amz-acl": "bucket-owner-full-control",
"aws:SourceAccount": "${var.aws_account_id}"
},
"ArnLike": {
"aws:SourceArn": "${local.cloudtrail_arn_like}*"
}
}
}
]
}
POLICY
}
resource "aws_s3_bucket_lifecycle_configuration" "cloudtrail_logs" {
bucket = aws_s3_bucket.cloudtrail_logs.id
rule {
id = "archive-and-expire"
status = "Enabled"
transition {
days = 90
storage_class = "GLACIER"
}
expiration {
days = 1825 # 5 años
}
}
}
resource "aws_cloudtrail" "new" {
name = "control-cloudtrail"
s3_bucket_name = aws_s3_bucket.cloudtrail_logs.bucket
include_global_service_events = true
is_multi_region_trail = true
enable_log_file_validation = true
is_organization_trail = false
enable_logging = true
event_selector {
include_management_events = true
read_write_type = "All"
# No añadimos data events (S3/Lambda) porque no los exige CloudTrail.1 y encarecen.
}
}
Explicación rápida de los parámetros clave
include_global_service_events = true
Registra eventos de servicios globales (p. ej. IAM, STS, CloudFront) además de los de la región donde se crea el trail.is_multi_region_trail = true
Habilita el trail para registrar eventos en todas las regiones de AWS, no solo en la actual.enable_log_file_validation = true
Activa la validación de integridad de los ficheros de log mediante hashes digitales para detectar cambios no autorizados.is_organization_trail = false
Indica que el trail solo aplica a esta cuenta y no a toda una organización AWS Organizations.enable_logging = true
Activa la recopilación y envío de eventos al destino configurado (S3/CloudWatch).
Confirmar que CloudTrail.1 está arreglado en SecurityHub
Una vez que estamos seguros que el control ha de ser pasado satisfactoriamente, podemos indicarle a SecurityHub CSPM que este finding en base al control CloudTrail.1 está arreglado.




Y sobre las 24 horas, SecurityHub CSPM por detrás mirará el estado de las cosas y alehop!

Ya lo tenemos actualizado y hemos pasado de un 70% a un 73% de cumplimiento del AWS Foundational Security Best Practices
CloudTrail.2
Vamos ahora a por el número 2.

En el marco del AWS Foundational Security Best Practices y otros estándares como CIS AWS Foundations Benchmark, el control CloudTrail.2 se enfoca en garantizar que los logs de CloudTrail estén cifrados y protegidos, de forma que su integridad esté asegurada y no puedan ser alterados ni accedidos por usuarios no autorizados. Este control complementa a otros como CloudTrail.1, que exige que CloudTrail esté habilitado en todas las regiones.
El chequeo falla si el KmsKeyId
no está configurado, ya que sin KMS, CloudTrail usaría por defecto SSE-S3, que no proporciona el mismo nivel de control y trazabilidad sobre el acceso a los logs.
This control checks whether CloudTrail is configured to use the server-side encryption (SSE) AWS KMS key encryption. The control fails if the
KmsKeyId
isn’t defined.

Arreglar CloudTrail.2
El tema de la policy key merece un podcast aparte, así que, de momento, con esto debería bastar para pasar el Cloudtrail.2 y que CloudTrail funcione como se espera.
resource "aws_kms_key" "cloudtrail" {
description = "KMS key for CloudTrail logs encryption"
enable_key_rotation = true
deletion_window_in_days = 30
policy = jsonencode({
Version = "2012-10-17"
Statement = [
# 1) Control total de la cuenta
{
Sid = "AllowRootAccountFullAccess"
Effect = "Allow"
Principal = { AWS = "arn:aws:iam::${data.aws_caller_identity.current.account_id}:root" }
Action = "kms:*"
Resource = "*"
},
# 2) CloudTrail puede generar data keys para este trail concreto
{
Sid = "AllowCloudTrailToUseKeyForEncrypt"
Effect = "Allow"
Principal = {
Service = "cloudtrail.amazonaws.com"
}
Action = [
"kms:GenerateDataKey*",
"kms:DescribeKey"
]
Resource = "*"
Condition = {
StringEquals = {
"AWS:SourceAccount" = data.aws_caller_identity.current.account_id
}
}
}
]
})
}
resource "aws_kms_alias" "cloudtrail" {
name = "alias/cloudtrail-logs"
target_key_id = aws_kms_key.cloudtrail.key_id
}
Y repetiremos el proceso con SecurityHub CSPM para informarle de que ya lo hemos solucionado.
CloudTrail.5
Vamos a ver el último control sobre CloudTrail que tenemos que mejorar.
This control checks whether CloudTrail trails are configured to send logs to CloudWatch Logs. The control fails if the
CloudWatchLogsLogGroupArn
property of the trail is empty.

Esto permite:
- Monitoreo en tiempo real de eventos críticos sin esperar a que se procesen en S3.
- Alertas inmediatas mediante CloudWatch Alarms y EventBridge.
- Búsquedas interactivas en logs recientes desde la consola de CloudWatch Logs.
En resumen, este control nos dice que hemos enviar, no solo los logs a S3 sino a CloudWatch. Mi mente automáticamente pensó en Vince MacMahon. Cuidado con los dineros.
Pero puede ser interesante poder crear ciertas alertas usando CloudWatch alarms, por ejemplo, en el caso de querer notificar de un login por parte del usuario root, podríamos monitorizar los logs y usar una alarma para avisar. Creo que es algo que probaré más adelante. Pondremos una política de retención muy baja, para mantener los costes bajo control.
Arreglar CloudTrail.5
locals {
cloudtrail_log_group_arn_with_wildcard = "${aws_cloudwatch_log_group.audit.arn}:*"
}
resource "aws_cloudtrail" "audit_trail" {
name = "audit-trail"
s3_bucket_name = aws_s3_bucket.audit_logs.id
is_multi_region_trail = true
include_global_service_events = true
enable_log_file_validation = true
enable_logging = true
cloud_watch_logs_group_arn = local.cloudtrail_log_group_arn_with_wildcard
cloud_watch_logs_role_arn = aws_iam_role.cloudtrail_to_cw.arn
}
resource "aws_cloudwatch_log_group" "audit" {
name = "/aws/cloudtrail/audit"
retention_in_days = 90
}
resource "aws_iam_role" "cloudtrail_to_cw" {
name = "CloudTrailToCloudWatch"
assume_role_policy = jsonencode({
Version = "2012-10-17",
Statement = [{
Effect = "Allow",
Principal = { Service = "cloudtrail.amazonaws.com" },
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "cloudtrail_to_cw" {
name = "AllowCloudTrailLogsWrite"
role = aws_iam_role.cloudtrail_to_cw.id
policy = jsonencode({
Version = "2012-10-17",
Statement = [
{
Sid = "AWSCloudTrailCreateLogStream",
Effect = "Allow",
Action = ["logs:CreateLogStream"],
Resource = "${aws_cloudwatch_log_group.audit.arn}:log-stream:*"
},
{
Sid = "AWSCloudTrailPutLogEvents",
Effect = "Allow",
Action = ["logs:PutLogEvents"],
Resource = "${aws_cloudwatch_log_group.audit.arn}:log-stream:*"
}
]
})
}
Resumen
Hemos aplicado completamente las recomendaciones del AWS Foundational Best practices sobre CloudTrail.
CloudTrail.1 – Trail multi-región habilitado, con eventos globales y validación de logs.
CloudTrail.2 – Logs almacenados en un bucket S3 seguro (cifrado, versionado y acceso bloqueado).
CloudTrail.5 – Envío de logs a CloudWatch para monitorización y alertas en tiempo real (ajuste clave: ARN con :*
).

Para otro momento dejo que hacer con todos esos logs de CloudTrail que seguramente se me ocurrirá algo.
Recursos
- https://www.rubenortiz.es/2019/10/08/aws-solutions-architect-associate-saa-2018-v/#cloudtrail
- https://www.rubenortiz.es/2025/08/10/servir-contenido-con-seguridad-cloudfront-con-oac-y-s3/
- https://docs.aws.amazon.com/awscloudtrail/latest/userguide/create-kms-key-policy-for-cloudtrail.html
- https://docs.aws.amazon.com/securityhub/latest/userguide/cloudtrail-controls.html