How To Implement AWS SSB Controls in Terraform - Part 2

How To Implement AWS SSB Controls in Terraform - Part 2

Learn how to implement the AWS SSB using Terraform for the remaining account controls not yet covered in part 1 of the blog series.

Introduction

The AWS Startup Security Baseline (SSB) defines a set of controls that comprises a lean but solid foundation for the security posture of your AWS accounts. In part 1 of our blog series, we examined how to implement account controls related to account-level and identity settings using Terraform. In this installment, we will look at the remaining account controls that focus on both proactive and preventive security and governance measures. Let's begin with ACCT.07, which mandates the CloudTrail log delivery to a protected S3 bucket.

ACCT.07 – Log Events

The account control ACCT.07 requires that actions taken by users, roles, and services in your AWS account be recorded using AWS CloudTrail.

CloudTrail enables auditing, security monitoring, and operational troubleshooting by tracking user activity and API usage. Any API event that CloudTrail records can be used as an event source in Amazon EventBridge to trigger various automations. AWS does not charge for the first trail that records management events, making it a cost-effective choice to adopt.

You can create a CloudTrail trail using Terraform with the aws_cloudtrail resource. Since CloudTrail writes events to an S3 bucket, you also need to create one with the appropriate bucket policy. Here is a basic example:

data "aws_caller_identity" "this" {}

data "aws_region" "this" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
  region     =  data.aws_region.this.name
}

# Note: Bucket versioning and server-side encryption are not shown for brevity
resource "aws_s3_bucket" "cloudtrail" {
  bucket = "aws-cloudtrail-logs-${local.account_id}-${local.region}"
}

resource "aws_s3_bucket_policy" "cloudtrail" {
  bucket = aws_s3_bucket.cloudtrail.id
  policy = <<-EOT
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "AWSCloudTrailAclCheck",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:GetBucketAcl",
      "Resource": "${aws_s3_bucket.cloudtrail.arn}"
    },
    {
      "Sid": "AWSCloudTrailWrite",
      "Effect": "Allow",
      "Principal": {
        "Service": "cloudtrail.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "${aws_s3_bucket.cloudtrail.arn}/AWSLogs/${local.account_id}/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": "bucket-owner-full-control"
        }
      }
    }
  ]
}
EOT
}

resource "aws_cloudtrail" "this" {
  name                       = "aws-cloudtrail-logs-${local.account_id}-${local.region}"
  s3_bucket_name             = aws_s3_bucket.cloudtrail.id
  enable_log_file_validation = true
  is_multi_region_trail      = true
  advanced_event_selector {
    field_selector {
      field  = "eventCategory"
      equals = ["Management"]
    }
  }
}

If you are using AWS Organizations, you can create an organization trail in the management account to logs events for all accounts. In Terraform, an organization trail can be created by setting the is_organization_trail argument to true for the aws_cloudtrail resource. If you are using AWS Control Tower, a standard organization trail is created automatically when you launch your landing zone. You can import and manage it using Terraform thereafter.

ACCT.08 – Prevent Public Access To Private S3 Buckets

The account control ACCT.08 requires the S3 Block Public Access feature to be enabled if public access is not required.

To ensure security by default, AWS enables Block Public Access by default for S3 buckets created on or after April 28, 2023. For S3 buckets that are created earlier, you may still need to enable the feature by yourself. In Terraform, you can use the aws_s3_bucket_public_access_block resource to configure the settings as appropriate. Here is an example that enables block public access for an S3 bucket:

data "aws_caller_identity" "this" {}

locals {
  account_id = data.aws_caller_identity.current.account_id
}

resource "aws_s3_bucket" "alb_access_logs" {
  bucket = "alb-access-logs-${local.account_id}"
}

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

If you are confident that all S3 buckets in your account do not require public access, you can also use the aws_s3_account_public_access_block resource to enable public public access at the account level as follows:

resource "aws_account_public_access_block" "alb_access_logs" {
  block_public_acls       = true
  block_public_policy     = true
  ignore_public_acls      = true
  restrict_public_buckets = true
}

ACCT.09 – Delete Unused Resources

The account control ACCT.09 requires that unused resources be deleted or disabled to reduce the opportunity for security issues.

