Back to blog

Security

Protecting your ALB with AWS WAF and CloudFront

Learn how to protect your ALB with AWS WAF and CloudFront.

March 16, 2026Platform Engineering4 min read

Protecting your Application Load Balancer (ALB) from unwanted traffic is an important part of securing internet-facing applications. While products such as Cloudflare offer feature-rich edge protection, you can also build a strong native AWS solution with CloudFront and AWS WAF.

This post focuses on locking an ALB down so it only accepts the traffic that should be reaching it, while still allowing CloudFront to sit in front as the public edge.

Solution Overview

The solution has three main pieces:

  1. A CloudFront distribution in front of the ALB
  2. A CloudFront Web ACL for edge filtering and rate limiting
  3. An ALB Web ACL that only allows requests with the expected origin token header

Together they create a layered protection model:

  • CloudFront handles public edge traffic
  • WAF filters traffic both at the edge and at the origin
  • the ALB only accepts traffic that matches the expected path into the application

CloudFront Configuration

The first step is to generate an origin token:

resource "random_string" "origin_token" {
  length  = 30
  special = false
}

Then configure CloudFront to send that value to the ALB as a custom header:

resource "aws_cloudfront_distribution" "distribution" {
  origin {
    domain_name = aws_lb.example.dns_name
    origin_id   = "alb"

    custom_header {
      name  = "X-Origin-Token"
      value = random_string.origin_token.result
    }
  }

  enabled = true
  aliases = ["yourdomain.example.com"]

  default_cache_behavior {
    allowed_methods  = ["GET", "HEAD", "OPTIONS", "PUT", "POST", "PATCH", "DELETE"]
    cached_methods   = ["GET", "HEAD"]
    target_origin_id = "alb"

    forwarded_values {
      query_string = true
      headers      = ["X-Origin-Token"]

      cookies {
        forward = "all"
      }
    }

    viewer_protocol_policy = "redirect-to-https"
  }
}

Configuring AWS WAF

CloudFront Web ACL

The CloudFront Web ACL is where you can apply managed rule sets, reputation filtering, and rate limiting:

module "cloudfront_waf" {
  source = "coresolutions-ltd/wafv2/aws"

  name_prefix    = "CloudFront"
  default_action = "allow"
  scope          = "CLOUDFRONT"
  rate_limit     = 1000
  managed_rules  = [
    "AWSManagedRulesCommonRuleSet",
    "AWSManagedRulesAmazonIpReputationList",
    "AWSManagedRulesAdminProtectionRuleSet",
    "AWSManagedRulesKnownBadInputsRuleSet"
  ]
}

ALB Web ACL

The ALB Web ACL should be stricter. A useful pattern is to block by default and only allow traffic that includes the expected origin token:

module "alb_waf" {
  source = "coresolutions-ltd/wafv2/aws"

  name_prefix    = "ALB"
  default_action = "block"
  scope          = "REGIONAL"
  origin_token   = random_string.origin_token.result
}

resource "aws_wafv2_web_acl_association" "waf_association" {
  resource_arn = aws_lb.example.arn
  web_acl_arn  = module.alb_waf.waf_arn
}

Configuring the ALB Security Group

As another layer of defence, restrict the ALB security group to CloudFront IP ranges:

data "aws_ip_ranges" "cloudfront" {
  services = ["CLOUDFRONT"]
}

resource "aws_security_group" "alb_sg" {
  name        = "alb_sg"
  description = "Allow traffic from CloudFront"
  vpc_id      = aws_vpc.main.id

  ingress {
    from_port   = 443
    to_port     = 443
    protocol    = "tcp"
    cidr_blocks = data.aws_ip_ranges.cloudfront.cidr_blocks
  }
}

Because CloudFront IP ranges change over time, you may also want automation to keep those rules up to date.

Testing the Configuration

To verify the setup:

  1. Access the application through CloudFront and confirm it works.
  2. Attempt direct access to the ALB and confirm it is blocked.
  3. Review WAF logs and metrics to confirm non-matching requests are being denied.

Verify the bypass, not the happy path

The configuration that matters here is the one you cannot see working. Confirm that a request straight to the ALB is refused from outside CloudFront before you call this done. An origin that still answers on its own DNS name means the WAF in front of it is decoration, and that is the state most half-finished setups sit in.