DEV Community

Cover image for How To Implement AWS SSB Controls in Terraform - Part 4
Anthony Wat for AWS Community Builders

Posted on • Updated on • Originally published at blog.avangards.io

How To Implement AWS SSB Controls in Terraform - Part 4

Introduction

The AWS Startup Security Baseline (SSB) defines a set of controls that comprise a lean but solid foundation for the security posture of your AWS accounts. By the end of part 3 of our blog series, we have covered all of the account controls and workload controls that are related to workload access and data protection. In this installment, we will review the remaining workload controls that focus on network security. Let's begin with WKLD.10, which is about keeping private resources secure within private subnets.

WKLD.10 – Deploy private resources in private subnets

The workload control WKLD.10 requires that all AWS resources that don't require direct internet access be deployed to a VPC private subnet.

Amazon VPC provides the means to isolate your network and keep traffic from and to workloads in your VPCs secure. For a typical workload, resources such as backend and database systems should be deployed to private subnets. If a private subnet allows outbound access to the internet, its route table should route the to a NAT gateway deployed in a public subnet. This setup is shown in the following diagram:

Typical setup for outbound internet access from a private subnet

To prevent accidental assignment of public IP addresses, this control recommends disabling the option for auto-assigning public IP address when a private subnet is created. In Terraform, this option corresponds to the map_public_ip_on_launch argument in the aws_subnet resource. Since the default value is false, omitting the argument in the resource definition will effectively create a private subnet. Here is an example:

# Dependent resources omitted for brevity

resource "aws_subnet" "private" {
  vpc_id     = aws_vpc.this.id
  cidr_block = "10.0.0.0/24"
  # Already false by default, no need to set explicitly
  # map_public_ip_on_launch = false
}
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ The AWS VPC Terraform module provides abstraction for private subnets to help you manage a secure VPC structure.

Similar option to auto-assign public IP exists for EC2 instances, so it should be disabled when an EC2 instance is provisioned. This setting corresponds to the associate_public_ip_address argument in the aws_instance resource, which not set thus implying false by default. So you can omit the argument to create an EC2 instance as per the following example:

# Dependent resources omitted for brevity

resource "aws_instance" "web" {
  ami                  = data.aws_ami.ubuntu.id
  iam_instance_profile = aws_iam_instance_profile.ssm.name
  instance_type        = "t3.micro"
  subnet_id            = data.aws_subnet.private.id
  # Already false by default, no need to set explicitly
  # associate_public_ip_address = false
}
Enter fullscreen mode Exit fullscreen mode

WKLD.11 – Use security groups to restrict access

The workload control WKLD.11 requests the use of security groups to restrict network access.

Security groups serve as virtual stateful firewalls to control inbound and outbound traffic to the resource it is associated with. A typical baseline would be to allow all outbound traffic, and allow inbound traffic only to trusted sources on specific service ports and protocols. You can further restrict the outbound traffic for more isolation.

To demonstrate how to properly define security groups in Terraform, let's consider this hypothetical LAMP application:

Infrastructure architecture for the example workload

Here are the required inbound rules:

Security group Purpose Source Protocol and port
ALB HTTPS access from the internet Internet (0.0.0.0/0) TCP 443 (HTTPS)
Web server HTTPS access from the ALB Security group of the ALB TCP 443 (HTTPS)
MySQL Instance MySQL access from web server Security group of the web server TCP 3306 (MySQL)

And here are the required outbound rules - note that we assume that the RDS for MySQL DB instance does not require internet access:

Security group Purpose Destination Protocol and port
ALB Internet access Internet (0.0.0.0/0) All
Web Server Internet access Internet (0.0.0.0/0) All

The security groups can be defined in Terraform as follows:

# ALB security group
resource "aws_security_group" "alb" {
  name        = "app-prod-sg-use1-alb"
  description = "Security group for the ALB"
  vpc_id      = aws_vpc.this.id
}

resource "aws_vpc_security_group_ingress_rule" "alb_https_all" {
  security_group_id = aws_security_group.alb.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = 443
  ip_protocol       = "tcp"
  to_port           = 443
}

