AWS Security Monitoring with Terraform: CloudTrail, GuardDuty, Config and Security Hub

· 2 min read · Terraform & OpenTofu Tutorials

The security baseline of an AWS account #

Four services form the minimum monitoring of an AWS account. They detect, record and evaluate what happens:

Service What it does
CloudTrail Records every API call: who did what, when and from where
GuardDuty Detects threats using logs and machine learning (compromised credentials, crypto-mining)
AWS Config Records resource configuration and evaluates rules (is every bucket encrypted?)
Security Hub Collects the findings of the other services and checks standards such as CIS and AWS Foundational Security Best Practices

In an organization, enable them in every account and region through delegated administrators (multi-account setup). The examples here are for a single account.

Encrypted bucket for the trail #

cloudtrail.tf
data "aws_caller_identity" "current" {}
data "aws_partition" "current" {}

resource "aws_s3_bucket" "trail" {
  bucket = "ditwl-cloudtrail-${data.aws_caller_identity.current.account_id}"
}

resource "aws_s3_bucket_public_access_block" "trail" {
  bucket                  = aws_s3_bucket.trail.id
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

resource "aws_s3_bucket_versioning" "trail" {
  bucket = aws_s3_bucket.trail.id

  versioning_configuration {
    status = "Enabled"
  }
}

data "aws_iam_policy_document" "trail_bucket" {
  statement {
    sid       = "AWSCloudTrailAclCheck"
    actions   = ["s3:GetBucketAcl"]
    resources = [aws_s3_bucket.trail.arn]

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }
  }

  statement {
    sid       = "AWSCloudTrailWrite"
    actions   = ["s3:PutObject"]
    resources = ["${aws_s3_bucket.trail.arn}/AWSLogs/${data.aws_caller_identity.current.account_id}/*"]

    principals {
      type        = "Service"
      identifiers = ["cloudtrail.amazonaws.com"]
    }

    condition {
      test     = "StringEquals"
      variable = "s3:x-amz-acl"
      values   = ["bucket-owner-full-control"]
    }
  }
}

resource "aws_s3_bucket_policy" "trail" {
  bucket = aws_s3_bucket.trail.id
  policy = data.aws_iam_policy_document.trail_bucket.json
}

See S3 with Terraform for encryption and lifecycle rules for this bucket.

CloudTrail #

cloudtrail.tf
resource "aws_cloudtrail" "main" {
  name                          = "ditwl-main"
  s3_bucket_name                = aws_s3_bucket.trail.id
  is_multi_region_trail         = true
  include_global_service_events = true
  enable_log_file_validation    = true
  kms_key_id                    = aws_kms_key.trail.arn

  depends_on = [aws_s3_bucket_policy.trail]
}
  • is_multi_region_trail records all regions with one trail.
  • enable_log_file_validation lets you prove that the logs were not modified.
  • The first copy of management events is free, and additional trails and data events have a cost.
  • The KMS key needs a policy that allows CloudTrail to use it (KMS and secrets).

GuardDuty #

guardduty.tf
resource "aws_guardduty_detector" "main" {
  enable = true
}

resource "aws_guardduty_detector_feature" "s3" {
  detector_id = aws_guardduty_detector.main.id
  name        = "S3_DATA_EVENTS"
  status      = "ENABLED"
}

Optional protection features (S3, EKS, malware, runtime) have separate charges, so enable the ones you use. GuardDuty is per region: repeat it in each region that you use or use a delegated administrator.

AWS Config #

config.tf
resource "aws_iam_service_linked_role" "config" {
  aws_service_name = "config.amazonaws.com"
}

resource "aws_config_configuration_recorder" "main" {
  name     = "default"
  role_arn = aws_iam_service_linked_role.config.arn

  recording_group {
    all_supported                 = true
    include_global_resource_types = true
  }
}

resource "aws_config_delivery_channel" "main" {
  name           = "default"
  s3_bucket_name = aws_s3_bucket.config.id

  depends_on = [aws_config_configuration_recorder.main]
}

resource "aws_config_configuration_recorder_status" "main" {
  name       = aws_config_configuration_recorder.main.name
  is_enabled = true

  depends_on = [aws_config_delivery_channel.main]
}

resource "aws_config_config_rule" "s3_encrypted" {
  name = "s3-bucket-server-side-encryption-enabled"

  source {
    owner             = "AWS"
    source_identifier = "S3_BUCKET_SERVER_SIDE_ENCRYPTION_ENABLED"
  }

  depends_on = [aws_config_configuration_recorder.main]
}

The aws_s3_bucket.config bucket needs a bucket policy that allows Config, similar to the one for CloudTrail. Config charges per configuration item recorded, so recording everything in a busy account is not free.

Security Hub #

securityhub.tf
resource "aws_securityhub_account" "main" {}

data "aws_region" "current" {}

resource "aws_securityhub_standards_subscription" "fsbp" {
  standards_arn = "arn:aws:securityhub:${data.aws_region.current.name}::standards/aws-foundational-security-best-practices/v/1.0.0"

  depends_on = [aws_securityhub_account.main]
}

Security Hub depends on Config being enabled to evaluate many controls. Add the CIS standard with its own ARN if you need compliance reports.

Act on the findings #

Send the findings to a team: an EventBridge rule on GuardDuty Finding or Security Hub Findings - Imported events with severity high that publishes to an SNS topic, which notifies by email or chat. Add CloudWatch alarms for root account usage and unauthorized API calls.

Costs #

All four are paid services with free trials (GuardDuty and Security Hub have 30 days). Costs grow with the number of events and resources. Estimate before enabling them organization-wide, and scan your own Terraform with security scanners.

#AWS #Security #Terraform #OpenTofu