DEV Community

Cover image for Exposing Private Load Balancers with CloudFront VPC Origins
Daniel Kraszewski for u11d

Posted on • Originally published at u11d.com

Exposing Private Load Balancers with CloudFront VPC Origins

Let's explore CloudFront VPC Origins, an AWS feature that allows you to connect CloudFront directly to private resources in your VPC without exposing them to the Internet. In this article, we'll see why this matters and how you can implement it using Terraform.

Introduction: Why Keep Your Load Balancers Private?

When building web applications on AWS, we've traditionally needed public load balancers so CloudFront could reach our origin servers. This presented several challenges:

  1. Your load balancer is exposed to the world - Even with tight security groups, a public load balancer increases your application's attack surface
  2. Maintaining IP allow lists is a pain - The traditional approach requires whitelisting CloudFront's IP ranges in your security groups, which change periodically and require updates
  3. Secret headers aren't secure enough - Some try using secret headers as an alternative to IP whitelisting, but this "security by obscurity" approach can be discovered and exploited
  4. Direct access bypassing - Attackers might discover your load balancer's public endpoint and bypass CloudFront entirely, circumventing any protections you've set up there

CloudFront VPC Origins provides a better solution. It creates a direct, secure connection between CloudFront and resources in your private subnets, keeping your load balancers completely hidden from the public internet while still being accessible through CloudFront.

The Architecture

Before diving into implementation details, let's visualize how CloudFront VPC Origins creates a secure architecture. The diagram below illustrates the end-to-end flow from internet users to your private resources, showing how CloudFront VPC Origins bridges the gap between the public internet and your private VPC without exposing your infrastructure:

Practical Implementation with Terraform

Let's implement this using Terraform, based on our Medusa.js AWS module. We'll go through this in a logical order based on dependencies.

1. Setting Up the Private Load Balancer

First, we create a load balancer in private subnets:

resource "aws_lb" "main" {
  load_balancer_type = "application"  # Default value
  subnets            = var.vpc.private_subnet_ids  # The key part - using private subnets!
  security_groups    = [aws_security_group.lb.id]
  name               = "${local.prefix}-lb"
  tags               = local.tags
}

resource "aws_lb_target_group" "main" {
  port        = 9000  # Default container port for Medusa backend
  protocol    = "HTTP"
  vpc_id      = var.vpc.id
  target_type = "ip"
  name        = "${local.prefix}-tg"
  health_check {
    protocol            = "HTTP"
    port                = 9000
    interval            = 30
    matcher             = "200"
    timeout             = 3
    path                = "/health"
    healthy_threshold   = 3
    unhealthy_threshold = 3
  }

  tags = local.tags
}

resource "aws_lb_listener" "main" {
  load_balancer_arn = aws_lb.main.arn
  port              = 80
  protocol          = "HTTP"

  default_action {
        type = "forward"
        forward {
            target_group {
                arn = aws_lb_target_group.main.arn
            }
        }
    }

  lifecycle {
    replace_triggered_by = [aws_lb_target_group.main]
  }

  tags = local.tags
}
Enter fullscreen mode Exit fullscreen mode

2. Creating the Security Groups

Next, we set up security groups that only allow traffic from CloudFront VPC Origins:

# Using AWS-managed prefix list for CloudFront VPC Origins
data "aws_ec2_managed_prefix_list" "vpc_origin" {
  name = "com.amazonaws.global.cloudfront.origin-facing"
}

resource "aws_security_group" "lb" {
  name_prefix = "${local.prefix}-lb-"
  description = "Allow inbound traffic from CloudFront VPC Origins"
  vpc_id      = var.vpc.id
  tags        = local.tags

  lifecycle {
    create_before_destroy = true
  }
}

resource "aws_vpc_security_group_ingress_rule" "vpc_origin" {
  security_group_id = aws_security_group.lb.id
  prefix_list_id    = data.aws_ec2_managed_prefix_list.vpc_origin.id
  from_port         = 80
  to_port           = 80
  ip_protocol       = "tcp"
  tags              = local.tags
}

