How To Implement AWS SSB Controls in Terraform - Part 2
Tags: aws, terraform, security
Published: 2024-03-18 22:59:08

## Introduction

The [AWS Startup Security Baseline (SSB)](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/welcome.html) defines a set of controls that comprises a lean but solid foundation for the security posture of your AWS accounts. In [part 1](https://dev.to/aws-builders/how-to-implement-aws-ssb-controls-in-terraform-part-1-3ggm) of our [blog series](https://dev.to/acwwat/series/26815), 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](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-07.html) requires that actions taken by users, roles, and services in your AWS account be recorded using [AWS CloudTrail](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/cloudtrail-user-guide.html).

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](https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-service-event.html#eb-service-event-cloudtrail) 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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/cloudtrail). Since CloudTrail writes events to an S3 bucket, you also need to create one with the appropriate bucket policy. Here is a basic example:

```terraform
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](https://docs.aws.amazon.com/awscloudtrail/latest/userguide/creating-trail-organization.html) 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](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-08.html) requires the [S3 Block Public Access feature](https://docs.aws.amazon.com/AmazonS3/latest/userguide/access-control-block-public-access.html) 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](https://aws.amazon.com/about-aws/whats-new/2022/12/amazon-s3-automatically-enable-block-public-access-disable-access-control-lists-buckets-april-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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_bucket_public_access_block) to configure the settings as appropriate. Here is an example that enables block public access for an S3 bucket:

```terraform
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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/s3_account_public_access_block) to enable public public access at the account level as follows:

```terraform
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](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-09.html) requires that unused resources be deleted or disabled to reduce the opportunity for security issues.

In particular, the [default VPC](https://docs.aws.amazon.com/vpc/latest/userguide/default-vpc.html) 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`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/default_vpc) and [`aws_default_subnet`](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/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](https://registry.terraform.io/providers/cloudposse/awsutils/latest) to remove the default VPC resources more efficiently. The `awsutils` module provides the [`awsutils_default_vpc_deletion` resource](https://registry.terraform.io/providers/cloudposse/awsutils/latest/docs/resources/default_vpc_deletion), 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
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](https://docs.aws.amazon.com/controltower/latest/userguide/account-factory.html), you can [uncheck all regions](https://docs.aws.amazon.com/controltower/latest/userguide/configuring-account-factory-with-VPC-settings.html) 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](https://docs.aws.amazon.com/controltower/latest/userguide/aft-feature-options.html#delete-default-vpc-option) if you are using that feature to dispense new accounts.

## ACCT.10 – Monitor Costs

The account control [ACCT.10](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-10.html) requires cost monitoring and notification using services such as [AWS Budgets](https://docs.aws.amazon.com/cost-management/latest/userguide/budgets-managing-costs.html).

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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/budgets_budget). Here is an example configuration that creates budgets similar to the [Monthly cost budget template](https://docs.aws.amazon.com/cost-management/latest/userguide/budget-templates.html):

```terraform
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](https://docs.aws.amazon.com/cost-management/latest/userguide/getting-started-ad.html) 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](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_monitor) and subscriptions using the [`aws_ce_anomaly_subscription` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ce_anomaly_subscription) 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.

```terraform
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](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-11.html) recommends enabling [Amazon GuardDuty](https://docs.aws.amazon.com/guardduty/latest/ug/what-is-guardduty.html) to continuously monitor for malicious and unauthorized behavior to help protect against threats.

To enable GuardDuty in Terraform, use the [`aws_guardduty_detector` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector) to enable the service and the [`aws_guardduty_detector_feature` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_detector_feature) 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.

```terraform
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:

```terraform
provider "aws" {
  alias   = "management"
  profile = "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" {
  provider = aws.audit
  enable   = true
}

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

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

>💡 Based on the feedback I received, I have written a new blog post [How To Manage Amazon GuardDuty in AWS Organizations Using Terraform](https://dev.to/acwwat/how-to-manage-amazon-guardduty-in-aws-organizations-using-terraform-3pg6) with more details on this topic. Feel free to read it if you are interested.

As an alternative to auto enablement, you can use the [`aws_guardduty_member` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_member) to add GuardDuty members individually and use the [`aws_guardduty_invite_accepter` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/guardduty_invite_accepter) 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](https://aws.amazon.com/guardduty/pricing/) and understand how much each feature costs. You should [leverage the 30-day free trial period to estimate the GuardDuty cost](https://docs.aws.amazon.com/guardduty/latest/ug/monitoring_costs.html). 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](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-12.html) recommends using [AWS Trusted Advisor](https://docs.aws.amazon.com/awssupport/latest/user/trusted-advisor.html) 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](https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html). There is however a [`trusted-advisor-refresh` module](https://registry.terraform.io/modules/trussworks/trusted-advisor-refresh/aws/latest) that helps refresh the Trusted Advisor check results more often than the [automatic one-week schedule](https://docs.aws.amazon.com/awssupport/latest/user/get-started-with-aws-trusted-advisor.html#refresh-check-results) 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](https://docs.aws.amazon.com/awssupport/latest/user/organizational-view.html#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](https://dev.to/acwwat/series/26815), 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](https://blog.avangards.io/).