Back to blog
AzureBest PracticesCloud SecurityNetworking

Public-Facing Azure Load Balancer: Risks and Remediation

Learn what a public-facing Azure Load Balancer exposes, why it's risky, and how to lock it down with NSGs, internal LBs, Azure Policy, and CI/CD gates.

TL;DR

This check flags Azure Load Balancers that have a public IP on their frontend, meaning the service behind them is reachable from the internet. If that exposure is unintended, move the workload behind an internal load balancer or lock down access with NSGs and a WAF.

A public-facing load balancer is sometimes exactly what you want, a web app that needs to serve traffic to the world. Other times it is an accident, the result of a default template, a copy-paste deployment, or someone choosing the wrong SKU at 4pm on a Friday. The Lensix lb_public check exists to make sure you actually meant to put that frontend on the public internet.

This post walks through what the check looks at, the risk of leaving a load balancer exposed by mistake, and how to either confirm the exposure is intentional or shut it down.


What this check detects

The check inspects every Azure Load Balancer in your subscriptions and looks at the frontend IP configuration. If any frontend is associated with a public IP address rather than a private subnet IP, the load balancer is flagged as public-facing.

Azure Load Balancers come in two flavors:

  • Public Load Balancer — has a public IP on the frontend and routes inbound internet traffic to backend pool members.
  • Internal (private) Load Balancer — has a private IP from a VNet subnet and is only reachable from inside the network or via connected networks.

The lb_public check fires on the first type. It is not saying the configuration is wrong, it is telling you the frontend is internet-reachable so you can verify that decision.

Note: The Azure Load Balancer operates at Layer 4 (TCP/UDP). It does not inspect HTTP headers, terminate TLS, or filter by URL path. Access control happens through Network Security Groups on the backend, not on the load balancer itself. This is a common source of confusion and a common source of accidental exposure.


Why it matters

A load balancer is not a firewall. A public Azure Load Balancer forwards whatever traffic matches its rules straight to the backend pool, and the only thing standing between the internet and your VMs is the Network Security Group attached to the NIC or subnet. If that NSG is permissive, or missing, your backend instances are directly exposed.

Here is where this turns into a real incident:

  • Management ports left open. A load balancing rule for port 3389 (RDP) or 22 (SSH) on a public frontend, combined with an Allow Any NSG, gives the entire internet a login prompt to brute force.
  • Internal services accidentally published. Databases, message queues, admin dashboards, and internal APIs that were never meant to leave the VNet end up addressable from anywhere.
  • Wider attack surface for scanning. Public IPs are constantly scanned. Shodan and Censys index them within hours. A misconfigured public LB shows up fast.
  • DDoS exposure. Every public IP is a potential target for volumetric attacks. Without Azure DDoS Protection, a public frontend can become a cost and availability problem.

Warning: A Standard SKU Load Balancer is "secure by default" in the sense that traffic is denied unless an NSG explicitly allows it. But many teams attach broad NSG rules to get things working during deployment and never tighten them. The default-deny only helps if you do not undo it.


How to fix it

Start by deciding whether the public exposure is intended. For a customer-facing web tier, a public frontend is correct, and the fix is hardening rather than removal. For an internal workload, the fix is converting to an internal load balancer.

Step 1: Identify the public load balancers

az network lb list \
  --query "[].{name:name, rg:resourceGroup, frontends:frontendIPConfigurations[].publicIPAddress.id}" \
  -o json

Any entry with a non-null publicIPAddress.id has a public frontend. Cross-reference the load balancing rules to see which ports are exposed:

az network lb rule list \
  --lb-name myLoadBalancer \
  --resource-group myResourceGroup \
  --query "[].{name:name, frontendPort:frontendPort, backendPort:backendPort, protocol:protocol}" \
  -o table

Step 2a: If it should be internal, recreate it as a private LB

Azure does not let you flip a frontend from public to private in place. You create a new frontend IP configuration bound to a subnet, repoint the rules, and remove the public frontend.

# Add an internal frontend bound to a subnet with a static private IP
az network lb frontend-ip create \
  --lb-name myLoadBalancer \
  --resource-group myResourceGroup \
  --name internalFrontend \
  --vnet-name myVnet \
  --subnet myBackendSubnet \
  --private-ip-address 10.0.2.10

Then update each load balancing rule to use the internal frontend, and once traffic is confirmed flowing through it, remove the public frontend.

Danger: Removing the public frontend will sever any live connections coming through that IP. If clients depend on the public address, schedule this during a maintenance window and update DNS or client configuration first. Validate the internal path end to end before deleting anything.

