Intro

This is a complete guide to help you achieve compliance with the CIS AWS Foundations Benchmark. By following this guide, you can launch infrastructure that is compliant with the Benchmark recommendations, and you’ll be set to retain a compliant state over time because all of the infrastructure is defined as code. This guide targets version 1.3.0 of the Benchmark.

Note
Previously, we supported version 1.2.0 of the Benchmark. If you are looking to upgrade from v1.2.0 to v1.3.0, please follow the upgrade guide instead.

CIS Benchmark Architecture

What is the CIS AWS Foundations Benchmark?

The CIS Benchmarks are objective, consensus-driven configuration guidelines developed by security experts to help organizations improve their security posture. The AWS Foundations Benchmark is a set of configuration best practices for hardening AWS accounts to establish a secure foundation for running workloads on AWS. It also provides ongoing monitoring to ensure that the account remains secure.

What you’ll learn in this guide

This guide consists of five main sections:

Core concepts

An overview of the AWS Foundations Benchmark, including its control sections and structure.

Production-grade design

How to use infrastructure as code to achieve compliance with minimal redundancy and maximum flexibility.

Deployment walkthrough

A step-by-step guide to achieving compliance using the Gruntwork Infrastructure as Code Library and the Gruntwork CIS AWS Foundations Benchmark wrapper modules.

Next steps

How to measure and maintain compliance.

Traceability matrix

A reference table that maps each Benchmark recommendation to the corresponding section in the deployment walkthrough.

Feel free to read the guide from start to finish or skip around to whatever part interests you!

Core concepts

The CIS AWS Foundations Benchmark is organized into the following sections:

  1. Identity and Access Management

  2. Storage

  3. Logging

  4. Monitoring

  5. Networking

There are multiple recommendations within each section. Note the use of the term recommendation as opposed to control or requirement. This reinforces the point that CIS is a self-imposed, best-practices standard, as opposed to compulsory or regulated and centralized standards such as the PCI DSS for the payment card industry or HIPAA for covered health care entities.

Assessment Status

Each recommendation is classified as either Automated or Manual. Automated recommendations indicate that the check for the recommendation may be accessed programmatically (e.g., an API exists to validate or enable the recommendation). Manual recommendations must be checked and remediated manually.

Profiles

The Benchmark defines two profile levels. Level one recommendations are easier to implement, incur less overhead, but still substantially improve security. Level two recommendations are meant for highly sensitive environments with a lower risk appetite. They may be more difficult to implement and/or cause more overhead in day-to-day usage.

CIS Controls

Each recommendation is also linked to a corresponding CIS Control. The controls are distinct from the Benchmark. They’re described by CIS as "a prioritized set of actions that collectively form a defense-in-depth set of best practices that mitigate the most common attacks against systems and networks." Organizations seeking to implement a comprehensive security program or framework can use the controls to measure their progress and prioritize security efforts. The Foundations Benchmark is just one of several guidelines that can help reach the bar set by the CIS Controls. Refer to the Benchmark document directly to view how the recommendations map to controls.

Recommendation sections

Identity and Access Management

Number of recommendations: 22

The recommendations in this section involve the use of identity, accounts, authentication, and authorization. On AWS, most identity and access control related concerns are managed using the eponymous IAM service. Hence, most (but not all) of the recommendations in this section discuss particular IAM configurations, such as the configuration of the password policy, the use of various groups and roles, and the configuration of multi-factor authentication (MFA) devices.

Storage

Number of recommendations: 3

This section was added in the version 1.3.0 and involves the use of storage capabilities of AWS. The relevant services for this section are S3 and EC2. The recommendations in this section pertain to in-transit and at-rest encryption.

Logging

Number of recommendations: 11

AWS has a variety of logging, monitoring, and auditing features, and the Benchmark has recommendations for several of them:

AWS has several other logging related features that are not covered directly by the Benchmark. For example, the primary log ingestion and query service, Amazon CloudWatch Logs, is integrated with many other AWS services. The Benchmark does not contain any recommendations specifically for CloudWatch Logs, though many recommendations do make mention of it.

Monitoring

Number of recommendations: 15

Monitoring is an overloaded term in the industry. In the context of the AWS Foundations Benchmark, the monitoring section is exclusively about monitoring for specific API calls using the CloudTrail service paired with CloudWatch Logs filter metrics. Each recommendation in this section spells out a specific filter and an associated alarm.

Networking

Number of recommendations: 4

The Benchmark is uncomfortably light on networking, considering its central role in the security of any distributed system. The recommendations merely limit traffic from the zero network (0.0.0.0/0) and suggest limiting routing for VPC peering connections based on the principle of least-privilege.

Production-grade design

In Core concepts we discussed the basics of the AWS Foundations Benchmark. Although it’s possible to achieve compliance with the Benchmark by manually configuring each setting in the web console or entering the CLI commands, we strongly discourage this approach. It precludes the myriad benefits of using code to manage infrastructure.

Instead, we advise using Terraform (or similar tools, such as CloudFormation or Pulumi) to configure cloud resources programmatically. This section will cover the Terraform resources you can use to implement each of the recommendations. We assume that you’re familiar with the basics of Terraform. If you aren’t, read our Introduction to Terraform blog post, or pick up the 2nd edition of Terraform Up & Running.

Identity and Access Management

The first section of the Benchmark centers on Identity and Access Management, including the following:

  • Avoiding usage of the "root" account

  • Requiring MFA for IAM users

  • Setting a specific password policy

  • Disabling administrative permissions

  • Limiting the use of API access keys

  • Using IAM roles

  • Removing expired SSL/TLS certificates

  • Enabling IAM Access Analyzer

In the subsequent sections, we’ll review the recommendations and discuss how to implement them using Terraform resources and data sources.

Configure authentication

One of main areas of concern in the IAM section relates to authentication. The Benchmark has recommendations for IAM users and the root user account, password policy, and multi-factor authentication. There is more than one way to authenticate to AWS, and the method you choose determines how to implement these recommendations in your code.

Federated authentication using SAML

