Non-Prod Network Security — Current State
This document captures the actual deployed networking and security posture for the non-production estate (rg-eshars-nonprod and related RGs). It covers what is in place, what is intentionally deferred, and what are known gaps.
Network Diagram
flowchart TB
Internet(["🌐 Internet"])
VPNUser(["👤 VPN User\nEntra ID auth"])
OpsUser(["👤 Ops / Portal User"])
subgraph vpngw_vnet["vnet-vpngw-nonprod (10.9.0.0/26)"]
direction TB
GWSubnet["GatewaySubnet\n10.9.0.0/27"]
PESubnet2["sn-pe-nonprod\n10.9.0.32/27"]
VPNGW["vpngw-eshars-nonprod\nVpnGw1 · OpenVPN · Entra ID\nClient pool: 172.16.1.0/24"]
GWSubnet --> VPNGW
end
subgraph nonprod_vnet["rg-eshars-nonprod VNet (10.1.0.0/16)"]
direction TB
subgraph bastion_snet["AzureBastionSubnet (10.1.1.0/26) — retained, no Bastion deployed"]
end
subgraph agw_snet["sn-agw-eshars-np (10.1.2.0/24)"]
AGW["agw-eshars-np\nStandard_v2\n⚠️ No WAF"]
end
subgraph apps_snet["sn-webapp-eshars-np (10.1.3.0/24)"]
AppSvc["App Service VNet Integration\napp-eshars-nonprod-cus"]
end
subgraph pe_snet["sn-pe-sql-001 (10.1.5.0/28)"]
PE["Private Endpoints"]
end
subgraph default_snet["default subnet (10.1.0.0/24)"]
direction LR
SSRS["vm-sql16-np\n10.1.0.4\nSSRS nonprod"]
SSRSTrain["vm-sql16-1-np\n10.1.0.6\nSSRS training\n(rg-eshars-nonprod-centralus)"]
JobSched["vm-jobscheduler-np-001\n10.1.0.10\nBatch/import jobs"]
CleanupJobs["vm-cleanup-jobs\n10.1.0.9\nScheduled jobs"]
OpsVM["vm-ops-nonprod\n10.1.0.12\nOps + build agent"]
DbApi["vm-db-api\n10.1.0.8\nSQL jump box\n🗑️ To deprecate"]
SuccVM["vm-successed-nonprod\n10.1.0.5\n⚠️ Purpose unknown"]
end
end
VPNUser -->|"OpenVPN\nEntra ID"| VPNGW
VPNGW -->|"172.16.1.x → 10.1.0.x\nvia peering"| default_snet
Internet -->|"HTTPS :443\nHTTP :80"| AGW
AGW -->|"10.1.0.4 :80/443"| SSRS
AGW -->|"10.1.0.6 :80/443"| SSRSTrain
AGW --> AppSvc
vpngw_vnet <-->|"VNet Peering\nUseRemoteGateways=true"| nonprod_vnet
VNet Topology
Two VNets are peered together to form the non-prod network:
rg-eshars-nonprod (10.1.0.0/16) vnet-vpngw-nonprod (10.9.0.0/26)southcentralus southcentralus───────────────────────────── ──────────────────────────────AzureBastionSubnet 10.1.1.0/26 GatewaySubnet 10.9.0.0/27sn-agw-eshars-np 10.1.2.0/24 ◄─── sn-pe-nonprod 10.9.0.32/27sn-webapp-eshars-np 10.1.3.0/24 peering (UseRemoteGateways=true)sn-dnsresolver-nonprod 10.1.4.0/28sn-pe-sql-001 10.1.5.0/28default 10.1.0.0/24The rg-eshars-nonprod VNet uses UseRemoteGateways=true on the peering, so all VMs in the non-prod VNet route through the VPN gateway in vnet-vpngw-nonprod.
Access Controls
VPN Gateway
| Property | Value |
|---|---|
| Name | vpngw-eshars-nonprod |
| Resource Group | rg-eshars-nonprod |
| SKU | VpnGw1 |
| Type | RouteBased |
| Protocol | OpenVPN |
| Auth | Azure AD (Entra ID) |
| Client address pool | 172.16.1.0/24 |
| Active-active | No |
| BGP | No |
VPN clients authenticate with their Entra ID credentials. Once connected, they receive an IP in 172.16.1.0/24 and can reach VMs in the default subnet (10.1.0.0/24) via the peering.
Azure Bastion
Management Access Summary
VMs have no public IP addresses. Access is via VPN only:
- VPN — OpenVPN (Entra ID) →
172.16.1.0/24client pool → reaches VMs at10.1.0.x
Virtual Machines
All non-prod VMs sit in the default subnet (10.1.0.0/24), spread across two resource groups:
| VM | IP | Size | RG | Role |
|---|---|---|---|---|
vm-sql16-np | 10.1.0.4 | Standard_D4s_v3 | rg-eshars-nonprod | SSRS nonprod (SQL Express + SSRS 2022) |
vm-sql16-1-np | 10.1.0.6 | — | rg-eshars-nonprod-centralus | SSRS training (same VNet, different RG) |
vm-cleanup-jobs | 10.1.0.9 | Standard_D2s_v5 | rg-eshars-nonprod | Scheduled/timer jobs |
vm-jobscheduler-np-001 | 10.1.0.10 | Standard_DS3_v2 | rg-eshars-nonprod | Import/upload/batch jobs |
vm-ops-nonprod | 10.1.0.12 | Standard_D2s_v3 | rg-eshars-nonprod | Ops machine + Azure DevOps build agent |
vm-db-api | 10.1.0.8 | Standard_B2ms | rg-eshars-nonprod | SQL Server jump box — to be deprecated (devs should use Entra ID direct access) |
vm-successed-nonprod | 10.1.0.5 | Standard_B2ms | rg-eshars-nonprod | Purpose unknown — candidate for decommission |
App Gateways
agw-eshars-np — SSRS/Nonprod
| Property | Value |
|---|---|
| Tier | Standard_v2 (no WAF) |
| Location | centralus |
| State | Running |
Backend pools:
| Pool | Target | Used for |
|---|---|---|
appGatewayBackendPool | 10.1.0.4 | Nonprod SSRS (vm-sql16-np) |
bepool-ssrs-training | 10.1.0.6 | Training SSRS (vm-sql16-1-np) |
app-eshars-nonprod-cus | app-eshars-nonprod-cus.azurewebsites.net | Nonprod web app |
Listeners: nonprodreports (HTTPS, no host filter), listener-ssrs-training (HTTPS, trainingreports.myeshars.com)
**Migration path**: When SSRS migrates to the new `vnet-eshars-core` shared App Gateway (WAF_v2, Phase 3), `agw-eshars-np` will be decommissioned.ag-cus-np — Dev/Test (to be decommissioned)
| Property | Value |
|---|---|
| Tier | WAF_v2 |
| WAF Policy | ag-waf-policy |
| Backend | wa-hellowrld-test-... (test app) |
This gateway was used to test the dev web app with the new shared gateway pattern. To be decommissioned by end of week once dev site validation is complete.
Network Security Groups
SSRS VM NSG (vm-sql16-np)
| Rule | Priority | Direction | Access | Protocol | Src | Dst | Port |
|---|---|---|---|---|---|---|---|
| AllowBastionRDP | 1000 | Inbound | Allow | TCP | AzureBastionSubnet | * | * |
| AllowVpnRDP | 1001 | Inbound | Allow | TCP | 172.16.1.0/24 | 10.1.0.4 | 3389 |
| reportserverurl | 1509 | Inbound | Allow | TCP | * | * | 443 |
| report-server-url | 1510 | Inbound | Allow | TCP | * | * | 80 |
**Fix**: Update both rules to set `sourceAddressPrefix = 10.1.2.0/24`.Bastion NSG (nsg-bastion-nonprod)
Standard Microsoft-recommended Bastion NSG rules — correctly configured:
| Direction | Rule | Port | Source |
|---|---|---|---|
| Inbound | AllowHttpsInbound | 443 | Internet |
| Inbound | AllowGatewayManagerInbound | 443 | GatewayManager |
| Inbound | AllowAzureLoadBalancerInbound | 443 | AzureLoadBalancer |
| Inbound | AllowBastionHostCommunication | * | VirtualNetwork |
| Outbound | AllowSshRdpOutbound | * | * → VirtualNetwork |
| Outbound | AllowAzureCloudOutbound | 443 | * → AzureCloud |
| Outbound | AllowBastionCommunication | * | * → VirtualNetwork |
| Outbound | AllowHttpOutbound | 80 | * → Internet |
Private Endpoints
The subnet sn-pe-sql-001 (10.1.5.0/28) exists for private endpoints. The VPN gateway VNet also has sn-pe-nonprod (10.9.0.32/27).
Private endpoints in use: to be fully inventoried.
What’s in Place — Summary
| Control | Status |
|---|---|
| All VMs have private IPs only | ✅ |
| VPN access via Azure AD (Entra) | ✅ |
| Azure Bastion | ⛔ Decommissioned 2026-04-17 — VPN is sole access path |
| AzureBastionSubnet retained for recreation | ✅ |
| SSRS behind App Gateway | ✅ |
| Per-VM NSGs | ✅ (all VMs have individual NSGs) |
Known Gaps
| Gap | Severity | Fix |
|---|---|---|
SSRS NSG (vm-sql16-np) allows 80/443 from * | Medium | Restrict to App GW subnet 10.1.2.0/24 |
agw-eshars-np has no WAF | Low (intentional for non-prod) | Accept until SSRS migrates to shared WAF_v2 gateway |
vm-db-api — SQL jump box, devs connecting via shared VM | Medium | Deprecate: enable Entra ID auth on SQL Servers, have devs connect directly via VPN |
vm-successed-nonprod — purpose unknown | Low | Investigate, document or decommission |
vm-sql16-1-np in wrong RG | Low | Move to rg-eshars-nonprod during RG reorg |
Flat default subnet — no VM-level segmentation | Low | Future: segment by workload type as estate grows |
| VPN gateway not in IaC | Medium | Add vpngw-eshars-nonprod to eshars-ops Terraform |
ag-cus-np (WAF_v2) still running | Low | Decommission after dev site validation |
Planned Improvements
See Shared Core Architecture for the full migration plan. Relevant phases for non-prod:
- Phase 3 — Move SSRS VM into new
vnet-eshars-coreshared VNet, behind the shared WAF_v2 App Gateway. Decommissionagw-eshars-np. - Phase 4 — Investigate and decommission unknown VMs (
vm-db-api,vm-successed-nonprod). - RG Reorg — Consolidate
rg-eshars-nonprod-centralusVMs intorg-eshars-nonprod.