AWS CloudFront Security Group for Workload Protection (Terraform)

AWS CloudFront allows you to front your application (be it on EC2 instances or REST API etc.) so you can provide high availability, caching and protection (with WAF) to the workload. When configuring your application, you don’t want the application to be directly accessible without using CloudFront, so therefore you need to add some restrictions, there’s a few ways you can do this, its best to layer them up. You can also add things like a “shared secret”, which your application knows, CloudFront knows, and then reject connections not specifying the shared secret, but that is beyond the scope of this document.

The method shown below is to have a Security Group on the EC2 Instance(s), Application Load Balancer etc. only accept connections from AWS CloudFront, and reject from anywhere else; thus protecting the resource from being attacked from any point you don’t want it to be!

AWS provide a continually updated set of ranges you can use for this purpose. One of the things that makes adding this is in Terraform a little tricky is the limitations on the Security Group itself. A Security Group can only have 50 (or 60 with a support request) rules within it, but there are many more IP ranges than that used by AWS CloudFront. So you need to split them out into separate Security Groups and attach all of them to the EC2 Instance, Application Load Balancer etc.

I’d found a number of examples, but often they didn’t work, so here is a known working example, along with how you can add it to an EC2 Instance as an example.

// Cloudfront Security Group ----------------------------------------------------------------------------

data "aws_ip_ranges" "cloudfront" {
  regions  = ["global"]
  services = ["cloudfront"]
}

locals {
  chunks_v4 = chunklist(data.aws_ip_ranges.cloudfront.cidr_blocks, 50)
}

output "chunks_v4" {
  value = local.chunks_v4
}

resource "aws_security_group" "cloudfront_sg" {
  count = length(local.chunks_v4)
  name        = "cloudfront-sg-${count.index + 1}"
  vpc_id      = aws_vpc.VPC_simple-cf.id

  ingress {
    from_port   = 80
    to_port     = 80
    protocol    = "tcp"
    cidr_blocks = local.chunks_v4[count.index]
  }

  # ingress {
  #   from_port   = 443
  #   to_port     = 443
  #   protocol    = "tcp"
  #   cidr_blocks = local.chunks_v4[count.index]
  # }

  egress {
    from_port   = 0
    to_port     = 0
    protocol    = "-1"
    cidr_blocks = ["0.0.0.0/0"]
  }

  lifecycle {
    create_before_destroy = true
  }
}

The example above obtains the list of AWS CloudFront IP Addresses, puts them into a list, then splits them on every 50. Note: there is a limit of 50 rules (of each type ingress/egress) per Security Group. So in the example above, I’m allowing HTTP to the workload, if you wanted to add HTTPS as well, that would double up the number of rules meaning, you’d need to double the number of splits! (i.e. drop the 50 to 25), because you’d be adding 100 rules otherwise, which would fail.

Adding the Security Groups to an EC2 Instance is fairly straightforward, you add them with the vpc_security_group_ids directive, but then specify each group created in the index by the [*].

vpc_security_group_ids = aws_security_group.cloudfront_sg[*].id

So it looks like:

resource "aws_instance" "VPC_simple-cf-Instance-1" {
  ami                    = "ami-008ea0202116dbc56"
  instance_type          = "t2.micro"
  tenancy                = "default"
  availability_zone      = "eu-west-2a"
  key_name               = ""
  subnet_id              = aws_subnet.VPC_simple-cf-Public-Subnet-A.id
  #vpc_security_group_ids = [aws_security_group.VPC_simple-cf-SecurityGroup-1.id]
  vpc_security_group_ids = aws_security_group.cloudfront_sg[*].id
  iam_instance_profile   = aws_iam_instance_profile.ec2_profile.name
  user_data              = local.example_user_data

  tags = {
    Name = "VPC_simple-cf-Instance-1"
  }
}

Leave a comment