resource "aws_vpc_security_group_ingress_rule" "alb_all" {
  security_group_id = aws_security_group.alb.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = -1
  ip_protocol       = -1
  to_port           = -1
}

# Web server security group
resource "aws_security_group" "web" {
  name        = "app-prod-sg-use1-web"
  description = "Security group for the web server"
  vpc_id      = aws_vpc.this.id
}

resource "aws_vpc_security_group_ingress_rule" "web_http_alb" {
  security_group_id            = aws_security_group.web.id
  referenced_security_group_id = aws_security_group.alb.id
  from_port                    = 443
  ip_protocol                  = "tcp"
  to_port                      = 443
}

resource "aws_vpc_security_group_ingress_rule" "web_all" {
  security_group_id = aws_security_group.web.id
  cidr_ipv4         = "0.0.0.0/0"
  from_port         = -1
  ip_protocol       = -1
  to_port           = -1
}

# MySQL instance security group
resource "aws_security_group" "db" {
  name        = "app-prod-sg-use1-db"
  description = "Security group for the RDS for MySQL DB instance"
  vpc_id      = aws_vpc.this.id
}

resource "aws_vpc_security_group_ingress_rule" "db_mysql_web" {
  security_group_id            = aws_security_group.db.id
  referenced_security_group_id = aws_security_group.web.id
  from_port                    = 3306
  ip_protocol                  = "tcp"
  to_port                      = 3306
}

# Associate these security groups to the resources accordingly
Enter fullscreen mode Exit fullscreen mode

πŸ’‘ For more tips on managing security groups in Terraform, check out my blog posts 5 Tips to Efficiently Manage AWS Security Groups Using Terraform and Building a Dynamic AWS Security Group Solution With CSV in Terraform.

WKLD.12 – Use VPC endpoints to access services

The workload control WKLD.12 recommends the use of VPC endpoints to privately access AWS and other services without traversing the internet.

Some industry and security compliance standards require that networks be isolated without outbound internet access. To facilitate access to AWS and external services without traversing the internet, you can use VPC endpoints.

There are two types of VPC endpoints - gateway endpoints and interface endpoints. Gateway endpoints are available only for Amazon S3 and Amazon DynamoDB, but are otherwise free to use. Interface endpoints support numerous AWS services and external services that are exposed and shared as endpoint services. Both endpoint types support resource policies, however only interface endpoints support security groups since they are deployed as ENIs.

Building upon the scenario above, let's assume that the web application needs to integrate with S3. We can provision a gateway endpoint in Terraform to enable private access as follows:

resource "aws_vpc_endpoint" "s3" {
  vpc_id          = aws_vpc.this.id
  route_table_ids = [aws_route_table.web.id]
  service_name    = "com.amazonaws.us-east-1.s3"
  # Defining a permissive resource policy for illustation.
  # You can finetune it for more security.
  policy = <<-EOT
  {
    "Version": "2008-10-17",
    "Statement": [
      {
        "Action": "*",
        "Effect": "Allow",
        "Resource": "*",
        "Principal": "*"
      }
    ]
  }
  EOT
}
Enter fullscreen mode Exit fullscreen mode

Let's now assume that we need to use SSM Session Manager to connect to the web server EC2 instance (see WKLD.06 in part 3 of the blog series for details). We can provision a set of interface endpoints in Terraform as follows:

locals {
  ssm_service_names = [
    "com.amazonaws.us-east-1.ec2messages",
    "com.amazonaws.us-east-1.ssm",
    "com.amazonaws.us-east-1.ssmmessages"
  ]
}

resource "aws_security_group" "vpce" {
  name        = "app-prod-sg-use1-vpce"
  description = "Security group for interface endpoints to AWS services"
  vpc_id      = aws_vpc.this.id
}

resource "aws_vpc_security_group_ingress_rule" "vpce_https_vpc" {
  security_group_id = aws_security_group.vpce.id
  cidr_ipv4         = aws_vpc.this.cidr_block
  from_port         = 443
  ip_protocol       = "tcp"
  to_port           = 443
}

