Skip to content

AWS Cloud Misconfiguration

Name Id Description
AWS Athena Encryption Off aws-athena-encryption-off Ensure Athena Workgroup should enforce configuration to prevent client disabling encryption
AWS Cloudtrail All Regions aws-cloudtrail-all-regions Ensure CloudTrail is enabled in all Regions
AWS Cloudtrail Validation Off aws-cloudtrail-validation-off Ensure CloudTrail log file validation is enabled
AWS Cloudwatch Log Retention aws-cloudwatch-log-retention Ensure cloudwatch log groups specify retention days
AWS Database Backup Off aws-db-backup-off Ensure that database backup is enabled
AWS Database No Version Upgrade aws-db-no-version-upgrade Ensured that database auto-upgrade is enabled
AWS EC2 Public IP aws-ec2-public-ip EC2 instance should not have public IP.
AWS ECR Scanning Off aws-ecr-scanning-off Ensure ECR image scanning on push is enabled
AWS ECR Tags Mutable aws-ecr-tags-mutable Ensure ECR Image Tags are immutable
AWS ECS Container Insights Off aws-ecs-container-insights-off Ensure container insights are enabled on ECS cluster
AWS IAM Password Policy aws-iam-password-policy Ensure that IAM password policy has sufficient complexity based on industry best practices
AWS IAM Policy Lax Full Admin aws-iam-policy-lax-full-admin Ensure IAM policies that allow full "*-*" administrative privileges are not created
AWS IAM Policy On Users aws-iam-policy-on-users Ensure IAM policies are attached only to groups or roles
AWS IAM Wildcard Actions aws-iam-wildcard-actions Ensure no IAM policies documents allow "*" as a statement's actions
AWS KMS Key Rotation aws-kms-key-rotation Ensure rotation for customer created CMKs is enabled
AWS Load Balancer Allow Invalid Headers aws-lb-allow-invalid-headers Ensure that the load balancer drops invalid HTTP headers
AWS Legacy Instance Meta aws-legacy-instance-meta Ensure Instance Metadata Service Version 1 is not enabled
AWS Network Https Off aws-network-https-off Ensure the the networking resource enforces the use of HTTPS
AWS Network Insecure TLS aws-network-insecure-tls Ensure that load balancer is using TLS 1.2
AWS Network Public RDP aws-network-public-rdp Ensure no security groups allow ingress from 0.0.0.0:0 to port 3389 (RDP)
AWS Network Public SSH aws-network-public-ssh Ensure that the resource or security group allow ingress from 0.0.0.0:0 to port 22 (SSH)
AWS S3 Logging Off aws-resource-logging-off Ensure that the resource has some form of audit logging enabled to help with forensics
AWS Resource Outside VPC aws-resource-outside-vpc Ensure that the resource is configured inside a VPC
AWS Resource Public Access aws-resource-public-access Ensure that all data stored in the managed service is not publicly accessible
AWS Resource Public Policy aws-resource-public-policy Ensure that the resource policy is not set to public
AWS Resource Unencrypted At Rest aws-resource-unencrypted-at-rest Ensure that all data stored in the managed service is securely encrypted at rest
AWS Resource Unencrypted In Transit aws-resource-unencrypted-in-transit Ensure that data going to and from the managed service is securely encrypted at transit
AWS S3 Public Access aws-s3-public-access Ensure the S3 bucket does not allow Read or Write permissions to anyone on the Internet
AWS S3 Public Policy aws-s3-public-policy Ensure S3 bucket does not allow an action with any Principal (i.e. anyone on the Internet)
AWS S3 Unencrypted At Rest aws-s3-unencrypted-at-rest Ensure all data stored in the S3 bucket is securely encrypted at rest
AWS VPC Assign Public Ip aws-vpc-assign-public-ip Ensure VPC subnets do not assign public IP by default
AWS VPC Endpoint Auto Accept aws-vpc-endpoint-auto-accept Ensure that VPC Endpoint Service is configured for Manual Acceptance

Deprecated AWS rules

The following rules should not be reported by BoostSecurity anymore as they have been replaced by other, more modern rules. Those are kept here for historical reference on old scan runs.

