AWS Security Monitoring with Terraform: CloudTrail, GuardDuty, Config and Security Hub
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 #
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 #
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_trailrecords all regions with one trail.enable_log_file_validationlets 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 #
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 #
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 #
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.