Securing a Private VM with Azure Firewall, Bastion, NSG, and NAT Gateway

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

  1. Virtual Network

    Purpose: Network boundary and subnets

  2. Azure Firewall

    Purpose: Public entry point and traffic filtering

  3. Firewall Policy

    Purpose: Centralized rules

  4. Azure Bastion

    Purpose: Secure browser-based SSH

  5. NAT Gateway

    Purpose: Outbound-only internet access

  6. Public 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

  1. Layer your defenses. Firewalls and NSG enforce policy at different levels.
  2. Private doesn't mean unreachable. Publish services through a controlled gateway.
  3. Eliminate open SSH. Bastion removes a common attack vector.
  4. Separate inbound and outbound paths. The firewall handles inbound; the NAT Gateway handles outbound.
  5. 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.

Story originally reported by Dev.to. View at Dev.to →
← Back to all news