Resources Unencrypted at Rest

Most AWS resources that store data (data at rest) have the option to turn on disk-level encryption. Disk-level encryption protects data by encrypting & decrypting data as it's written and read from disk. Depending on how you configure the encryption keys, enabling disk-level encryption can have different effects.

Using AWS Managed Keys

By default, most disk-level encryption options in AWS use AWS Managed Keys (the keys are called Customer Master Keys, or CMKs, and can be viewed in the AWS Key Management Service, or KMS). When using AWS Managed CMKs, encryption happens transparently to the users and there are no keys you need to manage or rotate. Enabling disk-level encryption this way doesn't provide a lot of practical security (realistically, it protects against the threat of an attacker trying to steal your data from within an AWS data center), but it does fulfill an almost universal requirement for security compliance.

Enabling disk-level encryption with AWS Managed Keys is simple to do, fast to implement, and carries no maintenance costs. Although the security benefits are limited, in almost all cases, enabling compliance concerns is worthwhile.

Using Customer Managed Keys

The other option is to use a Customer Managed Key. A Customer Managed CMK does require management and requires key rotation to be configured. Using a Customer Managed CMK comes with the advantage that you can attach an IAM policy to it. A policy attached to a Customer Managed CMK can be used to restrict access to resources cryptographically. In this case, the Customer Managed CMK does provide practical security by enabling access control.

Enabling disk-level encryption with Customer Managed Keys is more difficult to do, slower to implement, and does require maintenance. There are practical security benefits for restricting access, but the use case should be considered before implementation.

unencrypted-block-device

In a launch configuration for auto-scaling, the added block device does not have disk-level encryption enabled.

Insecure Example

resource "aws_launch_configuration" "as_conf" {
  name_prefix   = "terraform-lc-example-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"

  lifecycle {
    create_before_destroy = true
  }

  # Missing root_block_device block defining encryption
}

Secure Example

resource "aws_launch_configuration" "as_conf" {
  name_prefix   = "terraform-lc-example-"
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"

  lifecycle {
    create_before_destroy = true
  }

  root_block_device {
    volume_type           = "gp2"
    volume_size           = 100
    delete_on_termination = true
    encrypted = true
  }
}

More Information

Adding the encrypted = true property to the root_block_device Terraform block will enable disk-level encryption with an AWS Managed Key. If you are missing a root_block_device Terraform block, you will need to add one.

Unencrypted-sns-queue

An SNS topic does not have disk-level encryption enabled. When disk-level encryption is enabled, the body of a message in a topic will be encrypted, but the topic and message metadata will not be. (see more)

Insecure Example

resource "aws_sns_topic" "sns_asg" {
  name = "MyTopic"
}

Secure Example

resource "aws_sns_topic" "sns_asg" {
    name = "MyTopic"
    kms_master_key_id = "alias/aws/sns"
}

More Information

Adding the kms_master_key_id = "alias/aws/sns" property will enable disk-level encryption with the "alias/aws/sns" AWS Managed Key. This is not something that needs to be set up, AWS will handle the key if this exact line of code is used. (You can choose to set the kms_master_key_id to your own Customer Managed Key by changing the value, but this is more involved)

unencrypted-sqs-queue

An SQS queue does not have disk-level encryption enabled. When disk-level encryption is enabled, the body of a message in an SQS queue will be encrypted, but the queue and message metadata will not be. (see more)

Insecure Example

resource "aws_sqs_queue" "terraform_queue" {
  name                      = "terraform-example-queue"
  delay_seconds             = 90
  max_message_size          = 2048
  message_retention_seconds = 86400
  receive_wait_time_seconds = 10
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.terraform_queue_deadletter.arn
    maxReceiveCount     = 4
  })
}

Secure Example

resource "aws_sqs_queue" "terraform_queue" {
  name                      = "terraform-example-queue"
  kms_master_key_id                 = "alias/aws/sqs"       
  kms_data_key_reuse_period_seconds = 300
  delay_seconds             = 90
  max_message_size          = 2048
  message_retention_seconds = 86400
  receive_wait_time_seconds = 10
  redrive_policy = jsonencode({
    deadLetterTargetArn = aws_sqs_queue.terraform_queue_deadletter.arn
    maxReceiveCount     = 4
  })
}