Perhaps the most robust and secure method for authenticating to AWS is to use federated SAML authentication with an identity provider (IdP) like Okta, Google, or Active Directory. In this configuration, users authenticate to the IdP and assume IAM roles to obtain permissions in AWS. All user management is handled in the IdP, where you can assign roles to users according to their needs. If you use this approach, several of the Benchmark recommendations, including recommendations 1.10, 1.15, and 1.11, are not applicable (assuming you have no IAM users at all).

Configuring SAML is a multi-step process that is outside the scope of this guide. Familiarize yourself with the process by reviewing the AWS documentation on the matter. You can use the aws_iam_saml_provider and aws_iam_policy_document Terraform resources to manage your SAML provider via code.

IAM user authentication

Another option is to authenticate using IAM users. The accounts are created and managed directly in AWS as opposed to a third-party provider. IAM users log in to AWS with a password and an optional MFA device. IAM users are easier to get started with than SAML, and they’re also free to use. However, to avoid unauthorized access, it’s crucial to configure the IAM user settings securely. IAM users may be more suitable for smaller environments with only a few users.

A few tips on creating IAM users with Terraform:

  • To create IAM users, use the aws_iam_user and aws_iam_user_login_profile resources.

  • As instructed by recommendation 1.11, do not create API access keys for new users automatically. The intent is that users should create them on their own if and when needed.

  • To stay compliant with recommendation 1.15, be sure to never attach IAM policies directly to IAM users. Instead, create IAM groups, attach policies to those groups, and add the user to groups using the aws_iam_user_group_membership. This helps to avoid scenarios where auditing the exact permissions of IAM users becomes difficult and unmaintainable.

Consider the following example which creates a user with access to AWS Support:

resource "aws_iam_user" "support_user" {
  name = "support"
}

resource "aws_iam_group" "example_group" {
  name = "support-group"
}

resource "aws_iam_group_policy_attachment" "support_group_attach" {
  group      = aws_iam_group.example_group.name
  policy_arn = "arn:aws:iam::aws:policy/AWSSupportAccess"
}

resource "aws_iam_user_group_membership" "example" {
  user = aws_iam_user.example_user.name
  groups = [aws_iam_group.example_group.name]
}

This code creates an IAM user called support, adds them to a new group called support-group, and attaches the AWSSupportAccess managed policy to the group. It demonstrates how to meet a few of the Benchmark recommendations:

  1. The user is created without an API access key (recommendation 1.11). Access keys should only be created by the user later.

  2. The policy is attached to an IAM group, not directly to the IAM user (recommendation 1.15).

  3. Recommendation 1.17 specifically requires that the Support policy be used. You should attach it to a group, as shown here.

Do not use full administrator privileges

Recommendation 1.16 states that no IAM policies with full administrator privileges be assigned. However, some administrator access is needed to maintain the account on an ongoing basis, and use of the root account is also prohibited. What to do?

One approach is to create an IAM policy with full permissions to IAM and nothing else. Attach the policy to a group, and give access only to trusted users. This allows effective administrator access without an explicit administrator policy. For example, you could use the following Terraform code to create such a policy:

data "aws_iam_policy_document" "iam_admin" {
  statement {
    sid = "iamAdmin"
    actions = [
      "iam:*",
    ]
    resources = ["*"]
    effect = "Allow"
  }
}

You can then attach that policy to a group:

resource "aws_iam_policy" "iam_admin" {
  name   = "iam_admin"
  path   = "/"
  policy = data.aws_iam_policy_document.iam_admin.json
}

resource "aws_iam_group" "iam_admin" {
  name = "iam-admins"
}

resource "aws_iam_group_policy_attachment" "iam_admin_group_attach" {
  group      = aws_iam_group.iam_admin.name
  policy_arn = aws_iam_policy.iam_admin.arn
}

In this example, any IAM user that is a member of the iam-admins group will have has permissions to access all functionality in the IAM service, make them an effective administrator of the account.

Enabling multi-factor authentication for IAM users

Recommendation 1.10, which requires all IAM users to have MFA enabled, seems straightforward on the surface, but in AWS, there’s no way to explicitly require MFA for log in. Instead, you can make sure that all groups and roles have a conditional IAM policy attached that explicitly denies all actions unless MFA is enabled. This way, whenever a user logs in without MFA, all services will show a permission denied error if the user didn’t use MFA.

The AWS documentation has an example of this policy. Create the policy with Terraform, and attach it to every group you create - including the iam-admins and support groups we created above. Here’s an example:

data "aws_iam_policy_document" "require_mfa_policy" {
  statement {
    sid = "AllowViewAccountInfo"
    effect = "Allow"
    actions = ["iam:ListVirtualMFADevices"]
    resources = ["*"]
  }

  statement {
    sid = "AllowManageOwnVirtualMFADevice"
    effect = "Allow"
    actions = [
      "iam:CreateVirtualMFADevice",
      "iam:DeleteVirtualMFADevice"
    ]
    resources = [
      "arn:aws:iam::${var.aws_account_id}:mfa/$${aws:username}",
    ]
  }

  statement {
    sid = "AllowManageOwnUserMFA"
    effect = "Allow"
    actions = [
      "iam:DeactivateMFADevice",
      "iam:EnableMFADevice",
      "iam:GetUser",
      "iam:ListMFADevices",
      "iam:ResyncMFADevice"
    ]
    resources = [
      "arn:aws:iam::${var.aws_account_id}:user/$${aws:username}",
      "arn:aws:iam::${var.aws_account_id}:mfa/$${aws:username}"
    ]
  }

  statement {
    sid = "DenyAllExceptListedIfNoMFA"
    effect = "Deny"
    not_actions = [
      "iam:CreateVirtualMFADevice",
      "iam:EnableMFADevice",
      "iam:GetUser",
      "iam:ListMFADevices",
      "iam:ListVirtualMFADevices",
      "iam:ResyncMFADevice",
      "sts:GetSessionToken"
    ]
    resources = ["*"]
    condition {
      test     = "Bool"
      variable = "aws:MultiFactorAuthPresent"
      values   = ["false"]
    }
  }
}

resource "aws_iam_group" "support" {
  name  = "support"
}


resource "aws_iam_group_policy" "require_mfa_for_support" {
  name   = "RequireMFA"
  group  = aws_iam_group.support.name
  policy = data.aws_iam_policy_document.require_mfa_policy
}