In particular, the default VPC that is automatically created in each AWS account and enabled region should be considered for deletion. Default VPCs are created with public subnets that automatically assign IPv4 addresses, so novice AWS users could inadvertently expose private workloads to the internet. A multi-VPC environment with peering requirements also ought to use a well-defined CIDR allocation scheme other than the default 172.31.0.0/16 range. It is therefore recommended that you delete the default VPCs and create ones that are more thought out as necessary.

In Terraform, there are resources such as aws_default_vpc and aws_default_subnet which can technically be used to delete the default VPC resources with the force_destroy argument set to true. However, you would first have to define these resources in your Terraform configuration to "bring them in" before you can destroy them, making it a two-step process.

Alternatively, you can use the awsutils module from cloudposse to remove the default VPC resources more efficiently. The awsutils module provides the awsutils_default_vpc_deletion resource, which when defined in your Terraform configuration will deletes the default VPC along with the child resources of the VPC in the configured region, for example:

terraform {
  required_providers {
    awsutils = {
      source = "cloudposse/awsutils"
    }
  }
}

provider "awsutils" {
  region = "us-east-1"
}

resource "awsutils_default_vpc_deletion" "default" {}

However, it might be more efficient to simply write a shell script to delete the default VPC from all regions instead of using Terraform.

In the case where your AWS environment is created using AWS Control Tower's Account Factory, you can uncheck all regions so that the default VPC is not created in any of them. Account Factory for Terraform (AFT) also has an option to delete the default VPC if you are using that feature to dispense new accounts.

ACCT.10 – Monitor Costs

The account control ACCT.10 requires cost monitoring and notification using services such as AWS Budgets.

AWS Budgets allows users to set custom budgets for AWS resource usage and sends notifications when actual or forecasted usage exceeds the budgeted amounts. A budget can be created in Terraform using the aws_budgets_budget resource. Here is an example configuration that creates budgets similar to the Monthly cost budget template:

resource "aws_budgets_budget" "this" {
  name         = "My Monthly Cost Budget"
  budget_type  = "COST"
  limit_amount = "1000"
  limit_unit   = "USD"
  time_unit    = "MONTHLY"
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 85
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["finance@example.com"]
  }
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "ACTUAL"
    subscriber_email_addresses = ["finance@example.com"]
  }
  notification {
    comparison_operator        = "GREATER_THAN"
    threshold                  = 100
    threshold_type             = "PERCENTAGE"
    notification_type          = "FORECASTED"
    subscriber_email_addresses = ["finance@example.com"]
  }
}

Although it is not mentioned in the documentation for this control, I would also recommend that you also configure AWS Cost Anomaly Detection as an additional safeguard for cost overruns. This feature uses machine learning models to detect and alert on anomalous spending patterns in your deployed AWS services. You can create a cost monitor using the aws_ce_anomaly_monitor resource and subscriptions using the aws_ce_anomaly_subscription resource in Terraform. The following is an example that sets up a cost monitor and a daily summary alert when the cost is 50% above the expected spend.

resource "aws_ce_anomaly_monitor" "service" {
  name              = "AWSServiceMonitor"
  monitor_type      = "DIMENSIONAL"
  monitor_dimension = "SERVICE"
}

resource "aws_ce_anomaly_subscription" "service_daily" {
  name      = "DAILYSUBSCRIPTION"
  frequency = "DAILY"
  monitor_arn_list = [
    aws_ce_anomaly_monitor.service.arn
  ]
  subscriber {
    type    = "EMAIL"
    address = "finance@example.com"
  }
  threshold_expression {
    dimension {
      key           = "ANOMALY_TOTAL_IMPACT_PERCENTAGE"
      match_options = ["GREATER_THAN_OR_EQUAL"]
      values        = ["50"]
    }
  }
}

ACCT.11 – Enable GuardDuty

The account control ACCT.11 recommends enabling Amazon GuardDuty to continuously monitor for malicious and unauthorized behavior to help protect against threats.

To enable GuardDuty in Terraform, use the aws_guardduty_detector resource to enable the service and the aws_guardduty_detector_feature resource to enable individual features. The following is a full example that enables all available protection features:

GuardDuty is a regional service and thus must be enabled in each region that you are using.
resource "aws_guardduty_detector" "this" {
  enable                       = true
  finding_publishing_frequency = "SIX_HOURS"
}

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