# Note: Requires enableDnsSupport and enableDnsHostnames set to true for the VPC
resource "aws_vpc_endpoint" "ssm" {
  for_each            = toset(local.ssm_service_names)
  vpc_id              = aws_vpc.this.id
  service_name        = each.key
  vpc_endpoint_type   = "Interface"
  private_dns_enabled = true
  security_group_ids  = [aws_security_group.vpce.id]
  # Alternatively you can create a subnet for VPC endpoints
  subnet_ids          = [aws_subnet.web.id]
}
Enter fullscreen mode Exit fullscreen mode

WKLD.13 – Require HTTPS for all public web endpoints

The workload control WKLD.13 mandates the use of HTTPS for all public web endpoints.

The HTTPS protocol provides the level of web security that is considered the norm nowadays, so much so that Google defines the use of HTTPS a ranking factor for their search results and mark websites using HTTP as "not secure" in Chrome. With the prevalence the zero trust and encryption everywhere security approaches, TLS encryption between the load balancers/reverse proxies and backend systems, as well as end-to-end encryption, are also strongly recommended.

AWS Certificate Manager (ACM) integrates with AWS endpoint services including Elastic Load Balancing and Amazon CloudFront. You can either issue a certificate for a domain that you own, or import a certificate that is generated with a third-party provider. For a better experience, you can use the AWS Certificate Manager (ACM) Terraform module to create and validate ACM certificates - refer to the examples for usages. Meanwhile, to import a certificate for which you have the necessary PEM-formatted key and certificate files, you can use the aws_acm_certificate resource as follows:

resource "aws_acm_certificate" "alb" {
  private_key       = "${file("private.key")}"
  certificate_body  = "${file("cert.cer")}"
  certificate_chain = "${file("chain.cer")}"
}
Enter fullscreen mode Exit fullscreen mode

Continuing with the scenario above, here is the Terraform configuration that provisions an ALB with an HTTPS listener:

# Dependent resources omitted for brevity

resource "aws_lb" "this" {
  name               = "app-prod-alb-use1"
  internal           = false
  load_balancer_type = "application"
  security_groups    = [aws_security_group.alb.id]
  subnets            = [for subnet in data.aws_subnet.public : subnet.id]
}

resource "aws_lb_target_group" "web" {
  name             = "web"
  port             = 443
  protocol         = "HTTPS"
  target_type      = "ip"
  vpc_id           = data.aws_vpc.this.id
  health_check {
    enabled  = true
    matcher  = "200-299"
    path     = "/"
    protocol = "HTTPS"
  }
  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_lb_target_group_attachment" "web" {
  target_group_arn = aws_lb_target_group.web.arn
  target_id        = aws_instance.web.id
  port             = 443
}

resource "aws_lb_listener" "this" {
  load_balancer_arn = aws_lb.this.arn
  protocol          = "HTTPS"
  port              = 443
  ssl_policy        = "ELBSecurityPolicy-TLS13-1-2-2021-06"
  certificate_arn   = aws_acm_certificate.alb.arn
  default_action {
    type = "forward"
    target_group_arn = aws_lb_target_group.web.arn
  }
}
Enter fullscreen mode Exit fullscreen mode

For CloudFront, you can consider using the AWS CloudFront Terraform module. The complete example demonstrates how to configure a CloudFront distribute with HTTPS. Under the cover, it configures the custom origin and view certificate blocks in the aws_cloudfront_distribution resource.

WKLD.14 – Use edge-protection services for public endpoints

The workload control WKLD.14 recommends using an edge-protection service to expose a public endpoint instead of directly through the underlying workload such as an EC2 instance.

Such endpoint services include Elastic Load Balancing and Amazon CloudFront as mentioned, as well as Amazon API Gateway and AWS Amplify Hosting. To provide additional endpoint protection, you can integrate services such as AWS WAF, AWS Network Firewall, and Gateway Load Balancer with a virtual firewall appliance.