More Information

Adding the kms_master_key_id = "alias/aws/sqs" property will enable disk-level encryption with the "alias/aws/sqs" AWS Managed Key. This is not something that needs to be set up; AWS manages this key automatically. (You can choose to set the kms_master_key_id to your own Customer Managed Key by changing the value, but this is more involved)

Adding the kms_data_key_reuse_period_seconds property specifies the re-use periods of the key in seconds.

unencrypted-kinesis-queue

A Kinesis queue does not have disk-level encryption enabled. When disk-level encryption is enabled, the body of a message in an SQS queue will be encrypted, but the queue and message metadata will not be. (see more)

Insecure Example

resource "aws_kinesis_stream" "test_stream" {
  name             = "terraform-kinesis-test"
  shard_count      = 1
}

Secure Example

resource "aws_kinesis_stream" "test_stream" {
  name             = "terraform-kinesis-test"
  shard_count      = 1
  encryption_type = KMS
  kms_key_id = "alias/aws/kinesis"
}

More Information

Adding the kms_key_id = "alias/aws/kinesis" property will enable disk-level encryption with the "alias/aws/kinesis" AWS Managed Key. This is not something that needs to be set up, AWS will manage this key automatically. (You can choose to set the kms_master_key_id to your own Customer Managed Key by changing the value, but this is more involved)

unencrypted-data-managed-kafka

AWS Managed Streaming for Kafka (MSK) cluster has been configured without encryption at rest and/or encryption in transit.

Insecure Example

resource "aws_msk_cluster" "example" {
  cluster_name           = "example"
  kafka_version          = "2.4.1"
  number_of_broker_nodes = 3

  broker_node_group_info {
    instance_type   = "kafka.m5.large"
    ebs_volume_size = 1000
    client_subnets = [
      aws_subnet.subnet_az1.id,
      aws_subnet.subnet_az2.id,
      aws_subnet.subnet_az3.id,
    ]
    security_groups = [aws_security_group.sg.id]
  } 
}

Secure Example

resource "aws_msk_cluster" "example" {
  cluster_name           = "example"
  kafka_version          = "2.4.1"
  number_of_broker_nodes = 3

  broker_node_group_info {
    instance_type   = "kafka.m5.large"
    ebs_volume_size = 1000
    client_subnets = [
      aws_subnet.subnet_az1.id,
      aws_subnet.subnet_az2.id,
      aws_subnet.subnet_az3.id,
    ]
    security_groups = [aws_security_group.sg.id]
  }

  encryption_info {                # Just specifying this block enables encryption at-rest
    encryption_in_transit {
        client_broker = "TLS",     # Enable encryption in-transit for clients
        in_cluster = true          # Enable encryption in-transit between brokers within the cluster
    }
  }
}

More Information

AWS Managed Streaming for Kafka (MSK) provides two encryption locations:

  • Data at rest
  • Data in transit

AWS MSK will always encrypt data at rest. If a key is not provided, MSK will provide a key transparently.

However, for data-in-transit between the Kafka broker and the Kafka client (or alternatively, the inter-cluster communication between the Kafka brokers) the feature has to be explicitly enabled.

unencrypted-s3-bucket

An S3 bucket has been defined without server-side encryption at-rest enabled.

Insecure Example

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-tf-test-bucket"
  acl    = "private"
}

Secure Example

# The simplest option is to use the default aws/s3 AWS KMS master key

resource "aws_s3_bucket" "my_bucket" {
  bucket = "my-tf-test-bucket"
  acl    = "private"

  # provides encryption at-rest without any maintenance required
  server_side_encryption_configuration {
      rule {
          apply_server_side_encryption_by_default {
              sse_algorithm = "AES256"
          }
      }
  }
}
# To specify which AWS KMS master key to use use the kms_master_key_id property
# Access to the s3 bucket will require access to the KMS master key specified

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-test-bucket"
  acl    = "private"

  tags = {
    Name        = "My bucket"
    Environment = "Dev"
  }

  server_side_encryption_configuration {
    rule {
      apply_server_side_encryption_by_default {
        kms_master_key_id = aws_kms_key.mykey.arn
        sse_algorithm     = "aws:kms"
      }
    }
  }
}