resource "aws_guardduty_detector_feature" "eks" {
  detector_id = aws_guardduty_detector.this.id
  name        = "EKS_AUDIT_LOGS"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "runtime" {
  detector_id = aws_guardduty_detector.this.id
  name        = "RUNTIME_MONITORING"
  status      = "ENABLED"
  additional_configuration {
    name   = "EKS_ADDON_MANAGEMENT"
    status = "ENABLED"
  }
  additional_configuration {
    name   = "ECS_FARGATE_AGENT_MANAGEMENT"
    status = "ENABLED"
  }
}

resource "aws_guardduty_detector_feature" "malware" {
  detector_id = aws_guardduty_detector.this.id
  name        = "EBS_MALWARE_PROTECTION"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "rds" {
  detector_id = aws_guardduty_detector.this.id
  name        = "RDS_LOGIN_EVENTS"
  status      = "ENABLED"
}

resource "aws_guardduty_detector_feature" "lambda" {
  detector_id = aws_guardduty_detector.this.id
  name        = "LAMBDA_NETWORK_LOGS"
  status      = "ENABLED"
}

If you have a multi-account landing zone that uses AWS Organizations or AWS Control Tower, you can use the aws_guardduty_organization_admin_account resource, the aws_guardduty_organization_configuration resource, and the aws_guardduty_organization_configuration_feature resource to configure GuardDuty at the organization level. Here is an example that configures GuardDuty with a delegated administrator to the Audit account (which you generally find in a Control Tower landing zone) and auto-enable GuardDuty for all member accounts in the organization:

provider "aws" {
  alias = "management"
}

provider "aws" {
  alias   = "audit"
  profile = "audit"
}

data "aws_caller_identity" "audit" {
  provider = aws.audit
}

locals {
  audit_account_id = data.aws_caller_identity.audit.account_id
}

resource "aws_guardduty_organization_admin_account" "this" {
  admin_account_id = local.audit_account_id
}

resource "aws_guardduty_detector" "audit" {
  enable   = true
  provider = aws.audit
}

resource "aws_guardduty_organization_configuration" "audit" {
  auto_enable_organization_members = "ALL"
  detector_id                      = aws_guardduty_detector.audit.id
  provider                         = aws.audit
}

resource "aws_guardduty_organization_configuration_feature" "audit_s3" {
  auto_enable = "ALL"
  detector_id = aws_guardduty_detector.audit.id
  name        = "S3_DATA_EVENTS"
  provider    = aws.audit
}

As an alternative to auto enablement, you can use the aws_guardduty_member resource to add GuardDuty members individually and use the aws_guardduty_invite_accepter resource at the member account to accept the invitation. Since the fully automated method is preferred, we won't go through an example for these resources.

GuardDuty is a costly service, so make sure that you review the pricing and understand how much each feature costs. You should leverage the 30-day free trial period to estimate the GuardDuty cost. This allows you to weigh the cost against the risks and requirements before deciding whether to enable GuardDuty and which protections to enable.

ACCT.12 – Monitor High-Risk Issues

The account control ACCT.12 recommends using AWS Trusted Advisor to scan for high-risk or high-impact issues related to security, performance, cost, and reliability.

If you do not have a Business Support Plan or higher, you are only eligible for some basic security checks and service limit checks in Trusted Advisor. The Trusted Advisor API also cannot be used to enable automation such as refreshing check results and custom notification. So Trusted Advisor is restrictive and frankly not very useful at the free tier.

There is no Terraform resource for interacting with Trusted Advisor, so you need to configure notification in the AWS Management Console. There is however a trusted-advisor-refresh module that helps refresh the Trusted Advisor check results more often than the automatic one-week schedule if you have access to the Trusted Advisor API with a higher-tier Support Plan.

If you are using AWS Organizations or AWS Control Tower, you can enable organizational view in the management account. However the Support Plan requirement still applies to each member account, that is, you will not receive any additional checks in member accounts that do not have Business Support Plan or higher.

Summary

In this second blog post of the series How to implement the AWS Startup Security Baseline (SSB) using Terraform, we examined the remaining account-level controls and explained how you can implement them using Terraform. In the next installment, we will focus on the the workload-level controls for complete coverage. Please continue to follow the series and check out other posts in the Avangards Blog.