resource "aws_vpc_security_group_egress_rule" "lb" {
  security_group_id = aws_security_group.lb.id
  cidr_ipv4         = "0.0.0.0/0"
  ip_protocol       = "-1"
  tags              = local.tags
}

Enter fullscreen mode Exit fullscreen mode

This is where the magic happens - instead of maintaining our own list of CloudFront IPs, we use AWS's managed prefix list com.amazonaws.global.cloudfront.origin-facing. AWS keeps this updated automatically, so you don't have to worry about it.

3. Setting Up the VPC Origin Connection

Now, we create the CloudFront VPC Origin that connects to our private load balancer:

locals {
  origin_id = "${local.prefix}-lb"
}

resource "aws_cloudfront_vpc_origin" "main" {
  vpc_origin_endpoint_config {
    name                   = local.origin_id
    arn                    = aws_lb.main.arn
    http_port              = 80
    https_port             = 443
    origin_protocol_policy = "http-only"

    origin_ssl_protocols {
      quantity = 1
      items    = ["TLSv1.2"]
    }
  }

  timeouts {
    create = "30m"  # Important: VPC Origins take time to provision
  }

  depends_on = [aws_lb_target_group.main, aws_security_group.lb]

  tags = local.tags
}
Enter fullscreen mode Exit fullscreen mode

Note: CloudFront VPC Origins can take a while to provision, and without extended timeout, Terraform might give up too early.

4. Configuring the CloudFront Distribution

Finally, we set up the CloudFront distribution to use our VPC Origin:

resource "aws_cloudfront_distribution" "main" {
  enabled = true
  comment = "My-Project-Dev-Backend"  # Example of what title(local.prefix) might render

  origin {
    domain_name = aws_lb.main.dns_name
    origin_id   = local.origin_id

    vpc_origin_config {
      vpc_origin_id = aws_cloudfront_vpc_origin.main.id
    }
  }

  # Default cache behavior
  default_cache_behavior {
    target_origin_id       = local.origin_id
    viewer_protocol_policy = "redirect-to-https"

    # Cache settings for APIs - disabled to allow dynamic content
    min_ttl     = 0
    default_ttl = 0
    max_ttl     = 0

    forwarded_values {
      query_string = true
      headers      = ["*"]

      cookies {
        forward = "all"
      }
    }

    allowed_methods = ["GET", "HEAD", "POST", "PUT", "PATCH", "OPTIONS", "DELETE"]
    cached_methods  = ["GET", "HEAD", "OPTIONS"]
  }

  # Certificate configuration
  viewer_certificate {
    cloudfront_default_certificate = true  # Use CloudFront's default certificate
  }

  # Other settings
  price_class = "PriceClass_100"  # Use only North America and Europe edge locations

  restrictions {
    geo_restriction {
      restriction_type = "none"
    }
  }

  tags = {
    Project     = "my-project"
    Environment = "dev"
    Owner       = "DevOps Team"
    ManagedBy   = "terraform"
    Component   = "Backend"
  }
}
Enter fullscreen mode Exit fullscreen mode

The vpc_origin_config block that references our VPC Origin is the key difference from a traditional CloudFront setup.

Conclusion: Solving Real Security Challenges

CloudFront VPC Origins represents a significant advancement for securing web applications on AWS, directly addressing the security challenges we outlined at the beginning:

Remember the problem of exposed load balancers? With VPC Origins, your load balancers now remain completely isolated in private subnets, dramatically reducing your attack surface. The headache of maintaining IP allowlists is eliminated through AWS's managed prefix list com.amazonaws.global.cloudfront.origin-facing, which is automatically maintained for you.

Insecure secret header approaches are no longer needed, as the direct connection between CloudFront and your VPC resources provides a much stronger security model. And attackers trying to bypass CloudFront? With your load balancer in a private subnet, there's simply no way for external traffic to reach it except through CloudFront.

There are a few practical considerations: CloudFront VPC Origins take time to provision, each region needs its own VPC Origin, there's a cost for the connectivity, and setup is slightly more complex. However, for organizations handling sensitive data or with compliance requirements, these minor considerations are easily outweighed by the significant security benefits.

Top comments (0)