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:
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
}
💡 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
}
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:
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
💡 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
}
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]
}
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")}"
}
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
}
}
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
}
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)