ELB load balancers should prohibit public access
A public-facing load balancer puts an internet-resolvable DNS name in front of your backend compute. Security groups help, but the attack surface is still larger: misconfigurations, overly permissive listener rules, or a zero-day in the application stack can expose services that were never meant to be internet-accessible.
Internal load balancers restrict ingress to the VPC and peered networks. Public access, where genuinely needed, runs through explicitly provisioned entry points like API Gateway or CloudFront with origin access controls, not through a load balancer with a public IP.
Retrofit consideration
Implementation
Choose the approach that matches how you manage Terraform.
Use the compliance.tf module to enforce this control by default. See get started with compliance.tf.
module "alb" {
source = "registry.compliance.tf/terraform-aws-modules/alb/aws"
version = ">=10.0.0,<11.0.0"
access_logs = {
bucket = "example-bucket-abc123"
enabled = true
}
internal = true
listeners = {
https = {
certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
forward = {
target_group_key = "default"
}
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
}
}
load_balancer_type = "application"
name = "abc123"
security_groups = ["sg-abc12345"]
subnets = ["subnet-abc123", "subnet-def456"]
target_groups = {
default = {
create_attachment = false
name_prefix = "def-"
port = 443
protocol = "HTTPS"
target_type = "ip"
}
}
vpc_id = "vpc-12345678"
}This control is enforced automatically with Compliance.tf modules. Start free trial
If you use terraform-aws-modules/alb/aws, set the right module inputs for this control. You can later migrate to the compliance.tf module with minimal changes because it is compatible by design.
module "alb" {
source = "terraform-aws-modules/alb/aws"
version = ">=10.0.0,<11.0.0"
access_logs = {
bucket = "example-bucket-abc123"
enabled = true
}
listeners = {
https = {
certificate_arn = "arn:aws:acm:us-east-1:123456789012:certificate/12345678-1234-1234-1234-123456789012"
forward = {
target_group_key = "default"
}
protocol = "HTTPS"
ssl_policy = "ELBSecurityPolicy-TLS13-1-2-2021-06"
}
}
load_balancer_type = "application"
name = "abc123"
security_groups = ["sg-abc12345"]
subnets = ["subnet-abc123", "subnet-def456"]
target_groups = {
default = {
create_attachment = false
name_prefix = "def-"
port = 443
protocol = "HTTPS"
target_type = "ip"
}
}
vpc_id = "vpc-12345678"
internal = true
}Use AWS provider resources directly. See docs for the resources involved: aws_lb.
resource "aws_lb" "this" {
access_logs {
bucket = "pofix-logs-abc123"
}
name = "pofix-abc123"
subnets = ["subnet-abc123", "subnet-def456"]
internal = true
}What this control checks
aws_lb covers ALBs and NLBs: internal must be set to true. When internal is false or omitted (the default is false), the load balancer is internet-facing and fails this control. aws_elb for Classic Load Balancers uses the same argument with the same requirement. The underlying check is whether the scheme attribute resolves to "internal" rather than "internet-facing". Any load balancer with a publicly resolvable DNS name fails. Check that subnets referenced in subnets or subnet_mapping are private subnets with no routes to an internet gateway. Placing an internal load balancer in a public subnet is technically valid but undermines the network-segmentation goal this control is meant to enforce.
Common pitfalls
Default scheme is internet-facing
The internal argument defaults to false on both aws_lb and aws_elb. Omit it and you get a public-facing load balancer. Always set internal = true explicitly.
Replacement required on scheme change
Changing internal from false to true on an existing aws_lb triggers a destroy-and-recreate (ForceNew), which changes the DNS name and breaks any consumer that references it. Plan a blue-green migration rather than an in-place toggle.
Internal ALB in public subnet
Use private subnets for internal load balancers. Terraform won't stop you from placing an internal load balancer in a public subnet (one with an internet gateway route), but it muddies network segmentation and gives auditors reviewing subnet topology a reasonable question to ask.
Gateway Load Balancers excluded
Gateway Load Balancers (aws_lb with load_balancer_type = "gateway") don't support the internal argument. This control typically excludes them, but verify your compliance scanner's scope before assuming they pass automatically.
Audit evidence
AWS Config rule evaluation results showing all ELB resources as compliant are the primary artifact. The EC2 Load Balancers console has a 'Scheme' column; a screenshot or export filtered to the relevant accounts and regions works as point-in-time evidence.
For continuous coverage, provide Config conformance pack or Security Hub findings with historical compliance status. CloudTrail logs for CreateLoadBalancer calls can confirm the Scheme parameter was internal at provisioning time. Where exceptions exist for legitimately public-facing load balancers, include documented risk acceptance and compensating controls (WAF association, security group restrictions).
Related controls
Tool mappings
Use these identifiers to cross-reference this control across tools, reports, and evidence.
- Compliance.tf Control:
elb_application_classic_network_lb_prohibit_public_access - Powerpipe Control:
aws_compliance.control.elb_application_classic_network_lb_prohibit_public_access - Prowler Checks:
elb_internet_facing,elbv2_internet_facing - Trivy Check:
AWS-0053
Last reviewed: 2026-03-09