Chapter 6
Secure access & load balancing
Your two web servers are running but nobody can reach them. In this chapter you open exactly one port with an NSG, get admin access through Bastion without public IPs, and put a load balancer in front of them.
Theory
Network security groups
A network security group (NSG) is a stateful packet filter: a list of allow and deny rules for traffic to and from a subnet or a NIC. "Stateful" means that if a request is allowed in, the reply is automatically allowed out; you don't need a rule for the return traffic.
Rule properties
- Priority: 100 to 4096. Lower numbers are processed first, and processing stops at the first match.
- Source and destination: an IP address or CIDR range, a service tag, or an application security group.
- Port ranges and protocol (TCP, UDP, ICMP or Any).
- Action: Allow or Deny. Direction: Inbound or Outbound.
Default rules
Every NSG has six default rules that you can't delete, only override with a lower priority number:
| Priority | Inbound | Outbound |
|---|---|---|
| 65000 | AllowVnetInBound: VirtualNetwork β VirtualNetwork | AllowVnetOutBound |
| 65001 | AllowAzureLoadBalancerInBound: health probes | AllowInternetOutBound |
| 65500 | DenyAllInBound | DenyAllOutBound |
So by default: traffic inside the VNet (including peered VNets and on-premises via a gateway) is allowed, internet inbound is blocked, and internet outbound is allowed.
Service tags
A service tag is a Microsoft-maintained group of IP ranges, so you don't have to track IPs yourself. The ones you'll meet most:
Internet: everything outside the VNet address space and Azure's public ranges.VirtualNetwork: the VNet, peered VNets, connected on-premises ranges.AzureLoadBalancer: the source of load balancer health probes (168.63.129.16).Storage,Sql,AzureMonitor,AzureCloudβ¦, optionally per region, likeStorage.WestEurope.
Subnet NSG, NIC NSG, or both
You can associate an NSG with a subnet, a NIC, or both. When both exist, traffic must be allowed by both:
An NSG must be in the same region as the subnet or NIC, and one NSG can be associated with many subnets and NICs.
Application security groups
An application security group (ASG) is a label you put on NICs, like asg-web or asg-db. You then write NSG rules with ASGs as source or destination: "allow TCP 1433 from asg-web to asg-db". When you add a new web server, you only add its NIC to asg-web; no rule changes.
Limits: all NICs in an ASG must be in the same VNet, and if a rule uses two ASGs, both must be in the same VNet.
Evaluating effective rules
The exam loves "here are three NSG rules, is this traffic allowed?" questions. Work like ARM does:
- Pick the right direction and the right NSG(s) (subnet, NIC, or both).
- Go through the rules from the lowest priority number up. The first rule that matches source, destination, port and protocol decides.
- If nothing matches, the default rules decide.
In Azure you can check this with NIC β Effective security rules (the merged view of subnet and NIC NSGs) and Network Watcher β IP flow verify (tests one packet and names the rule that allowed or denied it).
Rule 100 Deny TCP 80 from Internet and rule 200 Allow TCP 80 from 203.0.113.10. Is traffic from 203.0.113.10 allowed? No: that address is also part of Internet, and rule 100 is evaluated first. Specific allows need a lower number than broad denies.
Azure Bastion
Azure Bastion gives you RDP and SSH in the browser (over HTTPS 443) to VMs that have no public IP. You don't open 22 or 3389 to the internet, and you don't need a jump box.
| SKU | Key points |
|---|---|
| Developer | Free, shared infrastructure, no dedicated subnet, one VNet only (no peered VNets), portal only, one session at a time. |
| Basic | Dedicated hosts in AzureBastionSubnet (/26 minimum) with a Standard public IP. Reaches VMs in peered VNets. |
| Standard | Adds native client support (az network bastion ssh/rdp), host scaling, IP-based connection (e.g. on-prem servers via VPN), file transfer, shareable links. |
| Premium | Adds session recording and private-only deployment (no public IP). |
The usual design is one Bastion in the hub (Basic or higher) that serves all peered spokes. If you put an NSG on AzureBastionSubnet, it needs specific rules (inbound 443 from Internet and GatewayManager, outbound 22/3389 to VirtualNetwork, and a few more), so most people don't.
"Admins must use their own SSH client (not the browser) through Bastion." That needs Standard or higher with native client support enabled. "Connect to an on-premises server by IP through Bastion" also needs Standard.
Azure Load Balancer
Azure Load Balancer distributes TCP and UDP traffic (layer 4) across healthy backend instances. It doesn't look at HTTP, so no URL routing and no TLS termination; that's what Application Gateway is for.
- Public load balancer: has a public IP frontend; balances internet traffic to VMs, and provides outbound internet access through outbound rules.
- Internal load balancer: has a private IP frontend in a subnet; balances traffic inside the VNet, for example from a web tier to an app tier, or from on-premises.
Use the Standard SKU. Basic was retired in September 2025. Standard is zone-redundant (or zonal), supports up to 1,000 backend instances, has an SLA, and is closed by default: an NSG must allow the traffic to the backends.
The parts of a load balancer
- Frontend IP configuration: the public or private IP clients connect to. One LB can have several.
- Backend pool: the VMs or scale set instances, by NIC or by IP address. All backends must be in the same VNet.
- Health probe: TCP, HTTP or HTTPS check on each backend. HTTP/HTTPS probes need a 200 OK. An instance that fails is taken out of rotation; existing connections continue.
- Load-balancing rule: maps a frontend IP and port to the backend pool and port, using a probe.
- Inbound NAT rule: port forwarding to one specific backend, e.g. frontend port 50001 β
vm-web1port 22. - Outbound rule: lets backends reach the internet via the frontend's public IP (SNAT), with explicit port allocation. When you use one, set the LB rule to not use implicit outbound SNAT.
Distribution and special options
- Session persistence: None (default; 5-tuple hash: source IP, source port, destination IP, destination port, protocol), Client IP (2-tuple: same client goes to the same backend), Client IP and protocol (3-tuple).
- HA ports (internal Standard LB only): one rule for all ports and protocols. Used to load-balance NVAs/firewalls.
- Floating IP (direct server return): needed for SQL Server Always On listeners.
- Cross-region load balancer: a global frontend in front of regional Standard LBs.
Which load balancing service?
| Service | Layer | Scope | Choose it when |
|---|---|---|---|
| Load Balancer | 4 (TCP/UDP) | Regional (or cross-region) | Any TCP/UDP app, internal tiers, NVAs, very high throughput |
| Application Gateway | 7 (HTTP/S) | Regional | URL path routing, TLS termination, WAF, cookie affinity |
| Front Door | 7 (HTTP/S) | Global | Global web apps, CDN caching, WAF at the edge |
| Traffic Manager | DNS | Global | DNS-based failover or geo-routing, any protocol |
Troubleshooting a load balancer
When the frontend doesn't answer or only some backends get traffic, check in this order:
- Health probe status (LB β Monitoring β Metrics β Health Probe Status, or Insights). Are the backends healthy?
- Is the app listening on the probe port and returning 200 on the probe path?
- Does an NSG block the probe? The default rule
AllowAzureLoadBalancerInBoundallows it, but a custom Deny all with a lower number would block it. - Does an NSG allow the client traffic? Standard LB needs an explicit allow.
- Are you testing from a backend VM to its own LB frontend? That doesn't work for an internal LB (the flow hairpins back to the same VM and fails).
The "which load balancer" table above is one of the most-used decision trees in AZ-305. Ask: HTTP or not? Global or regional? Need a WAF? Combinations are common: Front Door globally, with Application Gateway or a Standard LB per region.
Self-check
1. A subnet NSG allows inbound TCP 443 from Internet. The NIC NSG has only default rules. Can clients reach the VM on 443?
B. Both NSGs must allow it. Add the allow rule to the NIC NSG too, or remove the NIC NSG.
2. You add a third web server. With the least effort, how do you make the existing "allow 80 to web servers" rule apply to it?
D. That's exactly what ASGs are for.
3. A firewall appliance pair must receive all TCP and UDP traffic on all ports from an internal load balancer. What do you configure?
A. HA ports load-balance all ports and protocols, which is the standard NVA pattern.
4. Users must connect to a VM through Bastion using their local SSH client. Which SKU is the minimum?
C. Native client support starts at Standard.
5. One backend VM gets no traffic from the load balancer. The VM runs and nginx listens on 80. The probe is HTTP on port 8080. What's wrong?
B. (Then why does the other VM get traffic? It probably listens on 8080. Probes must match what the app serves.)
Lab: open the web servers to the world, safely
You open port 80 to the web servers only, put them behind lbe-web with the public IP from chapter 3, give them outbound internet through the load balancer, log in with Bastion, and break and fix the load balancer. Paste the variables first:
RG=rg-az104-lab; LOC=westeurope; LOC2=northeurope; INIT=xx
az vm start -g $RG -n vm-web1 --no-wait; az vm start -g $RG -n vm-web2 --no-wait
-
Task
Label the web servers
Create an application security group
asg-weband put the network interfaces of both web VMs in it.Done when- Each VM's Networking β Application security groups tab shows
asg-web.
Solution Β· Portal
- Create a resource β Application security group: name
asg-web, your region. vm-web1β Networking β Network settings (or Application security groups) β Add application security groups βasg-web. Repeat forvm-web2.
Solution Β· Azure CLI
az network asg create -g $RG -n asg-web -l $LOC for VM in vm-web1 vm-web2; do NICID=$(az vm show -g $RG -n $VM --query "networkProfile.networkInterfaces[0].id" -o tsv) NICNAME=$(basename $NICID) IPCFG=$(az network nic show --ids $NICID --query "ipConfigurations[0].name" -o tsv) az network nic ip-config update -g $RG --nic-name $NICNAME -n $IPCFG --application-security-groups asg-web done - Each VM's Networking β Application security groups tab shows
-
Task
Allow HTTP to the web servers only
Use
nsg-web(created with Bicep in chapter 4) to allow TCP 80 from the internet to members ofasg-web, and protect the wholesnet-websubnet with it. Don't open SSH.Done whennsg-webis associated withsnet-weband has one custom inbound rule.- The effective security rules of
vm-web1's NIC show your rule above the defaults.
Hint
Source: service tag
Internet. Destination: Application security group. Priority: something low like 100.Solution Β· Portal
nsg-webβ Settings β Inbound security rules β Add: Source Service TagInternet, source ports*, Destination Application security groupasg-web, service HTTP / port80, TCP, Allow, priority100, nameallow-http-internet.- Settings β Subnets β Associate:
vnet-spoke/snet-web. vm-web1β Networking β NIC β Help β Effective security rules.
Solution Β· Azure CLI
az network nsg rule create -g $RG --nsg-name nsg-web -n allow-http-internet --priority 100 \ --direction Inbound --access Allow --protocol Tcp \ --source-address-prefixes Internet --source-port-ranges '*' \ --destination-asgs asg-web --destination-port-ranges 80 az network vnet subnet update -g $RG --vnet-name vnet-spoke -n snet-web --network-security-group nsg-web NIC=$(az vm show -g $RG -n vm-web1 --query "networkProfile.networkInterfaces[0].id" -o tsv) az network nic list-effective-nsg --ids $NIC --query "value[0].effectiveSecurityRules[].{name:name, prio:priority, access:access, dir:direction}" -o table -
Task
Build the public load balancer
Create a Standard public load balancer
lbe-webthat:- uses the existing public IP
pip-lbe-webas frontend (fe-web); - has both web VMs in backend pool
be-web; - checks health with an HTTP probe on port 80, path
/(hp-http); - forwards TCP 80 to port 80 on the backends (
lbr-http); - gives the backends outbound internet through an outbound rule (
obr-web) instead of implicit SNAT.
Done whenhttp://<pip-lbe-web address>in your browser shows "Hello from vm-web1" or "vm-web2". Refresh a few times, or usecurlrepeatedly, to see both.- If you did the optional DNS task in chapter 3:
http://web.lab.yourdomain.comworks too.
Hint
A browser often reuses its connection and keeps hitting the same backend.
for i in $(seq 6); do curl -s http://IP; doneopens a new connection each time.Solution Β· Portal
- Create a resource β Load balancer: name
lbe-web, SKU Standard, type Public, tier Regional. - Frontend IP configuration β Add: name
fe-web, public IP use existingpip-lbe-web. - Backend pools β Add:
be-web, VNetvnet-spoke, configuration NIC, add both VMs. - Inbound rules β Add a load balancing rule:
lbr-http, frontendfe-web, poolbe-web, TCP, port 80, backend port 80, Create new health probehp-http(HTTP, port 80, path/), session persistence None, Outbound SNAT: (Recommended) Use outbound rulesβ¦. - Outbound rules β Add:
obr-web, frontendfe-web, protocol All, poolbe-web, port allocation default. - Review + create, wait a minute, then test.
Solution Β· Azure CLI
az network lb create -g $RG -n lbe-web -l $LOC --sku Standard \ --frontend-ip-name fe-web --public-ip-address pip-lbe-web --backend-pool-name be-web az network lb probe create -g $RG --lb-name lbe-web -n hp-http --protocol Http --port 80 --path / az network lb rule create -g $RG --lb-name lbe-web -n lbr-http --protocol Tcp \ --frontend-port 80 --backend-port 80 --frontend-ip-name fe-web --backend-pool-name be-web \ --probe-name hp-http --disable-outbound-snat true az network lb outbound-rule create -g $RG --lb-name lbe-web -n obr-web --protocol All \ --frontend-ip-configs fe-web --address-pool be-web for VM in vm-web1 vm-web2; do NICNAME=$(basename $(az vm show -g $RG -n $VM --query "networkProfile.networkInterfaces[0].id" -o tsv)) IPCFG=$(az network nic show -g $RG -n $NICNAME --query "ipConfigurations[0].name" -o tsv) az network nic ip-config address-pool add -g $RG --nic-name $NICNAME --ip-config-name $IPCFG \ --lb-name lbe-web --address-pool be-web done PIP=$(az network public-ip show -g $RG -n pip-lbe-web --query ipAddress -o tsv) for i in $(seq 6); do curl -s http://$PIP; done - uses the existing public IP
-
Task
Prove where outbound traffic comes from
Find out which public IP address
vm-web1uses when it browses the internet. Explain why.Done when- The address you see is the address of
pip-lbe-web.
Solution Β· Azure CLI
az vm run-command invoke -g $RG -n vm-web1 --command-id RunShellScript \ --scripts "curl -s https://api.ipify.org" --query "value[0].message" -o tsvThe outbound rule is an explicit outbound method, so it takes precedence over default outbound access. From now on the VMs would still have internet even in a private subnet.
- The address you see is the address of
-
Task
Log in with Bastion
Open an SSH session in your browser to
vm-web1using Azure Bastion, with the free Developer SKU. The VM must keep having no public IP.Done when- You're at a
azureuser@vm-web1prompt in a browser tab. - You can explain why Developer can't reach a VM in
vnet-hub.
Solution Β· Portal
vm-web1β Connect β Connect via Bastion β Deploy Bastion Developer (it attaches tovnet-spoke; takes a few minutes).- Enter
azureuserand your password (or private key) β Connect.
Developer works for one VNet only, without peering. A hub VM would need Bastion Basic or higher.
If Developer isn't offered in your region: add subnet
AzureBastionSubnet10.20.254.0/26tovnet-spokeand deploy Basic. It costs about β¬0.20/hour, so delete it after this chapter. - You're at a
-
Task
Break the load balancer, then fix it
In your Bastion session, stop nginx on
vm-web1. Then find out, from the load balancer, what it thinks about each backend, and check what clients now get. Start nginx again afterwards.Done when- All
curlrequests return "Hello from vm-web2" while nginx is stopped. - You found the probe status in the load balancer's metrics or Insights.
- Both backends serve again.
Solution Β· Portal
- In Bastion:
sudo systemctl stop nginx. lbe-webβ Monitoring β Metrics β metric Health Probe Status, Apply splitting by Backend IP Address:10.20.1.4drops to 0. (Or Monitoring β Insights.)- From your Linux VM:
for i in $(seq 6); do curl -s http://$PIP; done. - In Bastion:
sudo systemctl start nginx.
Solution Β· Azure CLI
PIP=$(az network public-ip show -g $RG -n pip-lbe-web --query ipAddress -o tsv) az vm run-command invoke -g $RG -n vm-web1 --command-id RunShellScript --scripts "systemctl stop nginx" sleep 30; for i in $(seq 6); do curl -s http://$PIP; done az vm run-command invoke -g $RG -n vm-web1 --command-id RunShellScript --scripts "systemctl start nginx" - All
-
Task
Prove that SSH from the internet is blocked
Without trying to connect, find out whether an SSH packet (TCP 22) from internet address
198.51.100.10tovm-web1would be allowed, and which rule decides.Done when- You have the answer Deny and the rule name DenyAllInBound.
Hint
Network Watcher has a tool that tests exactly one packet.
Solution Β· Portal
Network Watcher β Network diagnostic tools β IP flow verify: VM
vm-web1, protocol TCP, direction Inbound, local port 22, remote IP198.51.100.10, remote port 60000 β Access denied, ruleDenyAllInBound. Try again with port 80: allowed byallow-http-internet.Solution Β· Azure CLI
IP1=$(az vm show -g $RG -n vm-web1 -d --query privateIps -o tsv) az network watcher test-ip-flow -g $RG --vm vm-web1 --direction Inbound --protocol TCP \ --local $IP1:22 --remote 198.51.100.10:60000 az network watcher test-ip-flow -g $RG --vm vm-web1 --direction Inbound --protocol TCP \ --local $IP1:80 --remote 198.51.100.10:60000
The Standard load balancer costs about β¬0.025/hour plus data. Bastion Developer is free. Deallocate the VMs when you stop for the day; the load balancer can stay.
nsg-webonsnet-weballows only HTTP from the internet toasg-web.lbe-webserves both VMs onhttp://<pip-lbe-web>and provides their outbound internet.- Bastion Developer on
vnet-spokefor admin access.