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
Changing a load balancer from internet-facing to internal is a destructive change in Terraform (ForceNew). The resource gets replaced, the DNS name changes, and every consumer pointing at the old name breaks. If public access is genuinely needed, provision a new internal ALB behind API Gateway or CloudFront rather than toggling the existing one.
Implementation
Choose the approach that matches how you manage Terraform.
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
internalargument defaults tofalseon bothaws_lbandaws_elb. Omit it and you get a public-facing load balancer. Always setinternal = trueexplicitly.Replacement required on scheme change
Changing
internalfromfalsetotrueon an existingaws_lbtriggers 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_lbwithload_balancer_type = "gateway") don't support theinternalargument. 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).
Framework-specific interpretation
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_accessPowerpipe Control:
aws_compliance.control.elb_application_classic_network_lb_prohibit_public_accessProwler Checks:
elb_internet_facing,elbv2_internet_facingTrivy Check:
AWS-0053
Last reviewed: 2026-03-09