More Information

When sse_algorithm is set to "AES256" and kms_master_key_id isn't specified, AWS will enable disk-level encryption with the "alias/aws/s3" AWS Managed Key. This is not something that needs to be set up, AWS will manage this key automatically. (You can choose to set the kms_master_key_id to your own Customer Managed Key by changing the value, but this is more involved)

Resources Unencrypted in Transit

Many AWS resources that transfer data over the network provide the option to protect data by encrypting that traffic while it's "in transit" (note this is different than encryption for stored data "at rest"). The Transport Layer Security (TLS) protocol is used to encrypt data in transit and provides the S in HTTPS when partnered with the HTTP protocol. Encrypting traffic in transit protects sensitive data (such as passwords or personal information) from being intercepted or stolen by attackers as the data traverses a local network or the public internet.

TLS should always be enabled when users or public clients are connecting to public-facing resources such as load balancers or the CloudFront CDN. Using the correct versions of TLS is also important, as old versions have known security issues.

Internally, within a VPC, encryption in transit can provide defense-in-depth to help restrict the actions of an attacker that gains access to your private network.

use-of-plain-http

A load balancer has been configured to use a network protocol without encryption. This connection will not be secure to safely transmit sensitive information.

Insecure Example

resource "aws_alb_listener" "api_http" {
  load_balancer_arn = aws_alb.api.arn
  port              = "80"
  protocol          = "HTTP"

  default_action {
    target_group_arn = aws_alb_target_group.api.arn
    type             = "forward"
  }
}

Secure Example

resource "aws_alb_listener" "api_http" {
  load_balancer_arn = aws_alb.api.arn
  port              = "80"
  protocol          = "HTTPS"     # HTTPS or TLS are valid options for encrypted traffic

  default_action {
    target_group_arn = aws_alb_target_group.api.arn
    type             = "forward"
  }
}

More Information

outdated-ssl-in-load-balancer

A load balancer has been configured with an insecure, out-of-date version of TLS. This connection will not be secure to safely transmit sensitive information.

Insecure Example

resource "aws_alb_listener" "api_https" {
  load_balancer_arn = aws_alb.api.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-2016-08"     # An out of date TLS Policy
  certificate_arn   = aws_acm_certificate.api_adventar_org.arn

  default_action {
    target_group_arn = aws_alb_target_group.api.arn
    type             = "forward"
  }
}

Secure Example

resource "aws_alb_listener" "api_https" {
  load_balancer_arn = aws_alb.api.arn
  port              = "443"
  protocol          = "HTTPS"
  ssl_policy        = "ELBSecurityPolicy-TLS"     # Using this policy disables certain version of TLS that are not secure
  certificate_arn   = aws_acm_certificate.api_adventar_org.arn

  default_action {
    target_group_arn = aws_alb_target_group.api.arn
    type             = "forward"
  }
}

More Information

Secure Socket Layer (SSL) is an older version of TLS, so sometimes "SSL" will be used in place of "TLS" as seen in the ssl_policy property name.

unencrypted-cloudfront

A CloudFront distribution has been configured without encryption in transit. Enabling HTTPS for CloudFront protects data in transit being hosted by the CloudFront CDN.

Insecure Example

resource "aws_cloudfront_distribution" "www_mydomain_org" {
  enabled = true
  aliases = ["www.mydomain.org"]

  origin {
    origin_id   = local.www_mydomain_org_origin_id
    domain_name = aws_s3_bucket.www_mydomain.website_endpoint

    custom_origin_config {
      origin_protocol_policy = "http-only"
      http_port              = "80"
      https_port             = "443"
      origin_ssl_protocols   = ["TLSv1"]
    }
  }

  default_cache_behavior {
    target_origin_id       = local.www_mydomain_org_origin_id
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    viewer_protocol_policy = "allow-all"     # This allows requests over HTTP

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.www_mydomain_org.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2018"
  }
}

