Aplicar AWS Foundational Security Best Practices en CloudTrail

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.

AWS Foundational Security Best Practices CloudTrail

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.

AWS Foundational Security Best Practices CloudTrail

AWS Foundational Security Best Practices CloudTrail
AWS Foundational Security Best Practices CloudTrail
AWS Foundational Security Best Practices CloudTrail

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

AWS Foundational Security Best Practices CloudTrail

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.

AWS Foundational Security Best Practices CloudTrail

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 :*).

AWS Foundational Security Best Practices CloudTrail

Para otro momento dejo que hacer con todos esos logs de CloudTrail que seguramente se me ocurrirá algo.

Recursos

Leave a Reply

Your email address will not be published. Required fields are marked *