Introduction
Exposing a VM directly to the internet is one of the easiest ways to widen your attack surface. In this lab, I built an architecture where a web server has no public IP yet is still reachable through a controlled entry point: Azure Firewall.
Architecture overview
The flow, as shown in my diagram:
Users resolve DNS to the Azure Firewall's public IP.
Azure Firewall filters traffic and forwards it into the virtual network.
The NSG on the subnet allows port 80 and the Bastion subnet, and denies everything else.
The private VM has no public IP and serves the site.
Admins use Azure Bastion for SSH.
NAT Gateway handles outbound internet access.
Step 1: Deploy the network security stack.
I deployed everything into one resource group (FW-rg, Central US). The deployment created:
Resources
Virtual Network
Purpose: Network boundary and subnetsAzure Firewall
Purpose: Public entry point and traffic filteringFirewall Policy
Purpose: Centralized rulesAzure Bastion
Purpose: Secure browser-based SSHNAT Gateway
Purpose: Outbound-only internet accessPublic IPS Firewall
Purpose: Firewall management and Bastion
The deployment page showed Firewall and Bastion still provisioning while the network, NAT gateway, policy, and IPs were already OK. Firewall and Bastion take the longest, so be patient.
Step 2: Create the private VM
I created an Ubuntu VM (FW-VM) with no public IP, placed in the workload subnet protected by the NSG.
Step 3: Connect through Bastion
Instead of opening port 22 to the internet, I connected from the portal using Bastion. The login banner showed the source was 10.0.1.4, an internal address from the Bastion subnet. The admin never touches the VM over the public internet.
Step 4: Install a web server
bash
sudo apt update
sudo apt install apache2 -y
sudo vim /var/www/html/index.html
(My first attempt was sudo pt update, which failed with "command not found." Typos happen!)
The install output showed Apache enabled and running. I replaced the default Ubuntu page with a simple heading to prove the traffic path works.
Step 5: Configure the NSG
The NSG rules follow least privilege:
Allow TCP 80 for web traffic
Allow the Bastion subnet for administration
Deny all other traffic
Step 6: Test
Browsing to the firewall's public IP returned my custom page, served from a VM that has no public IP of its own. The firewall is the only door.
Key lessons
- Layer your defenses. Firewalls and NSG enforce policy at different levels.
- Private doesn't mean unreachable. Publish services through a controlled gateway.
- Eliminate open SSH. Bastion removes a common attack vector.
- Separate inbound and outbound paths. The firewall handles inbound; the NAT Gateway handles outbound.
- Deny by default.
What I want to explore next
Add explicit DNAT and network rules in the firewall policy.
Enable diagnostic logs and query them in Log Analytics.
Reproduce the setup with Bicep or Terraform
Conclusion
This lab showed me that good cloud security is mostly about reducing exposure and routing everything through well-defined chokepoints. If you're learning Azure networking, build this one yourself.







Top comments (0)