How To Implement AWS SSB Controls in Terraform - Part 1
Tags: aws, terraform, security
Published: 2024-03-17 22:23:57

## Introduction

In my AWS consultant role, I have helped numerous organizations with building a landing zone or reviewing their existing AWS environment against best practices and recommend remediations, with a focus on security. Many of them happen to be in the early stage of adopting AWS due to a recent migration or starting a new business. These companies look to us in guiding them towards best practices when they have limited time and resources to learn on their own.

As a big fan of frameworks and prescriptive guidance from AWS, I often spend hours browsing through them for inspirations to improve my deliverables. Early on, I came across the [AWS Security Baseline (SSB)](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/welcome.html) which I found immensely suitable for the profile of customers I work with. I then started incorporating it into my toolset, particularly the Terraform templates that I deploy landing zones with. Given the effectiveness of the framework, I wanted to share my experience with the AWS community and explain how you too can add these security controls into your Terraform configuration.

Since I will be going through each control in details, I organized the write-up into a [blog series](https://dev.to/acwwat/series/26815) consisting of four blog posts so that it is easier to follow. The first two will cover the account controls, while the latter two will cover the workload controls. With that said, let's first look at what the AWS SSB is about.

## What is the AWS Startup Security Baseline (SSB)?

The [AWS Startup Security Baseline (SSB)](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/welcome.html) comprises a collection of controls designed to establish a foundational level of security for startups and organizations in early phase of adopting AWS, while maintaining their agility on AWS. There are two types of controls in the AWS SSB:

1. Account controls, which help keep your AWS account secure.
    
2. Workload controls, which help secure your workloads and data in the cloud.
    

These controls can be considered "low-hanging fruits" that are easy to implement but provides a decent level of security, which is perfect for organizations that are still navigating through the intricacy of managing an AWS environment and figuring out an operational model. These controls also carry over to a multi-account model as your organization's AWS usage matures.

If you practice DevOps and leverage Infrastructure-as-Code (IaC), you'd be please to learn that these controls can easily be implemented in IaC including Terraform which this blog series focuses on. We will dive into each control and how it can be defined in Terraform configurations. Let's start with the first account control ACCT.01, which covers setting account-level contacts to valid email distribution lists.

## ACCT.01 – Set Account-Level Contacts

The account control [ACCT.01](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-01.html) requires that account-level primary and alternate contacts be set, ideally with email distribution lists.

Setting contacts is generally a one-time task, so it is often done in the AWS Management Console. However, it can also be done easily in Terraform using the [`aws_account_primary_contact` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_primary_contact) and the [`aws_account_alternate_contact` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/account_alternate_contact). The following example demonstrates how to set the primary contact and the alternate billing contact:

```terraform
# Please don't fact check the contact info!
resource "aws_account_primary_contact" "this" {
  address_line_1     = "742 Evergreen Terrace"
  city               = "Springfield"
  company_name       = "Mr. Plow"
  country_code       = "US"
  district_or_county = "Pressboard Estates"
  full_name          = "Homer Simpson"
  phone_number       = "+16365553226"
  postal_code        = "49007"
  state_or_region    = "NT"
  website_url        = "https://www.mrplow.com"
}

resource "aws_account_alternate_contact" "billing" {
  alternate_contact_type = "BILLING"
  name                   = "Marge Simpson"
  title                  = "CFO"
  email_address          = "finance@mrplow.com"
  phone_number           = "+16365553226"
}
```

> 💡For a multi-account landing zone with many member accounts, consider using plus addressing (a.k.a. sub-addressing) to reduce the number of required email distribution list. You can either define plus addressing by account (for example, `awsaccount1+billing@example.com`, `awsaccount1+security@example.com`, etc.) or by function (for example, `awsbilling+account1@example.com`, `awsbilling+account2@example.com`, etc.)

## ACCT.02 – Restrict Use of the Root User

The account control [ACCT.02](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-02.html) requires that the root user be put away from further use after all initial account setup activities are completed.

Root user configuration is typically done in the AWS Management Console, therefore we won't be using Terraform. You can refer to [Root user best practices for your AWS account](https://docs.aws.amazon.com/IAM/latest/UserGuide/root-user-best-practices.html) in the IAM User guide for more information. You may create additional administrative users for day-to-day use while following the controls that are described below.

## ACCT.03 – Configure Console Access

The account control [ACCT.03](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-03.html) recommends using temporary credentials to grant access to AWS accounts and resources.

Most AWS users will just use IAM initially to manage access to their AWS environment. While there are resources to manage IAM users in Terraform, it is certainly not the best approach. IAM users are *stateful* resources which end-users will modify themselves (for example, changing password or adding an virtual MFA device). These out-of-band changes will cause perpetual changes in Terraform unless you leverage the [`ignore_changes` lifecycle meta-argument](https://developer.hashicorp.com/terraform/language/meta-arguments/lifecycle#ignore_changes), which is a chore to configure. As well, Terraform stores [sensitive state data](https://developer.hashicorp.com/terraform/language/state/sensitive-data) in plain text, which as you know is not very secure. For these reasons, it is recommended to leverage identity federation and single sign-on to manage users to your AWS environment.

> 💡 As your organization grows, you will find a multi-account architecture and identity federation to be increasingly valuable for better workload and environment separation. Consider using [AWS Organizations](https://docs.aws.amazon.com/organizations/latest/userguide/orgs_introduction.html) and [AWS IAM Identity Center](https://docs.aws.amazon.com/singlesignon/latest/userguide/what-is.html) even for your single account environment. You can create a new organizations and enroll your existing account as the member, then enable single sign-on (SSO) with IAM Identity Center. You can also start with [AWS Control Tower](https://docs.aws.amazon.com/controltower/latest/userguide/what-is-control-tower.html) or convert your organization into an AWS Control Tower landing zone later.

With that said, you can use the [`aws_iam_user` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user) to create the user if you must. For example:

```terraform
resource "aws_iam_user" "john_doe" {
  name = "john_doe"
}
```

You can then use the [`aws_iam_user_login_profile` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_user_login_profile) to create the *initial* password:

```terraform
resource "aws_iam_user_login_profile" "john_doe" {
  user    = aws_iam_user.john_doe.name
}

output "john_doe_password" {
  # encrypted_password is the base64-encoded password
  # It provides a tad more security than the password attribute
  value = aws_iam_user_login_profile.john_doe.encrypted_password
}
```

You will also need to assign IAM policies to the user (or better yet, to a group to which the user is then assigned) as described in the next control.

## ACCT.04 – Assign Permissions

The account control [ACCT.04](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-04.html) requires user permissions to be configured by assigning policies to their IAM identity following the least privilege principle.

[IAM policies](https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_manage.html) are fundamental constructs that define what a principal can access within an AWS environment. There are two types of IAM policies - AWS-managed and user-managed. In Terraform, AWS-managed policies can be used via the [`aws_iam_policy` data source](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/iam_policy), for instance:

```terraform
data "aws_iam_policy" "lambda_basic_exec_role" {
  name = "AWSLambdaBasicExecutionRole"
}
```

You can also use this data source to retrieve IAM policies that are created outside of your Terraform configuration. Meanwhile, managed policies can be created with the [`aws_iam_policy` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_policy). The following is an example of a policy for a hypothetical Lambda function that processes some CloudWatch metrics and sends an email report via SES:

```terraform
resource "aws_iam_role_policy" "cw_stats_email_lambda_exec" {
  name        = "CWStatsEmailLambdaExecutionPolicy"
  description = "Grants permissions to the CWStatsEmail Lambda function."
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Action = [
          "cloudwatch:GetMetricStatistics"
        ]
        Effect     = "Allow"
        "Resource" = "*"
      },
      {
        Action = [
          "ses:SendEmail",
          "ses:SendRawEmail"
        ]
        Effect     = "Allow"
        "Resource" = "*"
      }
    ]
  })
}
```

How you use these IAM policies thereafter depends on the IdP. If you are using IAM directly, the best practice is to assign the policies to IAM groups or IAM roles as mentioned earlier. The assignment can be done in Terraform with the [`aws_iam_group_policy_attachment` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_group_policy_attachment) and the [`aws_iam_role_policy_attachment` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_role_policy_attachment) respectively. Using the same example above, the Terraform configuration below creates the Lambda execution role and attaches both the AWS-managed policy `AWSLambdaBasicExecutionRole` and the user-managed policy `CWStatsEmailLambdaExecutionPolicy` to the role:

```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
}

resource "aws_iam_role" "cw_stats_email_lambda_exec" {
  name        = "CWStatsEmailLambdaExecutionRole"
  description = "Execution role for the CWStatsEmail Lambda function."
  assume_role_policy = jsonencode({
    Version = "2012-10-17"a 
    Statement = [
      {
        Action = "sts:AssumeRole"
        Effect = "Allow"
        Principal = {
          Service = "lambda.amazonaws.com"
        }
        Condition = {
          StringEquals = {
            "aws:SourceAccount" = "${local.account_id}"
          }
          ArnLike = {
            "aws:SourceArn" = "arn:aws:lambda:${local.region}:${local.account_id}:function:*"
          }
        }
      }
    ]
  })
  managed_policy_arns = [data.aws_iam_policy.lambda_basic_exec_role.arn]
}

resource "aws_iam_role_policy_attachment" "cw_stats_email_lambda_exec" {
  policy_arn = aws_iam_policy.cw_stats_email_lambda_exec.arn
  role       = aws_iam_role.cw_stats_email_lambda_exec.name
}
```

If you are integrating an external OIDC or SAML IdP to IAM directly, the federated principal will also use IAM roles to access the AWS environment. The process to create IAM roles for this scenario is similar to the above, but you would need to adjust the trust policy (the `assume_role_policy` attribute) accordingly.

If you have set up identity federation using IAM Identity Center, permissions are assigned as IAM policies to [permissions sets](https://docs.aws.amazon.com/singlesignon/latest/userguide/permissionsetsconcept.html), which are then associated with users and groups for accounts in the organization. Permissions can be created in Terraform using the [`aws_ssoadmin_permission_set` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_permission_set) typically in the management account of the organization. Here is an example for setting up a permission set for network administrators:

```terraform
data "aws_ssoadmin_instances" "" {}

data "aws_iam_policy" "network_admin" {
  name = "NetworkAdministrator "
}

resource "aws_ssoadmin_permission_set" "network_admin" {
  name             = "MyOrgNetworkAdministrator"
  description      = "Grants full access permissions to AWS services and actions required to set up and configure AWS network resources."
  instance_arn     = tolist(data.aws_ssoadmin_instances.this.arns)[0]
}

resource "aws_ssoadmin_managed_policy_attachment" "network_admin" {
  instance_arn       = tolist(data.aws_ssoadmin_instances.this.arns)[0]
  managed_policy_arn = aws_iam_policy.network_admin.arn
  permission_set_arn = aws_ssoadmin_permission_set.network_admin.arn
}
```

The permission set can then be assigned to an IAM Identity Center user or group for an account in the organization using [`aws_ssoadmin_account_assignment` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ssoadmin_account_assignment).

## ACCT.05 – Require MFA

The account control [ACCT.05](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-05.html) requires MFA to be enabled for AWS account access especially for long-term user credentials as a security best practice.

IAM users can enable MFA devices via the AWS Management Console given the appropriate permissions. For more information see [Using multi-factor authentication (MFA) in AWS](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_mfa.html) in the IAM User Guide.

In terms of Terraform, although the [`aws_iam_virtual_mfa_device` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_virtual_mfa_device) can be used to provision an IAM virtual MFA device, user still needs to associate an actual device to their IAM user with the provided attributes `base_32_string_seed` or `qr_code_png`. Terraform is also not a particularly appropriate choice for managing IAM users due to the aforementioned security and procedural implications, so it's best left for separate management with a process that fits the IT security requirements of your organization.

If you have set up identity federation, MFA should be managed in the centralized IdP instead. For example, AWS IAM Identity Provider has [MFA support](https://docs.aws.amazon.com/singlesignon/latest/userguide/enable-mfa.html), and you can expect enterprise-grade IdP such as Microsoft Entra ID to have advanced [MFA capabilities](https://learn.microsoft.com/en-us/entra/identity/authentication/concept-mfa-howitworks) and additional features such as [Conditional Access](https://learn.microsoft.com/en-us/entra/identity/conditional-access/overview).

## ACCT.06 – Enforce a Password Policy

The account control [ACCT.06](https://docs.aws.amazon.com/prescriptive-guidance/latest/aws-startup-security-baseline/acct-06.html) requires that passwords adhere to a strong password policy, ideally one that aligns with the [Center for Internet Security (CIS) Password Policy Guide](https://www.cisecurity.org/insights/white-papers/cis-password-policy-guide), to help prevent discovery through brute force or social engineering.

The [password policy](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_credentials_passwords_account-policy.html) for IAM users are set on the account. The following is a mapping of the CIS Password Policy Guide recommendations to the IAM password policy settings. You may tune them as you see fit.

|  | Password Only | With MFA |
| --- | --- | --- |
| Password minimum length | 14 | 8 |
| Require at least one uppercase letter from the Latin alphabet (A-Z) | Yes | Yes |
| Require at least one lowercase letter from the Latin alphabet (a-z) | Yes | Yes |
| Require at least one number | Yes | Yes |
| Require at least one non-alphanumeric character | Yes | Yes |
| Password expires in `n` day(s) | 365 | 365 |
| Allow users to change their own password | Yes | Yes |
| Prevent password reuse from the past `n` changes | 5 | 5 |

> 💡 The CIS Password Policy Guide argues that password composition requirements are not effective because it leads to users choosing predictable patterns that are prone to dictionary attacks for convenience. However, there is also no common standard. Since this is an opinionated, I chose to follow the default settings in IAM.

To configure the account password policy in Terraform, use the [`aws_iam_account_password_policy` resource](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/iam_account_password_policy) as follows:

```terraform
resource "aws_iam_account_password_policy" "this" {
  allow_users_to_change_password = true
  max_password_age               = 365
  minimum_password_length        = 8
  password_reuse_prevention      = 5
  require_lowercase_characters   = true
  require_numbers                = true
  require_symbols                = true
  require_uppercase_characters   = true
}
```

If you have set up identity federation, the password policy should primarily be managed in the centralized IdP. That being said, it never hurts to update the IAM account password policy as above just in case.

> 💡 As for enforcing MFA, I would recommend adopting a detective approach using services such as [AWS Trusted Advisor](https://docs.aws.amazon.com/awssupport/latest/user/trusted-advisor.html)</a> (for [root account MFA](https://docs.aws.amazon.com/awssupport/latest/user/security-checks.html#mfa-root-account)) or [Amazon Security Hub](https://docs.aws.amazon.com/securityhub/latest/userguide/what-is-securityhub.html) with a standard that includes rules such as [[IAM.5] MFA should be enabled for all IAM users that have a console password](https://docs.aws.amazon.com/securityhub/latest/userguide/iam-controls.html#iam-5) and [[IAM.9] MFA should be enabled for the root user](https://docs.aws.amazon.com/securityhub/latest/userguide/iam-controls.html#iam-9) </a>.

## Summary

In this first 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 account-level controls that pertain to account and identity and explained how you can implement them using Terraform. In the next installment, we will focus on the remaining account-level controls. Please continue to follow the series and check out other posts in the [Avangards Blog](https://blog.avangards.io/).