Secure Example

resource "aws_cloudfront_distribution" "www_mydomain_org" {
  enabled = true
  aliases = ["www.mydomain.org"]

  origin {
    origin_id   = local.www_mydomain_org_origin_id
    domain_name = aws_s3_bucket.www_mydomain.website_endpoint

    custom_origin_config {
      origin_protocol_policy = "http-only"
      http_port              = "80"
      https_port             = "443"
      origin_ssl_protocols   = ["TLSv1"]
    }
  }

  default_cache_behavior {
    target_origin_id       = local.www_mydomain_org_origin_id
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    viewer_protocol_policy = "https-only"   # "redirect-to-https" would also work

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.www_mydomain_org.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2018"
  }
}

More Information

outdated-ssl-cloudfront

The SSL setup in the CloudFront portion of your Terraform files is uses an outdated SSL policy. Update the minimum_protocol_version to TLSv1.2_2018 or later.

Insecure Example

resource "aws_cloudfront_distribution" "www_mydomain_org" {
  enabled = true
  aliases = ["www.mydomain.org"]

  origin {
    origin_id   = local.www_mydomain_org_origin_id
    domain_name = aws_s3_bucket.www_mydomain.website_endpoint

    custom_origin_config {
      origin_protocol_policy = "http-only"
      http_port              = "80"
      https_port             = "443"
      origin_ssl_protocols   = ["TLSv1"]
    }
  }

  default_cache_behavior {
    target_origin_id       = local.www_mydomain_org_origin_id
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    viewer_protocol_policy = "https-only" 

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.www_mydomain_org.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1"
  }
}

Secure Example

resource "aws_cloudfront_distribution" "www_mydomain_org" {
  enabled = true
  aliases = ["www.mydomain.org"]

  origin {
    origin_id   = local.www_mydomain_org_origin_id
    domain_name = aws_s3_bucket.www_mydomain.website_endpoint

    custom_origin_config {
      origin_protocol_policy = "http-only"
      http_port              = "80"
      https_port             = "443"
      origin_ssl_protocols   = ["TLSv1"]
    }
  }

  default_cache_behavior {
    target_origin_id       = local.www_mydomain_org_origin_id
    allowed_methods        = ["GET", "HEAD"]
    cached_methods         = ["GET", "HEAD"]
    viewer_protocol_policy = "https-only" 

    forwarded_values {
      query_string = true

      cookies {
        forward = "none"
      }
    }
  }

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  viewer_certificate {
    acm_certificate_arn      = aws_acm_certificate.www_mydomain_org.arn
    ssl_support_method       = "sni-only"
    minimum_protocol_version = "TLSv1.2_2018"        # Using this or a later protocol is security best practice
  }
}

More Information

Unrestricted Public Access

Many AWS resources have the option to define whether access should be restricted to private traffic within a VPC or be exposed to the public internet. Publicly exposing resources should be a deliberate decision for services that need to be reached by web clients or end users. Too often, these settings are incorrectly set, and sensitive data is inadvertently made publicly available for attackers to pillage.

For resources that legitimately need to be publicly accessible (ALBs, publicly available servers, S3 buckets hosting static content for public sites), findings from these rules can be marked as false positives.

launch-configuration-public-ip-address

A public IP address has been associated with each instance in a launch configuration for an Auto Scaling group. These instances are now publicly exposed to the world.

Insecure Example

resource "aws_launch_configuration" "as_conf" {
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  associate_public_ip_address = true     # Provides public IP address to instances, exposing them to the world

  lifecycle {
    create_before_destroy = true
  }

  root_block_device {
    encrypted = true
  }
}

Secure Example

resource "aws_launch_configuration" "as_conf" {
  image_id      = data.aws_ami.ubuntu.id
  instance_type = "t2.micro"
  associate_public_ip_address = false     # This is the default option, so omitting this line would also set the option to false

  lifecycle {
    create_before_destroy = true
  }

  root_block_device {
    encrypted = true
  }
}

More Information