Setting up AWS Network Firewall and Gateway Load Balancer can be complex especially in a centralized architecture, so we will save them for a future blog series on network security (hint hint). Resuming the sample scenario in this blog post, let's focus on configuring an AWS WAF web ACL and associating it with the ALB. Here is the Terraform configuration that creates a web ACL with the Core rule set (CRS) managed rule group:

resource "aws_wafv2_web_acl" "regional" {
  name  = "app-prod-webacl-use1-regional"
  scope = "REGIONAL" # Use GLOBAL for web ACL meant for CloudFront
  default_action {
    allow {}
  }
  visibility_config {
    cloudwatch_metrics_enabled = true
    metric_name                = "appapp-prod-webacl-use1-regional"
    sampled_requests_enabled   = true
  }
  rule {
    name     = "AWS-AWSManagedRulesCommonRuleSet"
    priority = 0
    override_action {
      none {}
    }
    statement {
      managed_rule_group_statement {
      name        = "AWSManagedRulesCommonRuleSet"
      vendor_name = "AWS"
      # Use the rule_action_override block to override rule action
    }
    visibility_config {
      cloudwatch_metrics_enabled = true
      metric_name                = "AWS-AWSManagedRulesCommonRuleSet"
      sampled_requests_enabled   = true
    }
  }
}

# Set up logging for analysis, an important part of WAF implementation
resource "aws_cloudwatch_log_group" "waf_regional" {
  name              = "aws-waf-logs-app-prod-webacl-use1-regional"
  retention_in_days = 90
}

resource "aws_wafv2_web_acl_logging_configuration" "regional" {
  log_destination_configs = [aws_cloudwatch_log_group.waf_regional.arn]
  resource_arn            = aws_wafv2_web_acl.regional.arn
  redacted_fields {
    single_header {
      name = "authorization"
    }
  }
}

# Associate the web ACL with the ALB to enable WAF protection
resource "aws_wafv2_web_acl_association" "regional" {
  resource_arn = aws_lb.this.arn
  web_acl_arn  = aws_wafv2_web_acl.regional.arn
}
Enter fullscreen mode Exit fullscreen mode

The AWS whitepaper Guidelines for Implementing AWS WAF does a great job at explaining how to plan, implement, test, and roll out AWS WAF, so be sure to review it before implementation.

WKLD.15 – Use templates to deploy security controls

The final workload control, WKLD.15, recommends using infrastructure-as-code (IaC) and CI/CD pipelines to deploy security controls alongside your AWS resources.

If you are following this blog series, you should already know the benefits of using Terraform to define and deploy your AWS resources and configuration. Other IaC solutions such as AWS CloudFormation, AWS CDK, and Pulumi work the same way but differ in the programming or configuration language.

As you design your IaC templates, consider separating the SSB account controls into its own "stack" while incorporating the workload controls to the "workload stack". This allows a more flexible deployment model and practicing DevOps within application development teams.

Having CI/CD pipelines also helps you build and deploy configuration as soon as changes are made in the code repository. Your CI/CD pipeline can be customized according to your organization's needs, such as validations and approval gates. AWS CodePipeline is a decent choice if you prefer to stay in the AWS ecosystem, or you could also use a third-party solution such as GitHub Actions.

Since you will be providing AWS credentials to the CI/CD pipelines, it is crucial that they are set in a secure manner. For Terraform, the AWS provider documentation explains the different ways of providing AWS credentials to Terraform. You also should use a backend such as the S3 backend to securely store and share your states remotely.

Summary

Congratulations, we made it through the entire set of SSB controls! Over the course of this How to implement the AWS Startup Security Baseline (SSB) using Terraform blog series, we have reviewed every SSB control in detail and see how they can be implemented using Terraform. Keep in mind that these controls are still considered foundational, so you should evolve your security practice as your AWS usage scales and evolves. Frameworks such as the AWS Security Maturity Model can help you define new target states and implement them iteratively.

I thoroughly enjoyed writing this month-long blog series, especially as a new AWS Community Builder. I hope that you also learned something new and interesting. If you like my contents, please be sure to check out other posts in the Avangards Blog. Have a great one!

Top comments (0)