AZ-104 Refresher Chapter 6 Β· Secure access & load balancing

Chapter 6

Secure access & load balancing

πŸ“– Theory 35 minπŸ›  Lab 25 minVirtual networking Β· 15–20%

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.

Part A

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

Default rules

Every NSG has six default rules that you can't delete, only override with a lower priority number:

PriorityInboundOutbound
65000AllowVnetInBound: VirtualNetwork β†’ VirtualNetworkAllowVnetOutBound
65001AllowAzureLoadBalancerInBound: health probesAllowInternetOutBound
65500DenyAllInBoundDenyAllOutBound

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:

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:

Inbound Internet β‘  subnet NSGnsg-web on snet-web β‘‘ NIC NSG(none in our lab) VM Outbound VM β‘  NIC NSG β‘‘ subnet NSG Destination A Deny in either NSG drops the packet. Traffic between VMs in the same subnet is also filtered.
Both NSGs must allow the traffic. Using only a subnet NSG (as in this lab) is simpler to manage.

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:

  1. Pick the right direction and the right NSG(s) (subnet, NIC, or both).
  2. Go through the rules from the lowest priority number up. The first rule that matches source, destination, port and protocol decides.
  3. 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).

Exam trap

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.

SKUKey points
DeveloperFree, shared infrastructure, no dedicated subnet, one VNet only (no peered VNets), portal only, one session at a time.
BasicDedicated hosts in AzureBastionSubnet (/26 minimum) with a Standard public IP. Reaches VMs in peered VNets.
StandardAdds native client support (az network bastion ssh/rdp), host scaling, IP-based connection (e.g. on-prem servers via VPN), file transfer, shareable links.
PremiumAdds 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.

Exam trap

"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.

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

Clientsinternet lbe-web (Standard, public) Frontend IPpip-lbe-webzone-redundant Load-balancing ruleTCP 80 β†’ 80 Β· session persistence Inbound NAT rulee.g. 50001 β†’ vm-web1:22 Outbound ruleSNAT for backend β†’ internet Health probe Β· HTTP 80 / Backend poolbe-web vm-web1healthy βœ“ vm-web2healthy βœ“
The frontend receives traffic, rules decide where it goes, the probe decides which backends are allowed to receive it.

Distribution and special options

Which load balancing service?

ServiceLayerScopeChoose it when
Load Balancer4 (TCP/UDP)Regional (or cross-region)Any TCP/UDP app, internal tiers, NVAs, very high throughput
Application Gateway7 (HTTP/S)RegionalURL path routing, TLS termination, WAF, cookie affinity
Front Door7 (HTTP/S)GlobalGlobal web apps, CDN caching, WAF at the edge
Traffic ManagerDNSGlobalDNS-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:

  1. Health probe status (LB β†’ Monitoring β†’ Metrics β†’ Health Probe Status, or Insights). Are the backends healthy?
  2. Is the app listening on the probe port and returning 200 on the probe path?
  3. Does an NSG block the probe? The default rule AllowAzureLoadBalancerInBound allows it, but a custom Deny all with a lower number would block it.
  4. Does an NSG allow the client traffic? Standard LB needs an explicit allow.
  5. 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).
AZ-305 link

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.)

Part B

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
  1. Task

    Label the web servers

    Create an application security group asg-web and 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
    1. Create a resource β†’ Application security group: name asg-web, your region.
    2. vm-web1 β†’ Networking β†’ Network settings (or Application security groups) β†’ Add application security groups β†’ asg-web. Repeat for vm-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
  2. 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 of asg-web, and protect the whole snet-web subnet with it. Don't open SSH.

    Done when
    • nsg-web is associated with snet-web and 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
    1. nsg-web β†’ Settings β†’ Inbound security rules β†’ Add: Source Service Tag Internet, source ports *, Destination Application security group asg-web, service HTTP / port 80, TCP, Allow, priority 100, name allow-http-internet.
    2. Settings β†’ Subnets β†’ Associate: vnet-spoke / snet-web.
    3. 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
  3. Task

    Build the public load balancer

    Create a Standard public load balancer lbe-web that:

    • uses the existing public IP pip-lbe-web as 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 when
    • http://<pip-lbe-web address> in your browser shows "Hello from vm-web1" or "vm-web2". Refresh a few times, or use curl repeatedly, to see both.
    • If you did the optional DNS task in chapter 3: http://web.lab.yourdomain.com works too.
    Hint

    A browser often reuses its connection and keeps hitting the same backend. for i in $(seq 6); do curl -s http://IP; done opens a new connection each time.

    Solution Β· Portal
    1. Create a resource β†’ Load balancer: name lbe-web, SKU Standard, type Public, tier Regional.
    2. Frontend IP configuration β†’ Add: name fe-web, public IP use existing pip-lbe-web.
    3. Backend pools β†’ Add: be-web, VNet vnet-spoke, configuration NIC, add both VMs.
    4. Inbound rules β†’ Add a load balancing rule: lbr-http, frontend fe-web, pool be-web, TCP, port 80, backend port 80, Create new health probe hp-http (HTTP, port 80, path /), session persistence None, Outbound SNAT: (Recommended) Use outbound rules….
    5. Outbound rules β†’ Add: obr-web, frontend fe-web, protocol All, pool be-web, port allocation default.
    6. 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
  4. Task

    Prove where outbound traffic comes from

    Find out which public IP address vm-web1 uses 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 tsv

    The 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.

  5. Task

    Log in with Bastion

    Open an SSH session in your browser to vm-web1 using Azure Bastion, with the free Developer SKU. The VM must keep having no public IP.

    Done when
    • You're at a azureuser@vm-web1 prompt in a browser tab.
    • You can explain why Developer can't reach a VM in vnet-hub.
    Solution Β· Portal
    1. vm-web1 β†’ Connect β†’ Connect via Bastion β†’ Deploy Bastion Developer (it attaches to vnet-spoke; takes a few minutes).
    2. Enter azureuser and 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 AzureBastionSubnet 10.20.254.0/26 to vnet-spoke and deploy Basic. It costs about €0.20/hour, so delete it after this chapter.

  6. 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 curl requests 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
    1. In Bastion: sudo systemctl stop nginx.
    2. lbe-web β†’ Monitoring β†’ Metrics β†’ metric Health Probe Status, Apply splitting by Backend IP Address: 10.20.1.4 drops to 0. (Or Monitoring β†’ Insights.)
    3. From your Linux VM: for i in $(seq 6); do curl -s http://$PIP; done.
    4. 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"
  7. 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.10 to vm-web1 would 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 IP 198.51.100.10, remote port 60000 β†’ Access denied, rule DenyAllInBound. Try again with port 80: allowed by allow-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
Cost

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.

Checkpoint
  • nsg-web on snet-web allows only HTTP from the internet to asg-web.
  • lbe-web serves both VMs on http://<pip-lbe-web> and provides their outbound internet.
  • Bastion Developer on vnet-spoke for admin access.