By default, a launch configuration configured a private IP address within the VPC, but the associate_public_ip_address option provides the ability to override the safe default to configure unique, public IP addresses. This option should only be enabled if the application running on the instances is designed to be directly and publicly available, and there are other controls to protect against abuse.

s3-public-access

An S3 bucket has an ACL set to public-read. This bucket is now publicly exposed to the world.

Insecure Example

resource "aws_s3_bucket" "b" {
  bucket = "my-bucket"
  acl    = "public-read"     # This canned ACL exposes the bucket to the world
}

Secure Example

resource "aws_s3_bucket" "b" {
  bucket = "my-bucket"
  acl    = "private"
  }
}

More Information

AWS provides predefined permission sets called Canned ACLs.

If the bucket is intended to be publicly readable (when used to host static content or documents for a public website, for example), then the canned ACL (Access Control List) public-read is correct.

If the bucket contains any sensitive or private data, then the canned ACL should be restricted to private.

If the canned ACLs don't provide the specific permissions needed, then a more complex S3 IAM policy may be required.

load-balancer-public-access

A Load Balancer is routes traffic to a public IP address. While this may be intentional, it means that the application behind the load balancer is publicly exposed to the world.

Insecure Example

# Assuming the service behind the load balancer is a private service
resource "aws_lb" "test" {
  name               = "test-lb-tf"
  internal           = false                              # The load balancer has unintentionally been exposed
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = [aws_subnet.public.*.id]           # Here, we can see that the intention is a public subnet

  enable_deletion_protection = true

  access_logs {
    bucket  = aws_s3_bucket.lb_logs.bucket
    prefix  = "test-lb"
    enabled = true
  }

  tags = {
    Environment = "production"
  }
}

Secure Example

# Assuming the service behind the load balancer is a private service
resource "aws_lb" "test" {
  name               = "test-lb-tf"
  internal           = true                               # This load balancer is now private
  load_balancer_type = "application"
  security_groups    = [aws_security_group.lb_sg.id]
  subnets            = [aws_subnet.public.*.id]           # Here, we can see that the intention is a public subnet

  enable_deletion_protection = true

  access_logs {
    bucket  = aws_s3_bucket.lb_logs.bucket
    prefix  = "test-lb"
    enabled = true
  }

  tags = {
    Environment = "production"
  }
}

More Information

Load Balancers are the front end from which users can access running services. If this is indeed a service that is intentionally public, then this alert can be ignored. However, if the service is meant to be an internal service, then an internal load balancer should be used.

egress-security-group-public-access

A security group has been configured to allow outbound traffic (egress) to any destination IP address. In the case where an attacker compromises a service running in that security group, they will be able call out the rest of the internet unrestricted, which makes it significantly easier to exfiltrate data or download malicious code.

Ideally, egress rules should be restricted to required services when possible.

Insecure Example

resource "aws_security_group_rule" "egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]     # No restrictions on IP addresses
  security_group_id = "${aws_security_group.default.id}"
}

Secure Example

resource "aws_security_group_rule" "egress" {
  type              = "egress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["203.0.113.0/24"]     # Limiting the egress CIDR block to only the destinations that you need is good practice
  security_group_id = "${aws_security_group.default.id}"
}

More Information

When defining security group rules where a service requires some outbound access, a common shortcut is allowing outbound traffic to any destination IP address. This is typically done to avoid having to set constraints at development time. If an attacker can ever compromise a service running in that security group, there is nothing stopping the attacker from reaching the rest of the internet from that one instance. If, instead, the outbound access was limited to only those destination IP addresses that the application needs, then that would limit the attacker's capabilities should they be able to compromise the service.

If the application is designed to issue requests across the internet, the service should be placed in a tightly controlled VPC for extra security measures.

ingress-security-group-public-access

A security group has been configured to allow inbound traffic (ingress) from any IP address. Resources in this security group are potentially exposed to the public internet.

Ingress rules should be restricted to required services.

Insecure Example

resource "aws_security_group_rule" "ingress" {
  type              = "ingress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["0.0.0.0/0"]     # No restrictions on IP addresses
  security_group_id = "${aws_security_group.default.id}"
}

Secure Example