We’ve created an IAM policy that denies all access accept the necessary permissions to set up an MFA device, then we attached the policy to the support group. If a user that is a member of the support group logs in without MFA, they won’t have access to any services, even if the support group or the user had other policies attached. They will have enough permissions to set up an MFA device, and after doing so, they can log in and will have any permissions granted to them by other IAM policies.

Attach a policy like this one to every group in your account.

Password policy

The IAM password policy is perhaps the most straightforward and explicit set of recommendations (1.8-1.9 and 1.12) in the entire Benchmark. You can invoke the Terraform aws_iam_account_password_policy resource to implement the recommended policy.

For example:

resource "aws_iam_account_password_policy" "aws_foundations_benchmark_policy" {
  minimum_password_length        = 14
  allow_users_to_change_password = true
  hard_expiry                    = true
  max_password_age               = 90
  password_reuse_prevention      = 24
}

Cleanup Expired SSL/TLS certificates

The CIS AWS v1.3 recommendations require that all expired SSL/TLS certificates stored in AWS IAM are automatically removed (see 1.19). Unfortunately removing expired certificates via AWS Management Console is not currently supported so we must remove then using the AWS API. To view the current certificates stored in IAM, use the AWS CLI and execute the list-server-certificates command:

aws iam list-server-certificates

The command output should return an array that contains all of the SSL/TLS certificates currently stored in IAM and their metadata:

{
	"ServerCertificateMetadataList": [{
		"ServerCertificateId": "EHDGFRW7EJFYTE88D",
		"ServerCertificateName": "MyServerCertificate",
		"Expiration": "2020-07-10T23:59:59Z",
		"Path": "/",
		"Arn": "arn:aws:iam::012345678910:server-
		certificate / MySSLCertificate ",
		"UploadDate": "2018-06-10T11:56:08Z"
	}]
}

The Expiration attribute contains the expiration date for each SSL/TLS certificate which you can use to determine if it should be removed. To remove the certificate use the delete-server-certificate command, making sure to substitute <CERTIFICATE_NAME> with the ServerCertificateId attribute from the previous command:

aws iam delete-server-certificate --server-certificate-name <CERTIFICATE_NAME>

To automate this process you might decide to implement a Lambda function that runs on a regular schedule and removes all expired SSL/TLS certificates. Check out the Automatically remove expired SSL/TLS certificates from IAM section of the deployment walkthrough to see how to deploy a ready-made module that can be deployed in each of your AWS accounts.

IAM Access Analyzer

As of version 1.3.0, the CIS recommendations stipulate that the AWS IAM Access Analyzer service is enabled across all active regions in a given AWS Account or Organization.

To achieve this compliance requirement, enable the IAM Access Analyzer service for every AWS region you have enabled in every one of your AWS accounts. Alternatively, you could make use of the iam-access-analyzer-multi-region module available in the Gruntwork Service Catalog, as described in the Steps to enable IAM Access Analyzer for all enabled AWS regions section of the deployment walkthrough.

Once enabled, it will scan only within the boundaries of the AWS Account or Organization it has access to. Only specific resources are analyzed and included in the results - e.g. S3 buckets, SQS, etc. (For the full list of resources supported, please visit the relevant AWS docs). This lets you identify unintended access to these resources and data by external entities.

The findings from the IAM Access Analyzer can be found in the AWS web console, and can be archived or resolved. Please visit the AWS guidance on how to do so.

Manual steps

A few of the recommendations in the IAM section are not achievable via API and require a one-time manual configuration. Perform the steps in this section manually.

Enable MFA for the root account

Securing the "root" user, or the first user that is created when you set up an AWS account, is one of the first actions you should take in any new account. Unfortunately, there is no API or automation available for configuring an MFA device for the root user. Follow the manual steps outlined in the AWS docs. Configuring a virtual MFA device will achieve recommendation 1.5. You can also refer to the production-grade AWS account structure guide.

For the paranoid: configure a hardware MFA device, as suggested by recommendation 1.6. We suggest using a Yubikey due to its reputation for strong security characteristics and multitude of form factors. Refer to the documentation for more information on using a hardware device with the root user.

Subscribe to SNS topic

The Config alerts and CloudWatch Metric Alarms all go to an SNS topic. Unfortunately, there is no way to automate subscribing to the SNS topic as each of the steps require validating the delivery target. Follow the steps outlined in the AWS docs to be notified by Email, Phone, or SMS for each of the alerts.

You can also configure an automated system integration if you have a third party alerting system or central dashboard. Follow the steps in the AWS docs on how to add an HTTPS endpoint as a subscriber to the alerts.

Answer security questions and complete contact details

When setting up a new account, AWS asks for contact information and security questions. Unfortunately, there is no API or automation available for this functionality. In the AWS console, visit the Account settings page and complete the Alternate Contacts and Configure Security Challenge Questions questions.

For further detail, follow the manual steps outlined in the CIS Benchmark document and refer to the AWS Secure Account Setup steps.

Storage

Version 1.3.0 of the Benchmark includes a new storage section that has three recommendations pertaining to the S3 service as well as the EC2 service. These have to do with encryption at rest and in transit.

To comply with recommendation 2.1.1, make sure to enable server side encryption on your S3 buckets. In Terraform, this is achieved by configuring the server_side_encryption_configuration argument of the aws_s3_bucket resource.

To comply with recommendation 2.1.2, make sure that all access to your S3 buckets is over TLS. In Terraform, you will want to attach a policy to your buckets that includes a statement similar to this:

statement {
  sid     = "AllowTLSRequestsOnly"
  effect  = "Deny"
  actions = ["s3:*"]
  resources = [
    <YOUR BUCKET ARN>,
    "${<YOUR BUCKET ARN>}/*"
  ]
  principals {
    type        = "*"
    identifiers = ["*"]
  }
  condition {
    test     = "Bool"
    variable = "aws:SecureTransport"
    values   = ["false"]
  }
}

To comply with recommendation 2.2.1 be sure to configure EBS volume encryption in all of the enabled AWS regions within your AWS Account(s). You can invoke the Terraform aws_ebs_encryption_by_default resource to implement the recommendation.

For example:

resource "aws_ebs_encryption_by_default" "ebs_encryption" {
  enabled = true
}

Logging

In the Logging section, the Benchmark recommendations target the following services:

We’ll cover each of them in turn.

AWS CloudTrail

The Benchmark has specific requirements for the CloudTrail configuration, described in recommendations 3.1-4, 3.6-7 and 3.10-11. The CloudTrail must have the following characteristics:

  1. Collects events in all regions

  2. Enables log file integrity validation

  3. Ensures that the S3 bucket used by CloudTrail is not publicly accessible

  4. Integrates CloudTrail with CloudWatch Logs

  5. Encrypts CloudTrail logs at rest

  6. Enables access logging for the CloudTrail S3 bucket

  7. Enables object-level logging for read and write events for the CloudTrail S3 bucket

Use the aws_cloudtrail Terraform resource to create the CloudTrail. Include the following settings in the CloudTrail configuration:

is_multi_region_trail         = true
include_global_service_events = true
enable_log_file_validation    = true
s3_bucket_name                = <YOUR BUCKET NAME>
cloud_watch_logs_group_arn    = <YOUR CLOUDWATCH LOGS GROUP ARN>

event_selector {
  read_write_type           = "All"
  include_management_events = true

  data_resource {
    type   = "AWS::S3::Object"
    values = [<YOUR BUCKET ARN>]
  }
}

You’ll also need the aws_s3_bucket, aws_s3_account_public_access_block resources to create an S3 bucket for the CloudTrail to send its events to and to disable public access to the bucket; you wouldn’t want to expose the CloudTrail data publicly!

Finally, you’ll need the aws_cloudwatch_log_group resource to create a CloudWatch Log group as another location for CloudTrail to send events. Use this ARN for the aws_cloudtrail resource cloud_watch_logs_group_arn parameter when creating the CloudTrail.

AWS Config

Benchmark recommendation 3.5 states that AWS Config be enabled in all regions. This is challenging to implement with Terraform because running a particular configuration in all regions is not a feature that Terraform has natively. Terraform has loops, but they aren’t available for the purpose of repeating a resource in many regions. Unfortunately, at the time of writing, there isn’t a way to complete this recommendation without repetitive code.

To proceed, start by creating a Terraform module that takes the following actions:

  1. Creates an SNS topic for publishing Config events

  2. Creates an S3 bucket for Config events and disables public access

  3. Creates an IAM role for the config service to access an S3 bucket and an SNS topic

  4. Creates a configuration recorder

  5. Creates a delivery channel

  6. Enables the configuration recorder

When the module is working and sets up AWS Config according to the prescribed configuration, you should invoke it once for each region in the account. One way to do this is to use provider aliases. For example, you could specify one provider for each region, then invoke the module for each provider:

# The default provider configuration
provider "aws" {
  alias  = "us-east-1"
  region = "us-east-1"
}

# Additional provider configuration for west coast region
provider "aws" {
  alias  = "us-west-2"
  region = "us-west-2"
}

# ... repeat the provider for each region in the AWS account

module "aws_config_us_east_1" {
  source = "/path/to/your/config/module"
  providers = {
    aws = aws.us-east-1
  }
}

module "aws_config_us_west_2" {
  source = "/path/to/your/config/module"
  providers = {
    aws = aws.us-west-2
  }
}

# ... repeat the module invocation for each provider

When AWS launches new regions, they are not enabled by default, so you won’t need to add to this list over time.

Alternatively, you could disable the regions you aren’t using and only enable AWS Config for those that you need.

KMS Key rotation

Finally, a simple recommendation! To meet recommendation 3.8, create KMS keys with key rotation enabled. Using Terraform, it looks like this:

resource "aws_kms_key" "example" {
  description         = "Example Key"
  enable_key_rotation = true
}

VPC Flow Logs

Under the Benchmark, all VPCs must have a Flow Log to log network traffic. Use the aws_flow_log Terraform resource, being sure to use log_destination_type=cloud-watch-logs.

Because the recommendation is to attach flow logs to every single VPC, you’ll need to repeat the configuration for all the default VPCs which exist in all regions of the account. You can use the cloud-nuke defaults-aws command to easily remove all the default VPCs (and default security groups) from all regions of an account, making it easier to achieve this recommendation.

Monitoring

The Monitoring section has 15 recommendations for creating specific CloudWatch Logs metric filters that send alarms to an SNS topic when a particular condition is met.

The easiest way to achieve this recommendation is to create a Terraform module that creates CloudWatch Logs metrics filters and CloudWatch Alarms, and then invoke the module once for each recommendation. You’ll need the aws_cloudwatch_log_metric_filter and aws_cloudwatch_metric_alarm Terraform resources.

Networking

The networking section involves a paltry four recommendations. We don’t consider this section to be sufficient to ensure a secure networking configuration. For a deeper dive, refer to Gruntwork’s How to deploy a production-grade VPC on AWS guide, which includes recommendations for segmentation using network ACLs, security groups, and remote access. Moreover, our Reference Architecture can get you up and running with a secure network configuration immediately.

Recommendation 5.1 requires that you use Network ACL rules to block all access to the remote server administration ports, such as SSH to port 22 and Remote Desktop to port 3389, by default. You can then add additional NACL rules to allow remote admin access, but only from specific CIDR blocks. Recommendation 5.2 similarly allows you to allow remote admin access from specific CIDR blocks in your Security Groups. Note that allowing remote admin access from all IPs (0.0.0.0/0) is NOT allowed, so instead, if you require SSH or Remote Desktop to your cloud resources, provide a more restricted CIDR range, such as the IP addresses of your offices.

To meet recommendation 5.3, run the cloud-nuke defaults-aws command to remove the rules from all default security groups. Note that it isn’t possible to actually delete the default security group, so instead the command deletes the rules, eliminating the risk of something being mistakenly exposed.

Finally, for recommendation 5.4, the guidance is straightforward: when creating peering connections between VPCs, do not create routes for subnets that don’t need them. In other words, only create routes between subnets that need them based on the services running on those subnets. This can help to avoid exposing services between networks unnecessarily.

Deployment walkthrough

The Production-grade design section describes in detail the Terraform resources to use and the approach to take for each recommendation, but we’ve already done that grunt work! This section documents how to achieve compliance using the Infrastructure as Code modules from Gruntwork.

Pre-requisites

This walkthrough has the following pre-requisites:

Gruntwork Infrastructure as Code Library

This guide uses code from the Gruntwork Infrastructure as Code Library, as it implements most of the production-grade design for you out of the box. Make sure to read How to use the Gruntwork Infrastructure as Code Library.

Gruntwork Compliance for CIS AWS Foundations Benchmark

This guide also uses code from the Gruntwork CIS AWS Foundations Benchmark repository, which contains the necessary configurations to achieve compliance.

Important
You must be a Gruntwork Compliance subscriber to access the Gruntwork Infrastructure as Code Library and the CIS AWS Foundations Benchmark modules.
How to configure a production-grade AWS account structure

Review the production-grade AWS account structure guide to familiarize yourself with many of the concepts that this walkthrough depends on.

Terraform

This guide uses Terraform to define and manage all the infrastructure as code. If you’re not familiar with Terraform, check out A Comprehensive Guide to Terraform, A Crash Course on Terraform, and How to Use the Gruntwork Infrastructure as Code Library.

The Gruntwork solution

Gruntwork offers battle-tested infrastructure as code modules to help you create production grade infrastructure in a fraction of the time it would take to develop from scratch. Each of the code modules are "compliance-ready"; they are mostly unopinionated by default, but they can be configured for compliance with the right settings.

To further simplify and expedite compliance, the Gruntwork Compliance modules are "wrappers" around the core, unopinionated modules in the Infrastructure as Code Library. The wrappers call the core modules with configuration values that are compliant with the AWS Foundations Benchmark. You can use the wrapper modules by creating a module of your own (this can be considered a second wrapper) and using the compliance module as the source. Optionally, you can also use terragrunt to call your module, thus creating a chain of IaC modules.

wrappers
Figure 1. Nested wrapper modules help to avoid repetitive code and minimize the amount of extra work needed to achieve compliance.

Let’s unpack this a bit.

Core modules

Core modules are broadly applicable and can be used with or without compliance requirements. For example, the iam-groups core module creates a best practices set of IAM groups. The groups are configurable according to your needs. You could, for example, choose to create a group with read-only access, another group with full administrator access, and no other groups. All Gruntwork subscribers have access to the core modules, which reside in Gruntwork’s infrastructure as code repositories.

Compliance wrapper modules

The compliance wrapper modules are an extension of the IaC Library. They use the source argument in a Terraform module block to invoke the core module with a configuration that is customized for compliance with the CIS AWS Foundations Benchmark. These modules are in the terraform-aws-cis-service-catalog repository (accessible to Gruntwork Compliance subscribers).

infrastructure-modules

The infrastructure-modules are your organization’s "blueprint" for how to deploy infrastructure. You can use infrastructure-modules to customize the settings according to the needs of your environment. Subscribers can refer to the canonical ACME infrastructure-modules repository for an example of how you might use infrastructure-modules.

If you’re using Terragrunt, the infrastructure-modules are optional; you can call the compliance wrapper modules directly as the source from a Terragrunt configuration. The benefit of this is that you have less code to maintain by depending directly on Gruntwork’s upstream compliance modules.

infrastructure-live

infrastructure-live uses Terragrunt to make it easier to work with Terraform modules in multiple environments. infrastructure-live is optional - you can use all of the modules with or without Terragrunt.

If you’re not using Terragrunt, you can use infrastructure-modules to call the compliance wrapper modules. Refer to the canonical ACME infrastructure-live repository, and in particular the Infrastructure walkthrough for comprehensive documentation on how infrastructure-live, infrastructure-modules, and the core IaC modules interact.

Benefits

This modular, decomposed approach allows for maximum code reuse. The core modules can be used with or without compliance, depending on how they are configured. The compliance wrappers are like shadows of the core modules; they pass through most of the variables to the core modules without alteration, but hard code any settings needed for compliance. When you call the compliance modules from your own code in infrastructure-modules, you only need to set up any variables that are custom for your environment. Often times the default settings are good enough.

You can use this approach on AWS account. In many cases, you’ll only need compliance for production accounts, but the same methodology can be applied to pre-production accounts as well.

If you need to brush up on how the IaC Library works, read the How to use the Gruntwork Infrastructure as Code Library guide.

Manual steps

Start by completing the manual configurations describe above in Manual steps.

Create an IAM user password policy

After the manual steps are complete, the next step is to create an IAM user password policy using the iam-password-policy wrapper module. Complete this step before creating any IAM users!

Using the wrapper module

This section demonstrates how to use the CIS compliance password policy wrapper module to create a password policy. Follow this pattern for each of the wrapper modules discussed throughout this walkthrough.

Using the wrapper module with Terraform

If you’re using Terraform without Terragrunt, use this section to create a module in your infrastructure-modules repository.

infrastructure-modules
└── security
    └── iam-password-policy
        ├── main.tf
        └── variables.tf

Inside of main.tf, configure your AWS provider settings:

infrastructure-modules/security/iam-password-policy/main.tf
provider "aws" {
  region = var.aws_region
}

Now use the iam-password-policy compliance module from the Gruntwork terraform-aws-cis-service-catalog repository, making sure to replace the <VERSION> placeholder with the latest version from the releases page:

infrastructure-modules/security/iam-password-policy/main.tf
module "iam_password_policy" {
  # Make sure to replace <VERSION> in this URL with the latest terraform-aws-cis-service-catalog release
  source                  = "git@github.com:gruntwork-io/terraform-aws-cis-service-catalog.git//modules/iam-password-policy?ref=<VERSION>"
  minimum_password_length = var.minimum_password_length
  aws_region              = var.aws_region
}

The module parameters should be exposed as input variables in variables.tf:

infrastructure-modules/security/iam-password-policy/variables.tf
variable "aws_region" {
  description = "The AWS region in which all resources will be created"
  type        = string
}

variable "minimum_password_length" {
  description = "Minimum length to require for user passwords."
  type        = number
  default     = 14
}

With the module in place, it’s time to test your code. See Manual tests for Terraform code and Automated tests for Terraform code for instructions.

Merge and release your password policy module

Once the module is working the way you want, submit a pull request, get your changes merged into the master branch, and create a new versioned release by using a Git tag. For example, to create a v0.3.0 release:

git tag -a "v0.3.0" -m "Created iam-password-policy module"
git push --follow-tags

With the module released, you can use Terraform to create the password policy. Follow the instructions in the terraform usage example.

Using the compliance wrapper module with Terragrunt

If you’re using Terragrunt, you can use the password policy Terraform module in the CIS compliance repository from a Terragrunt configuration directly, with no need to create a Terraform module of your own! In this case, terraform-aws-cis-service-catalog is the canonical Terraform module. To do so, configure a terragrunt.hcl to invoke the module. First, create a directory for the password policy in your infrastructure-live repository:

infrastructure-live
└── <your account>
    └── _global
        └── iam-password-policy
            └── terragrunt.hcl

In the directory structure above, <your account> is a friendly name for the account you’re configuring for compliance. For example, the prod account or the security account. If this doesn’t make sense, see this post for more information on Terragrunt.

Next, create terragrunt.hcl with the following contents:

infrastructure-live/<your account>/_global/iam-password-policy/terragrunt.hcl
terraform {
  # Make sure to replace <VERSION> in this URL with the latest terraform-aws-cis-service-catalog release
  source = "git::ssh://git@github.com/gruntwork-io/terraform-aws-cis-service-catalog.git//modules/iam-password-policy?ref=<VERSION>"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

You can test this by simply running terragrunt apply. This will pull the iam-password-policy compliance module and apply it using the credentials you have defined in AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY. When you’re satisfied with the configuration, follow the merge process described in [merge_release_terragrunt_wrapper].

See also the terragrunt usage example.

Steps for authentication via SAML

If you’re unfamiliar with SAML authentication and identity providers, start with the federated authentication section of the Gruntwork production-grade AWS account structure guide. You may also find the AWS SAML-based Federation documentation to be helpful. Once you select an IdP, populate it with users and follow the provider’s documentation to configure SAML with AWS. If you use SAML authentication alone, with no IAM users, the account will immediately be compliant with several of the Benchmark recommendations!

Once the IdP is ready, proceed with the steps below.

Set up IAM roles for SAML

Use the saml-iam-roles wrapper module to configure a compliant-set of IAM roles and policies by following the pattern outlined in Using the wrapper module. The module creates a minimal, best practices set of of IAM roles that may be assumed from the SAML provider. Tweak the vars.tf according to your needs.

Enable MFA in the IdP

Ensure that MFA is configured for all AWS users in your IdP. Strictly speaking, MFA in the IdP is not required for compliance with the Benchmark. However, the intent of the Benchmark requirement is that all AWS users should have MFA, and we strongly advise this practice.

Create an IAM group for access to support

Use the iam-groups wrapper module to create a standardized set of IAM groups by following the pattern outlined in Using the wrapper module. The module will create a group called support with the AWSSupportAccess managed policy attached. Customize the variables in the module to create only the groups you want.

Use the IAM admin role for administration

To ensure compliance with recommendation 1.16, the saml-iam-roles wrapper module does not create any roles with explicit administrator (*:*) permissions. Instead, to grant "effective" administrator access to a SAML user, use the allow-iam-admin-access-from-saml role. Users that assume this role have the ability to grant, revoke, and update IAM permissions as needed. From a privileges standpoint, this is the same as full administrator access, so be judicious with this permission.

If you need to provision IAM users in addition to SAML, proceed with Steps for authentication via IAM users. Otherwise, continue with Use IAM roles for EC2 instances.

Steps for authentication via IAM users

If you’re new to IAM, refer to the Core concepts section of the production-grade AWS account structure guide. Once you’re familiar with IAM, proceed with the following configuration steps:

Create cross-account roles

If you’re using multiple AWS accounts, the best way to set up access to each account is to create a set of roles that can be assumed from a central account using the AssumeRole feature. This way you only need to create IAM users in the central account rather than in each account individually . You can create roles in your sub accounts using the cross-account-iam-roles wrapper module and the pattern outlined in Using the wrapper module.

Refer to the IAM roles section of the production-grade AWS account structure guide to learn more about cross-account roles.

Create IAM groups

Next, create a compliant set of IAM groups with the iam-groups wrapper module by following the pattern outlined in Using the wrapper module. If you’re using multiple AWS accounts, add the roles created in the previous step to the iam_groups_for_cross_account_access list.

Create IAM users

Now use the iam-users core module to create users and add them to the groups created in the previous step. Follow the pattern outlined in Using the wrapper module to use the iam-users module, even though there isn’t a wrapper module for iam-users because there isn’t anything particular needed to reach compliance.

If you are using multiple AWS accounts, create users in a central AWS account that you wish to use for authentication. For example, you might use a "security" account for authentication, and use the previously created cross-account roles and associated IAM groups to enable users to use AssumeRole to access other accounts (e.g. dev, stage, and production) where your applications run.

(Optional) Create custom IAM groups or roles

If you need to create customized IAM groups and/or roles, use the custom-iam-entity wrapper module. You can use this module to attach managed policies to roles and groups, and to require that MFA is in use. Refer to the example usage.

Use IAM roles for EC2 instances

All Gruntwork modules that require AWS API access use roles rather than an IAM user with static API credentials for authentication. For example:

Use these modules whenever possible. You should always use IAM roles in your own modules any time you need to provide access to the AWS API. Using static API credentials should be avoided whenever possible.

Automatically remove expired SSL/TLS certificates from IAM

Next, use the cleanup-expired-certs module to deploy a Lambda function to remove all expired SSL/TLS certificates from AWS IAM. This module will add all of the relevant IAM resources needed to periodically execute the Lambda function and optionally, report metrics to CloudWatch. Be sure to deploy this module into each of your AWS accounts.

If you’re using Terragrunt, then you can invoke the standalone module directly by following the pattern outlined in Using the compliance wrapper module with Terragrunt and creating a terragrunt.hcl file with the following contents:

infrastructure-live/<your account>/_global/cleanup-expired-certs/terragrunt.hcl
terraform {
  # Make sure to replace <VERSION> in this URL with the latest terraform-aws-cis-service-catalog release
  source = "git::ssh://git@github.com/gruntwork-io/terraform-aws-cis-service-catalog.git//modules/cleanup-expired-certs?ref=<VERSION>"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

inputs = {
  schedule_expression                = "rate(1 hour)"
  report_cloudwatch_metric           = true
  report_cloudwatch_metric_namespace = "custom/cis"
  report_cloudwatch_metric_name      = "cleanup-expired-iam-certs-count"
}

Steps to enable IAM Access Analyzer for all enabled AWS regions

To enable IAM Access Analyzer in your AWS Account or Organization, you need to do it separately for every region.

If you’re using Terragrunt, then you can invoke the standalone module directly by following the pattern outlined in Using the compliance wrapper module with Terragrunt and using a terragrunt.hcl file with the following contents:

infrastructure-live/<your account>/_global/iam-access-analyzer/terragrunt.hcl
terraform {
# Make sure to replace <VERSION> in this URL with the latest terraform-aws-security release
source = "git::ssh://git@github.com/gruntwork-io/terraform-aws-security.git//modules/iam-access-analyzer-multi-region?ref=<VERSION>"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

inputs = {
  iam_access_analyzer_type = "ACCOUNT"
  iam_access_analyzer_name = "iam-access-analyzer"
  seed_region              = "us-east-2"
}

And refer to the Readme document here: README.adoc.

Maintaining compliance by following IAM best practices

We conclude the IAM section with a few parting words of wisdom for maintaining compliance over time:

  1. Do not attach any policies without requiring MFA.

  2. Never use the AdministratorAccess AWS managed policy with any users, groups, or roles.

  3. Refrain from granting inline permissions or attaching managed policies directly to IAM users. Permissions should be granted exclusively via IAM groups and roles.

  4. Never use static IAM user access keys to allow an application to access AWS, whether that application is hosted on an EC2 instance or anywhere else!

  5. Avoid logging in as the root user. Unfortunately, there is nothing built-in to AWS to prevent use of the root user. It cannot be locked or removed from the account. In fact, there are several tasks that require the use of root. Fortunately, most of these activities are rare, so usage of the root account can be kept to a minimum.

Configure storage

S3 Buckets

To make sure your S3 buckets are compliant with the benchmark, use the private-s3-bucket module to create and manage all of your S3 buckets. This module blocks public access and enforces encryption by default. Note that all Gruntwork modules that create S3 buckets use this module under the hood.

You can either use the private-s3-bucket module in your own modules, or, if you wish to deploy a standalone S3 bucket, use the s3-bucket service from the Gruntwork Service Catalog.

Encrypt EBS volumes

Use the ebs-encryption-multi-region module to configure AWS EBS encryption in all enabled regions of an AWS Account, as per the Benchmark recommendation.

If you’re using Terragrunt, then you can invoke the standalone module directly by following the pattern outlined in Using the compliance wrapper module with Terragrunt and creating a terragrunt.hcl file with the following contents:

infrastructure-live/<your account>/_global/ebs-encryption/terragrunt.hcl
terraform {
  # Make sure to replace <VERSION> in this URL with the latest terraform-aws-security release
  source = "git::ssh://git@github.com/gruntwork-io/terraform-aws-security.git//modules/ebs-encryption-multi-region?ref=<VERSION>"
}

# Include all settings from the root terragrunt.hcl file
include {
  path = find_in_parent_folders()
}

inputs = {
  enable_encryption = true
  seed_region       = "us-east-2"
}

Configure logging

The logging section of the Benchmark includes configurations for CloudTrail, AWS Config, KMS keys, and VPC flow logs.

CloudTrail

Use the cloudtrail wrapper module to establish a compliant CloudTrail configuration. The wrapper module will configure CloudTrail with all the characteristics required by the Benchmark. Refer to the terraform and terragrunt examples.

Enable AWS Config in all regions

Use the aws-config-multi-region module to enable AWS Config in every region of an AWS account, along with a global configuration recorder in one region, as per the Benchmark recommendation.

Enable key rotation for KMS keys

Use the kms-master-key module to create KMS keys with key rotation enabled by default.

Create VPC flow logs

The Benchmark recommends enabling VPC Flow Logs for all VPCs in all regions. You can use the vpc service in the CIS Service Catalog to create your VPCs. This service is configured for CIS compliance, and as such has VPC flow logs enabled. For example, you might create a VPC using the VPC service:

infrastructure-modules/networking/vpc/myvpc/main.tf
module "vpc" {
  # Replace <VERSION> with the most recent release from https://github.com/gruntwork-io/terraform-aws-cis-service-catalog/releases
  source = "git::git@github.com:gruntwork-io/terraform-aws-cis-service-catalog.git//modules/networking/vpc?ref=<VERSION>"

  vpc_name                                                = var.vpc_name
  aws_region                                              = var.aws_region
  cidr_block                                              = var.cidr_block
  num_nat_gateways                                        = var.num_nat_gateways
  allow_administrative_remote_access_cidrs_public_subnets = var.allow_administrative_remote_access_cidrs_public_subnets
}

Under the hood, the service will enable VPC flow logs. All that’s remaining is to define the parameters in a variables.tf:

infrastructure-modules/networking/vpc/myvpc/variables.tf
variable "aws_region" {
  description = "The AWS region to deploy to (e.g. us-east-1)"
  type        = string
}

variable "vpc_name" {
  description = "The name of the VPC to create"
  type        = string
}

variable "cidr_block" {
  description = "The IP address range of the app VPC in CIDR notation."
  type        = string
}

variable "num_nat_gateways" {
  description = "The number of NAT Gateways to launch for this VPC."
  type        = number
}

variable "allow_administrative_remote_access_cidrs_public_subnets" {
  description = "A map of CIDR blocks that will be allowed access to administrative ports (e.g., SSH, RDP) in the public subnet tier."
  type        = map(string)

  # Example:
  #
  #   default = {
  #    UsOffice = "1.2.3.4/32"
  #    EuOffice = "5.6.7.8/32"
  # }
}

Refer to the VPC example code.

To limit the number of flow logs, you may want to use the cloud-nuke defaults-aws command. It will remove the default VPC from all regions in an account, saving you the hassle of creating flow logs in each default VPC.

Configure monitoring

The Monitoring section of the Benchmark centers on a collection of CloudWatch Logs Metric Filters. Gruntwork has simplified this section to a single module: the cloudwatch-logs-metric -filters wrapper module. It will create and configure all the CloudWatch Logs metric filters necessary for compliance with the Benchmark. Note that when you use the cloudtrail wrapper module as recommended in CloudTrail, this module will automatically be invoked inline so that you don’t have to do anything special to enable the metric filters on the deployed CloudTrail configuration.

Note that you must have a subscriber on the SNS topic to be compliant. Refer to Subscribe to SNS topic for details on how to setup a subscriber to the SNS topics that are created.

Configure networking

To ensure all the networking recommendations are satisfied, use the vpc (and/or vpc-mgmt) service from Gruntwork’s CIS Service Catalog to create all your VPCs. These services are specifically configured for CIS compliance, and as such they don’t allow security groups to access ports 22 or 3389 from the world. In addition, our architecture has a least-privileges-based routing configuration by default.

To meet the 5.1 recommendation, you’ll need to provide values for the allow_administrative_remote_access_* variables when creating VPCs. These variables are used to create appropriate Network ACL Rules. For example, you might create a VPC using the vpc service from terraform-aws-cis-service-catalog:

infrastructure-modules
└── networking
    └── vpc
        └── myvpc
            ├── main.tf
            └── variables.tf
infrastructure-modules/networking/vpc/myvpc/main.tf
module "vpc" {
  # Replace <VERSION> with the most recent release from the https://github.com/gruntwork-io/terraform-aws-cis-service-catalog/releases:
  source = "git::git@github.com:gruntwork-io/terraform-aws-cis-service-catalog.git//modules/networking/vpc?ref=<VERSION>"

  # Set the basic required variables first
  vpc_name         = var.vpc_name
  aws_region       = var.aws_region
  cidr_block       = var.cidr_block
  num_nat_gateways = var.num_nat_gateways

  # Next, pass values for the allow_administrative_remote_access_* variables, thus creating the NACL rules under the hood
  allow_administrative_remote_access_cidrs_public_subnets              = var.allow_administrative_remote_access_cidrs
  allow_administrative_remote_access_cidrs_private_app_subnets         = { all_app_vpc_cidrs  = module.vpc.vpc_cidr_block }
  allow_administrative_remote_access_cidrs_private_persistence_subnets = { all_app_vpc_cidrs  = module.vpc.vpc_cidr_block }

Refer to the cis-infrastructure-modules-acme and cis-infrastructure-live-acme, if you are using Terragrunt.

Finally, run the cloud-nuke defaults-aws command to remove all default security groups from all VPCs in all regions.

Next steps

Congratulations! If you’ve made it this far, you should have achieved compliance with the CIS AWS Foundations Benchmark. Now it’s time to confirm that your configurations are correct and you didn’t miss any steps.

Use the aws-securityhub module to enable AWS Security Hub in every region of an AWS account to check your account for compliance with the AWS CIS Foundations Benchmark. The Security Hub runs the exact audit steps specified in the Benchmark using AWS Config managed rules. By enabling the Security Hub, you can track your compliance efforts and be notified if any recommendations have not been implemented.

Traceability matrix

Use the table below as a quick reference to map the CIS AWS Foundations Benchmark recommendations to the sections above.

#

Section

Description

1.1

Answer security questions and complete contact details

Complete the contact details on the AWS account page

1.2

Answer security questions and complete contact details

Complete the security contact information on the AWS account page

1.3

Answer security questions and complete contact details

Answer the security questions on the AWS account page

1.4

Next steps

Use the Gruntwork Security Hub module to enable AWS Security Hub to ensure that no access key exists for the root user

1.5

Enable MFA for the root account

Manually configure MFA for the root user

1.6

Enable MFA for the root account

Use a Yubikey (or other hardware MFA) for the root user

1.7

Manual steps

Take manual steps to complete this recommendation

1.8-9

Create an IAM user password policy

Use the IAM password policy module

1.10

Configure authentication

Configure authentication using SAML or IAM

1.11

Steps for authentication via IAM users

Create IAM users with the iam-users module

1.12

Next steps

Use the Gruntwork Security Hub module to enable AWS Security Hub to ensure that there are no unused credentials

1.13

Next steps

Use the Gruntwork Security Hub module to enable AWS Security Hub to ensure that there are no extra access keys

1.14

Next steps

Use the Gruntwork Security Hub module to enable AWS Security Hub to ensure that there are no unused access keys

1.15

Steps for authentication via IAM users

Use IAM groups

1.16

Steps for authentication via IAM users

Use the Gruntwork modules to create best-practices groups and roles

1.17

Steps for authentication via IAM users

Use the iam-groups module to create a support group

1.18

Use IAM roles for EC2 instances

Use Gruntwork modules to ensure EC2 instances use roles for access

1.19

Cleanup Expired SSL/TLS certificates

Use Gruntwork modules to automatically remove expired certificates from IAM

1.20

S3 Buckets

Use the private-s3-bucket module

1.21

IAM Access Analyzer

Use Gruntwork modules to enable IAM Access Analyzer across regions

1.22

[account_structure]

Use the security AWS account as described in the Gruntwork production-grade AWS account structure

2.1.1-2.1.2

Configure storage

Use the private-s3-bucket module

2.2.1

Configure storage

Use Gruntwork modules to configure AWS EBS encryption

3.1-3.4

CloudTrail

Use the Gruntwork CloudTrail wrapper module

3.5

Enable AWS Config in all regions

Enable AWS Config for all regions

3.6-3.7

CloudTrail

Use the Gruntwork CloudTrail wrapper module

3.8

Enable key rotation for KMS keys

Use the KMS module

3.9

Create VPC flow logs

Use the Gruntwork CIS-compliant vpc service to provision VPCs with flow logs enabled

3.10-3.11

CloudTrail

Use the Gruntwork CloudTrail wrapper module

4.1-4.15

Configure monitoring

The CloudWatch Logs metrics filters wrapper module will satisfy each recommendation

5.1

Configure networking

Use the Gruntwork CIS-compliant vpc service to ensure there is no public remote access

5.2

Configure networking

Use the Gruntwork CIS-compliant vpc service for a secure network configuration

5.3

Configure networking

Use the cloud-nuke tool to remove all default security groups

5.4

Configure networking

Use the Gruntwork CIS-compliant vpc service to configure least-privilege routing by default