# Remove the public frontend once the internal path is verified
az network lb frontend-ip delete \
  --lb-name myLoadBalancer \
  --resource-group myResourceGroup \
  --name publicFrontend

Step 2b: If it must stay public, harden it

Lock the backend NSG down to only the ports and sources you actually need. Never leave management ports open to the internet.

# Allow HTTPS from anywhere to the web tier
az network nsg rule create \
  --resource-group myResourceGroup \
  --nsg-name myBackendNsg \
  --name Allow-HTTPS-Inbound \
  --priority 100 \
  --direction Inbound \
  --access Allow \
  --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-port-ranges 443

# Explicitly deny RDP and SSH from the internet
az network nsg rule create \
  --resource-group myResourceGroup \
  --nsg-name myBackendNsg \
  --name Deny-Mgmt-Inbound \
  --priority 200 \
  --direction Inbound \
  --access Deny \
  --protocol Tcp \
  --source-address-prefixes Internet \
  --destination-port-ranges 22 3389

Tip: If you need administrative access to backend VMs, use Azure Bastion or just-in-time VM access through Microsoft Defender for Cloud instead of opening management ports. Both give you access without a permanently exposed port.

For HTTP/HTTPS workloads, also consider fronting the public IP with Azure Application Gateway (which includes a WAF) or Azure Front Door. A Layer 4 load balancer cannot block SQL injection or filter malicious paths, a WAF can.


How to prevent it from happening again

Manual review does not scale. The exposure usually comes back through the next deployment, so the guardrail has to live in your pipeline and in Azure Policy.

Define internal load balancers in IaC

If a workload should be internal, make that explicit in your Terraform so a reviewer sees it and so drift gets caught:

resource "azurerm_lb" "internal" {
  name                = "internal-lb"
  location            = azurerm_resource_group.main.location
  resource_group_name = azurerm_resource_group.main.name
  sku                 = "Standard"

  frontend_ip_configuration {
    name                          = "internal-frontend"
    subnet_id                     = azurerm_subnet.backend.id
    private_ip_address_allocation = "Static"
    private_ip_address            = "10.0.2.10"
  }
}

Notice there is no public_ip_address_id here. The presence of that attribute in a plan is a clear signal during code review that something is going public.

Block accidental public IPs with Azure Policy

Azure Policy can audit or deny load balancers that attach a public IP in environments that should never have one. Apply a deny policy to internal-only resource groups so the deployment fails before anything is exposed:

{
  "if": {
    "allOf": [
      { "field": "type", "equals": "Microsoft.Network/loadBalancers" },
      { "field": "Microsoft.Network/loadBalancers/frontendIPConfigurations[*].publicIPAddress.id", "exists": "true" }
    ]
  },
  "then": { "effect": "deny" }
}

Gate it in CI/CD

Run a static scan on your Terraform plan before apply. Tools like Checkov or tfsec catch public load balancer frontends, and you can fail the build if one shows up in a tagged-internal environment. Pair that with the Lensix lb_public check running on a schedule so anything created outside the pipeline (click-ops, scripts) still gets caught.

Tip: Policy-as-code in CI catches what you deploy, continuous scanning catches what slips past it. You want both. The pipeline gate is your front door, the scheduled scan is your smoke detector.


Best practices

  • Default to internal. Only attach a public IP when a workload genuinely needs to serve the internet. Treat every public frontend as a deliberate, reviewed decision.
  • Use Standard SKU. Standard Load Balancers are secure by default (traffic denied unless an NSG allows it) and support zone redundancy and DDoS integration. Basic SKU is being retired, plan migrations off it.
  • Never expose management ports. RDP and SSH belong behind Bastion or JIT access, never on a public load balancing rule.
  • Layer a WAF in front of web traffic. For HTTP/HTTPS, put Application Gateway or Front Door between the internet and your backend so you get request inspection the LB cannot provide.
  • Enable DDoS Protection on VNets that host public-facing services.
  • Tag intent. Tag load balancers with something like exposure: public or exposure: internal so audits can distinguish deliberate exposure from accidents at a glance.
  • Review NSGs alongside the LB. A public frontend is only as safe as the NSG behind it. Audit both together, not separately.

The goal is not to eliminate every public load balancer, it is to make sure every one of them is there on purpose and properly fenced. Run the lb_public check on a schedule, confirm each finding against its intended exposure, and you turn a recurring surprise into a known, documented part of your architecture.