resource "aws_security_group_rule" "ingress" {
  type              = "ingress"
  from_port         = 0
  to_port           = 0
  protocol          = "-1"
  cidr_blocks       = ["203.0.113.0/24"]     # Limiting the egress CIDR block to only the source that you need is good practice
  security_group_id = "${aws_security_group.default.id}"
}

More information

inline-egress-security-group-public-access

A security group has been configured to allow outbound traffic (egress) to any destination IP address. In the case where an attacker compromises a service running in that security group, they will be able call out to the rest of the internet unrestricted, which makes it significantly easier to exfiltrate data or download malicious code.

Ideally, egress rules should be restricted to required services when possible.

Insecure Example

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]     # No restrictions on IP addresses
  }
}

Secure Example

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = [aws_vpc.main.cidr_block]
  }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["203.0.113.0/24"]     # Limiting the egress CIDR block to only the destinations that you need is good practice
  }
}

More Information

When defining security group rules where a service requires some outbound access, a common shortcut is allowing outbound traffic to any destination IP address. This is typically done to avoid having to set constraints at development time. If an attacker can ever compromise a service running in that security group, there is nothing stopping the attacker from reaching the rest of the internet from that one instance. If, instead, the outbound access was limited to only those destination IP addresses that the application needs, then that would limit the attacker's capabilities should they be able to compromise the service.

If the application is designed to issue requests across the internet, the service should be placed in a tightly controlled VPC for extra security measures.

inline-ingress-security-group-public-access

A security group has been configured to allow inbound traffic (ingress) from any IP address. Resources in this security group are potentially exposed to the public internet.

Ingress rules should be restricted to required services.

Insecure Example

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["0.0.0.0/0"]     # No restrictions on IP addresses 
  }
}

Secure Example

resource "aws_security_group" "allow_tls" {
  name        = "allow_tls"
  vpc_id      = aws_vpc.main.id

  ingress {
    description = "TLS from VPC"
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = ["10.10.10.0/24"]     # Limiting the ingress CIDR block to only the necessary addresses that you need is good practice  
  }
}

More information

resource-publicly-accessible

A database resource has been defined as publicly accessible. This resource is exposed to the public internet. These resources should be private to prevent attackers from attempting to access or abuse the resources.

This rule specifically checks for the publicly_accessible parameter on the following resources:

  • aws_db_instance (defaults to false)
  • aws_dms_replication_instance (defaults to false)
  • aws_rds_cluster_instance (defaults to false)
  • aws_redshift_cluster (defaults to true)

Insecure Example

resource "aws_db_instance" "default" {
  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  name                 = "mydb"
  parameter_group_name = "default.mysql5.7"
  publicly_accessible  = true     # This enables public access to the resource
}

Secure Example

resource "aws_db_instance" "default" {
  allocated_storage    = 20
  storage_type         = "gp2"
  engine               = "mysql"
  engine_version       = "5.7"
  instance_class       = "db.t2.micro"
  name                 = "mydb"
  parameter_group_name = "default.mysql5.7"
  publicly_accessible  = false
}

More Information

Security Best Practices

Many AWS resources have properties that can be enabled that provide extra security benefits. In most cases enabling these settings provide significant security benefits for little to no extra work.

s3-disabled-server-logging

An S3 bucket does not have server access logs enabled. Server access logs contain information about requests to S3 buckets and are critical for tracking unauthorized access during security incidents.

Insecure Example

resource "aws_s3_bucket" "b" {
  bucket = "my-tf-test-bucket"
  acl    = "private"

  # No server access logging defined!
}

Secure Example

resource "aws_s3_bucket" "log_bucket" {     # An additional bucket to store the logs is required
  bucket = "my-tf-log-bucket"
  acl    = "log-delivery-write"
}

resource "aws_s3_bucket" "b" {   
  bucket = "my-tf-test-bucket"
  acl    = "private"

  logging {
    target_bucket = aws_s3_bucket.log_bucket.id
    target_prefix = "log/"
  }
}

More Information

security-group-rule-missing-description

When defining security group or security group rules - it is always good practice to include a description. The description is useful for auditing purposes.

Insecure Example

In the first example below, a security group is defined without including a description.

resource "aws_security_group" "default" {
  name   = "my-security-group"
  vpc_id = "my-vpc-id"

  tags = {
    Name        = "mygroup-rds-sg"
    Environment = "dev"
  }
}

In the second example below, a security group rule is defined without including a description.

resource "aws_security_group_rule" "example" {
  type              = "ingress"
  from_port         = 2525
  to_port           = 2525
  protocol          = "tcp"
  cidr_blocks       = [aws_vpc.example.cidr_block]
  security_group_id = "sg-123456"
}

Secure Example

The security-best-practice version of the security group definition above is:

resource "aws_security_group" "default" {
  name   = "my-security-group"
  vpc_id = "my-vpc-id"
  description = "internal only sg for db access/dev env"     # Adding a description is good security practice

  tags = {
    Name        = "mygroup-rds-sg"
    Environment = "dev"
  }
}

The security-best-practice version of the security group rule definition above is:

resource "aws_security_group_rule" "example" {
  type              = "ingress"
  from_port         = 2525
  to_port           = 2525
  protocol          = "tcp"
  cidr_blocks       = [aws_vpc.example.cidr_block]
  security_group_id = "sg-123456"
  description       = "allow port 2525 access for custom microservice access"
}

kms-no-auto-rotation

A KMS Customer Master Key (CMK) does not have auto-rotation enabled. Key rotation helps limit exposure in the case that a key is compromised. Enabling auto-rotation for will instruct AWS to transparently rotate keys without affecting users of the key.

Insecure Example

resource "aws_kms_key" "a" {
  description             = "KMS key 1"
  deletion_window_in_days = 10
}

Secure Example

resource "aws_kms_key" "a" {
  description             = "KMS key 1"
  deletion_window_in_days = 10
  enable_key_rotation = true
}

More Information

ecr-disabled-image-scans

When defining your ECR repository, it is very good security practice to enable ECR automatic image scans. If malicious code made its way to your image (either through source code or potentially via attackers gaining access to credentials and thereby to the ECR repository), the scans provide an extra layer of detection/alerting.

Insecure Example

resource "aws_ecr_repository" "foo" {
  name                 = "bar"
  image_tag_mutability = "MUTABLE"
}

Secure Example

resource "aws_ecr_repository" "foo" {
  name                 = "bar"
  image_tag_mutability = "MUTABLE"

  image_scanning_configuration {
    scan_on_push = true
  }
}

More Information

aws-classic-resource-usage

For certain AWS services, namely:

  • AWS Redshift
  • AWS RDS
  • AWS ElastiCache

AWS allows you to specify the networking mode for your cluster. You can either specify:

  • EC2 Classic
  • EC2 VPC

In EC2 Classic, your cluster runs in a single, flat network that shares infrastructure with other AWS customers. You can specify security groups to isolate your instances from other customers, but there is still a security risk in sharing infrastructure. The better alternative is to always use the EC2-VPC cluster mode so that instances run in isolated VPCs.

Insecure Example

resource "aws_redshift_cluster" "default" {
  cluster_identifier = "tf-redshift-cluster"
  cluster_security_groups = ["classic-security-group"]
  database_name      = "mydb"
  master_username    = "foo"
  master_password    = var.password
  node_type          = "dc1.large"
  cluster_type       = "single-node"
}

# The aws_redshift_security_group type results in less secure EC2 Classic instances
resource "aws_redshift_security_group" "classic-security-group" {
  name = "redshift-sg"

  ingress {
    cidr = "10.0.0.0/24"
  }
}

Secure Example

resource "aws_redshift_cluster" "default" {
  cluster_identifier = "tf-redshift-cluster"
  vpc_security_group_ids = ["vpc-security-group"]
  database_name      = "mydb"
  master_username    = "foo"
  master_password    = var.password
  node_type          = "dc1.large"
  cluster_type       = "single-node"

}

# The aws_security_group type results in more secure EC2 VPC instances
resource "aws_security_group" "vpc-security-group" {
  description = "app1_security_group"
  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["10.0.0.0/24"]
  }
}

More Information