This page covers Microsoft Azure network hardening across the surfaces that decide whether an attacker who reaches the network edge can pivot inward, exfiltrate data, or sustain disruption. Scope is the Azure commercial regions; Azure Government and Azure operated by 21Vianet (China) inherit the same controls but expose a different region table, a different sovereign endpoint suffix, and a slightly different Microsoft Entra ID (formerly Azure Active Directory) tenant topology — re-verify region-table caveats and the Microsoft Graph endpoint before applying any of the IaC below to a non-commercial cloud. CIS sub-IDs and NIST / ISO mappings throughout this page reference the commercial Microsoft Azure Foundations Benchmark v3.0.0 (Feb 2025) unless explicitly annotated as a post-v3.0.0 feature or a best-practice recommendation that the current benchmark has not yet codified. The crosswalk page at compliance frameworks describes how the seven pinned framework columns relate to each other.
The Azure network model is the product of subscriptions (the billing and policy boundary), virtual networks (regional, RFC1918-addressed address spaces with their own DNS context), subnets (per-tier CIDR slices that anchor an NSG and a route table), Network Security Groups (stateful L4 ACLs evaluated at the NIC and at the subnet), route tables (user-defined routes that override Azure system routing — typically pointing east-west and egress traffic through a hub-and-spoke design at an Azure Firewall), Private Endpoints (private IPs inside a subnet that front PaaS services and remove the public attack surface entirely), Service Endpoints (legacy: traffic stays on the Azure backbone but the PaaS service still has a public IP), Azure Firewall (stateful L4/L7 NVA with FQDN filtering, IDPS and TLS inspection in the Premium tier), and edge primitives Azure Front Door (global L7 ingress with managed WAF) and Application Gateway (regional L7 ingress, the in-VNet alternative). The cross-cutting principles — segmentation, zero trust, egress control, encryption in transit, and DNS security — are owned by the General Network page; this page maps them to Azure primitives. Severity is assigned from the methodology severity rubric; equivalence callouts at the bottom of each control point at the matching control on the AWS, GCP, and OCI sibling pages.
Three anti-conflation callouts up front, because each pair gets conflated in audit reports and architecture reviews and the distinction matters for control design. First: NSGs, Azure Firewall, and Private Endpoints are complementary, not alternatives. NSGs are L4 ACLs at NIC/subnet scope (covered as azure-net-02); Azure Firewall is stateful L4/L7 with FQDN filtering, IDPS, and TLS inspection (covered as azure-net-08); Private Endpoints remove the public-IP attack surface from PaaS services entirely (covered as azure-net-03 and azure-net-04). Each addresses a different scope; reviewers who insist on "pick one" are wrong. Second: Azure Front Door WAF and Azure DDoS Protection Standard are complementary, not alternatives. Front Door WAF is a global, edge-deployed L7 filter (covered as azure-net-05); DDoS Protection Standard is a per-VNet L3/L4 volumetric-attack mitigation tier (covered as azure-net-06). One filters application-layer abuse; the other absorbs network-layer flooding. Third: Azure Front Door WAF and Application Gateway WAF answer different deployment questions. Front Door is global and runs at the Microsoft edge; Application Gateway WAF is regional and runs inside a VNet — the right choice for in-VNet backends that cannot front via the public edge (mTLS-required APIs, internal-only landing pages). Application Gateway WAF is referenced in prose where relevant; it is not a separate control because it does not add an attack-surface concept beyond what Front Door WAF already covers.
Order and scope matter. Controls 01–04 are foundational invariants enforced subscription-wide via Azure Policy assigned at the root management group: have no reliance on default networking, lock admin ports against the Internet service tag, disable public network access on regulated PaaS, and front it with Private Endpoints. Control 05 protects the L7 edge of public web traffic; control 06 absorbs L3/L4 volumetric attacks; control 07 signs the organisation's public DNS zones; control 08 closes the egress loop with stateful FQDN-filtering and default-deny — the missing complement to Private Endpoints, which only covers Azure-service traffic. Subscription and management-group scope: Azure Policy at the root management group enforces tenant-wide invariants (allowed locations, denied resource types, required-tag policies, regulatory compliance initiatives) and is the single most important lever for keeping the controls below from drifting out of compliance once dozens of subscriptions and hundreds of resource groups exist.
azure-net-01-default-vnet!MEDIUMPREVENTIVE
Azure does not provision a "default VNet" the way AWS provisions a default VPC, but the equivalent failure mode is real: workload teams creating ad-hoc VNets outside the approved IP-address plan, with overlapping CIDRs, no peering to the hub, and no NSG on the subnet at all. Enforce an explicit per-workload VNet design (hub-and-spoke or Azure Virtual WAN) via Azure Policy assigned at the root management group, deny VNet creation outside the approved IP plan, and require NSG attachment on every subnet at creation time (Microsoft Learn — Azure Virtual Network overview (accessed 2026-05)). The principle is reinforced in General Network — segmentation: a network the organisation did not consciously design is a network whose blast radius the organisation cannot reason about.
Remediation — Azure CLI
# Azure CLI 2.x
# Inventory: list every VNet in every subscription of the tenant.
for sub in $(az account list --query '[].id' -o tsv); do
az network vnet list --subscription "$sub" \
--query "[].{sub:'$sub', name:name, rg:resourceGroup, prefixes:addressSpace.addressPrefixes}" \
-o tsv
done
# Per workload: create an explicit VNet inside the approved IP plan.
az network vnet create \
--resource-group rg-net-prod-westeu \
--name vnet-app-prod-westeu \
--address-prefixes 10.40.0.0/16 \
--location westeurope
# Assign the built-in "Allowed locations" + a custom "denied address-space" policy
# at the root management group so future VNets cannot drift.
az policy assignment create \
--name pa-network-ip-plan \
--scope "/providers/Microsoft.Management/managementGroups/tenant-root" \
--policy-set-definition "/providers/Microsoft.Authorization/policySetDefinitions/<ip-plan-initiative-id>"
AzureActivity Microsoft.Network/virtualNetworks/write creating a VNet whose address space overlaps the documented hub/spoke plan or that lacks the standard environment + owner tags — accidental shadow VNet outside the landing-zone topology.
AzureActivity Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write establishing peering across subscriptions or to a VNet owned by a different management group — bypasses the documented hub interconnect.
AzureActivity Microsoft.Network/virtualNetworks/subnets/write creating a subnet without an attached networkSecurityGroup or routeTable reference — defeats the landing-zone enforcement baseline.
Query
AzureActivity
| where OperationNameValue in ("Microsoft.Network/virtualNetworks/write", "Microsoft.Network/virtualNetworks/subnets/write", "Microsoft.Network/virtualNetworks/virtualNetworkPeerings/write")
| extend body = tostring(parse_json(Properties).requestbody)
| project TimeGenerated, Caller, OperationNameValue, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. Pair with an Azure Resource Graph daily reconciliation to catch shadow VNets that bypass the resource-provider event surface; persist as a Sentinel analytics rule with Medium severity.
Alert threshold
VNet creation in a non-landing-zone subscription — page on first occurrence.
Cross-management-group peering — page; treat as escape-from-policy-boundary event.
Initial response
Hold the VNet via tag quarantine=true and disable peering on the new resource via the IaC pipeline; capture the AzureActivity record as the rollback ledger.
Walk recent AzureActivity for the Caller — shadow VNet creation often co-occurs with role-assignment expansion or with policy-exemption application.
Escalate per general/ir.html — confirm the Azure Policy Audit virtual networks created outside hub-spoke topology remains in deny mode at the management-group root.
No Network Security Group in any subscription may permit ingress from the Internet service tag (or 0.0.0.0/0) on administrative ports — SSH 22, RDP 3389, SQL 1433, PostgreSQL 5432, MySQL 3306, MongoDB 27017, Redis 6379, and any other database or management port the organisation uses — at either NIC scope or subnet scope (Microsoft Learn — Network Security Groups overview (accessed 2026-05)). NSGs are stateful L4 firewalls evaluated twice on the packet path (once at the subnet NSG and once at the NIC NSG), default-deny on ingress from the internet, and the most directly enforceable per-resource boundary Azure exposes at the network layer. This is the canonical "open the internet to my database" misconfiguration; Shodan-style scanners locate exposures within minutes. Anti-conflation: NSGs are L4 only — they do not do FQDN filtering, IDPS, or TLS inspection. Those live on Azure Firewall Premium (azure-net-08), which is centralised at the hub and complements NSGs rather than replacing them. Use the Internet service tag rather than literal 0.0.0.0/0 where possible; the tag is operationally clearer and survives address-prefix expansions.
Remediation — Azure CLI
# Azure CLI 2.x
# Audit: enumerate every NSG rule allowing Internet -> admin ports across the tenant.
for sub in $(az account list --query '[].id' -o tsv); do
az network nsg list --subscription "$sub" --query '[].id' -o tsv | while read nsg_id; do
az network nsg rule list --ids "$nsg_id" \
--query "[?direction=='Inbound' && access=='Allow' && (sourceAddressPrefix=='Internet' || sourceAddressPrefix=='*' || sourceAddressPrefix=='0.0.0.0/0')].{nsg:'$nsg_id', name:name, ports:destinationPortRanges}" \
-o tsv
done
done
# Apply the canonical deny rule at NIC + subnet scope.
az network nsg rule create \
--resource-group rg-net-prod-westeu \
--nsg-name nsg-app-subnet \
--name DenyInternetToSshRdpSql \
--priority 100 \
--direction Inbound \
--access Deny \
--protocol Tcp \
--source-address-prefixes Internet \
--destination-port-ranges 22 3389 1433 5432 3306 27017 6379
# Continuous enforcement: built-in CIS Microsoft Azure Foundations Benchmark v3.0.0
# initiative assigned at the root management group covers 6.1 and 6.2.
AzureActivity Microsoft.Network/networkSecurityGroups/securityRules/write where the request body resolves to access = Allow, direction = Inbound, sourceAddressPrefix in ("*", "0.0.0.0/0", "Internet") and destinationPortRange covers TCP 22, 3389, or 5985-5986.
AzureActivity write events on NSG rules whose priority is moved into the 100-200 range where the deny baselines previously occupied — silent override.
NetworkSecurityGroupFlowEvent inbound connections on admin ports from public-internet CIDRs that match the rule edit — downstream confirmation the change took effect.
Query
AzureActivity
| where OperationNameValue == "Microsoft.Network/networkSecurityGroups/securityRules/write"
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "\"access\":\"Allow\"" and body has "\"direction\":\"Inbound\""
| where body has "\"sourceAddressPrefix\":\"*\"" or body has "\"sourceAddressPrefix\":\"0.0.0.0/0\"" or body has "\"sourceAddressPrefix\":\"Internet\""
| where body has "\"22\"" or body has "\"3389\"" or body has "\"5985\"" or body has "\"5986\"" or body has "\"22-22\""
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. The internet-exposed-admin-port pattern is the single highest-fidelity NSG misconfiguration signal; persist as a Sentinel analytics rule with severity High and pair with an automation playbook that disables the rule on detection.
Alert threshold
Any inbound Allow rule on TCP 22/3389/5985/5986 from * or Internet — page on first occurrence.
NSG rule priority insertion below the existing deny baseline — page; the rule may not yet match traffic but the precedence regression has occurred.
Initial response
Delete the offending rule via az network nsg rule delete or apply the IaC baseline; capture the AzureActivity Caller and the rule body as the rollback ledger.
Pull NetworkSecurityGroupFlowEvent for the exposure window from each NIC bound to the NSG — every inbound flow on the admin port from internet CIDRs should be reconciled with documented administrator access.
Escalate per general/ir.html — confirm the Azure Policy Internet-facing virtual machines should be protected with NSGs remains in deny mode and that Defender for Cloud has refreshed its assessment.
Regulated PaaS services — Storage Accounts, Key Vault, Azure SQL, Cosmos DB, App Configuration, Container Registry — must have public_network_access_enabled = false at the resource level, default network rules set to Deny, and access strictly through Private Endpoints (covered as azure-net-04). Enforce subscription-wide via Azure Policy assigned at the root management group; do not rely on per-resource diligence (Microsoft Learn — Storage Account network security (accessed 2026-05)). The principle is reinforced in General Network — private connectivity: PaaS services with a public IP are reachable from any internet host that can guess the resource name and present valid credentials, so the network perimeter must remove the public IP rather than rely on identity alone. Anti-conflation: this is the policy invariant ("public off, Private Endpoints required"); the resource-level Private Endpoint attachment is the implementation pattern covered by azure-net-04. CRITICAL because exploitation is a single misconfiguration away from internet-reachable storage or secrets.
Remediation — Azure CLI
# Azure CLI 2.x
# Storage Account: turn off all three public-access toggles + default-deny firewall.
az storage account update \
--resource-group rg-data-prod-westeu \
--name stappprodwesteu001 \
--public-network-access Disabled \
--allow-blob-public-access false \
--default-action Deny
# Key Vault: turn off public network access; enforce RBAC.
az keyvault update \
--resource-group rg-data-prod-westeu \
--name kv-app-prod-westeu \
--public-network-access Disabled \
--default-action Deny \
--enable-rbac-authorization true
# Audit: list every Storage Account in the tenant that still allows public access.
for sub in $(az account list --query '[].id' -o tsv); do
az storage account list --subscription "$sub" \
--query "[?publicNetworkAccess=='Enabled' || allowBlobPublicAccess==\`true\`].{sub:'$sub', name:name, rg:resourceGroup}" \
-o tsv
done
Remediation — Terraform
# Terraform AzureRM provider ~> 3.0
# Source: Microsoft Learn (accessed 2026-05)
resource "azurerm_storage_account" "app" {
name = "stappprodwesteu001"
resource_group_name = azurerm_resource_group.data.name
location = azurerm_resource_group.data.location
account_tier = "Standard"
account_replication_type = "ZRS"
min_tls_version = "TLS1_2"
# Three independent public-access toggles — all must close.
public_network_access_enabled = false
allow_nested_items_to_be_public = false
shared_access_key_enabled = false # force Entra ID auth where supported
network_rules {
default_action = "Deny"
bypass = ["AzureServices"]
virtual_network_subnet_ids = [] # no Service Endpoints; Private Endpoints only
ip_rules = []
}
}
resource "azurerm_key_vault" "app" {
name = "kv-app-prod-westeu"
resource_group_name = azurerm_resource_group.data.name
location = azurerm_resource_group.data.location
tenant_id = data.azurerm_client_config.current.tenant_id
sku_name = "premium"
public_network_access_enabled = false
enable_rbac_authorization = true
purge_protection_enabled = true
soft_delete_retention_days = 90
network_acls {
default_action = "Deny"
bypass = "AzureServices"
}
}
# Root-management-group initiative: deny PaaS resources with public network access on.
resource "azurerm_management_group_policy_assignment" "deny_paas_public" {
name = "deny-paas-public-network-access"
management_group_id = "/providers/Microsoft.Management/managementGroups/tenant-root"
policy_definition_id = var.deny_paas_public_initiative_id
description = "Storage / KV / SQL / Cosmos must have public network access disabled"
}
Remediation — Bicep
targetScope = 'resourceGroup'
@description('Azure Policy assignment forbidding public-access PaaS endpoints.')
param assignmentName string = 'deny-public-paas-endpoints'
// Built-in initiative: Configure Azure PaaS services to use private link
var initiativeId = '/providers/Microsoft.Authorization/policySetDefinitions/d1cb47db-b7a1-4c46-814e-aad1c0e84f3c'
resource assign 'Microsoft.Authorization/policyAssignments@2024-04-01' = {
name: assignmentName
properties: {
policyDefinitionId: initiativeId
enforcementMode: 'Default'
displayName: 'Deny PaaS resources with public network access'
}
}
Remediation — Pulumi (TypeScript)
import * as pulumi from "@pulumi/pulumi";
import * as authorization from "@pulumi/azure-native/authorization";
new authorization.PolicyAssignment("deny-public-paas-endpoints", {
scope: "/subscriptions/<sub-id>",
policyAssignmentName: "deny-public-paas-endpoints",
policyDefinitionId: "/providers/Microsoft.Authorization/policySetDefinitions/d1cb47db-b7a1-4c46-814e-aad1c0e84f3c",
enforcementMode: authorization.EnforcementMode.Default,
displayName: "Deny PaaS resources with public network access",
});
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
n/a
3.x; 4.x; 8.x
n/a
n/a
SC-7; AC-4
A.8.20; A.8.22
CLD.9.5.1
Log signals
AzureActivity write events on data-plane services (Microsoft.Storage/storageAccounts, Microsoft.KeyVault/vaults, Microsoft.Sql/servers, Microsoft.DocumentDB/databaseAccounts) where publicNetworkAccess flips from Disabled to Enabled.
AzureActivity Microsoft.Network/privateEndpoints/delete on a private endpoint that was the sole replacement for the public surface — silent fallback to public access if combined with a public-network-access flip.
AzureActivity write events on data-plane firewalls (e.g. networkAcls.defaultAction = Allow) — equivalent regression even when public-network-access stays disabled.
Query
AzureActivity
| where OperationNameValue endswith "/write"
| where ResourceId has_any ("Microsoft.Storage/storageAccounts", "Microsoft.KeyVault/vaults", "Microsoft.Sql/servers", "Microsoft.DocumentDB/databaseAccounts", "Microsoft.CognitiveServices/accounts")
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "\"publicNetworkAccess\":\"Enabled\"" or body has "\"defaultAction\":\"Allow\""
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. Persist as a Sentinel analytics rule with severity High; private-endpoint posture regression is one of the highest-impact data-plane control failures.
Alert threshold
Any public-network-access flip to Enabled on a data-plane service holding production data — page on first occurrence.
Delete of a private endpoint without a same-window creation of a replacement — page; data-plane reachability has changed even if the resource flag did not.
Initial response
Flip publicNetworkAccess back to Disabled via the IaC baseline; capture the AzureActivity Caller and the resource-graph diff as the rollback ledger.
Walk the data-plane diagnostic stream (StorageBlobLogs, AzureDiagnostics for Key Vault category AuditEvent, SQLSecurityAuditEvents) for the exposure window — any successful read from a public IP should be considered exfiltration-pending-review.
Escalate per general/ir.html — confirm the Azure Policy Public network access should be disabled for {service} set remains in deny mode at the management-group root.
Front PaaS services accessed from VNets with Private Endpoints, not Service Endpoints. A Private Endpoint surfaces a private IP inside a chosen workload subnet that fronts the PaaS resource via Azure Private Link, integrates with a Private DNS Zone so the public PaaS FQDN resolves to the private IP from inside the VNet, and lets the upstream policy in azure-net-03 hold (Microsoft Learn — Private Endpoint overview (accessed 2026-05)). The principle is reinforced in General Network — zero trust: never traverse a network you do not control. Anti-pattern to flag: Service Endpoints (the legacy pattern, Microsoft.Storage etc. on a subnet) keep traffic on the Azure backbone but the PaaS service still exposes a public IP, so the public attack surface remains; Private Endpoints expose a private IP inside the VNet and remove the public attack surface entirely. Service Endpoints survive only for backwards-compatibility — new code should always use Private Endpoints. HIGH PREVENTIVE because this is the implementation pattern that makes the CRITICAL policy in azure-net-03 enforceable in workload code.
Remediation — Azure CLI
# Azure CLI 2.x
# Resolve the resource id of the target Storage Account.
STORAGE_ID=$(az storage account show \
--resource-group rg-data-prod-westeu \
--name stappprodwesteu001 --query id -o tsv)
# Create a Private Endpoint in the workload PE subnet for the blob sub-resource.
az network private-endpoint create \
--resource-group rg-net-prod-westeu \
--name pe-stappprodwesteu001-blob \
--vnet-name vnet-app-prod-westeu \
--subnet snet-pe-data \
--private-connection-resource-id "$STORAGE_ID" \
--group-id blob \
--connection-name pec-stappprodwesteu001-blob \
--location westeurope
# Create the Private DNS Zone and link the workload VNet to it.
az network private-dns zone create \
--resource-group rg-net-prod-westeu \
--name privatelink.blob.core.windows.net
az network private-dns link vnet create \
--resource-group rg-net-prod-westeu \
--zone-name privatelink.blob.core.windows.net \
--name pdz-link-vnet-app-prod \
--virtual-network vnet-app-prod-westeu \
--registration-enabled false
# Wire the Private Endpoint's A record into the zone.
az network private-endpoint dns-zone-group create \
--resource-group rg-net-prod-westeu \
--endpoint-name pe-stappprodwesteu001-blob \
--name pdzg-blob \
--private-dns-zone privatelink.blob.core.windows.net \
--zone-name blob
targetScope = 'resourceGroup'
@description('Resource ID of the PaaS service being wrapped (e.g. Storage / KV / SQL).')
param targetResourceId string
@description('Group ID for the private endpoint (e.g. blob, vault, sqlServer).')
param groupId string
@description('Subnet ID hosting the endpoint NIC.')
param subnetId string
param location string = resourceGroup().location
resource pe 'Microsoft.Network/privateEndpoints@2024-03-01' = {
name: 'pe-${groupId}-${uniqueString(targetResourceId)}'
location: location
properties: {
subnet: { id: subnetId }
privateLinkServiceConnections: [
{
name: 'plsc'
properties: {
privateLinkServiceId: targetResourceId
groupIds: [groupId]
}
}
]
}
}
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
n/a
6.x (verify)
n/a
n/a
SC-7(8); AC-4
A.8.20; A.8.22
CLD.9.5.1
Log signals
AzureActivity Microsoft.Network/privateEndpoints/privateLinkServiceConnections/write creating a connection request from a subscription outside the documented internal-consumer set — possible cross-tenant Private Link abuse.
AzureActivity Microsoft.Network/privateLinkServices/privateEndpointConnections/write where connectionState = Approved is set by an unexpected approver identity — bypasses the four-eyes service-exposure flow.
AzureActivity
| where OperationNameValue in ("Microsoft.Network/privateLinkServices/privateEndpointConnections/write", "Microsoft.Network/privateEndpoints/privateLinkServiceConnections/write")
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "Approved" or body has "Pending"
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. Approval of a Private Link connection from an unknown subscription is the canonical pivot signal for cross-tenant data egress; persist as a Sentinel analytics rule with severity Medium and route to the data-plane service owner.
Alert threshold
Approval issued by an identity that is not on the Private Link approval allow-list — page on first occurrence.
Connection request from a subscription outside the documented consumer set — page even at the Pending stage so the approval flow does not autocomplete.
Initial response
Reject the connection via az network private-endpoint-connection reject; capture the requester subscription and AzureActivity Caller as the ledger.
Walk the data-plane diagnostic stream for the exposure window — every dataplane operation from the Private Link CIDR during the connection should be reconciled with the requester's claimed purpose.
Escalate per general/ir.html — confirm the Azure Policy enforcing manual approval on Private Link Services remains assigned at the resource provider scope.
Front internet-facing web traffic with Azure Front Door Premium and attach a WAF Premium policy carrying the Microsoft-managed Default Rule Set (DRS) 2.1 and the Bot Manager Rule Set; use custom rules only for tenant-specific exceptions (Microsoft Learn — Azure Front Door WAF overview (accessed 2026-05)). Front Door WAF is global and runs at the Microsoft edge, inspecting HTTP and HTTPS payloads — URI, headers, body, cookies, query strings — and is therefore an L7 control. Anti-conflation: Front Door WAF is not a substitute for DDoS Protection Standard (azure-net-06); WAF cannot help with a 1 Tbps volumetric SYN flood any more than DDoS Standard can stop an SQLi attempt in a well-formed HTTPS request. They are layered, not alternative. Application Gateway WAF is the regional in-VNet alternative for backends that cannot front through the public edge (mTLS-required APIs, internal landing pages) — covered in prose here, not as a separate control. HIGH PREVENTIVE because managed rules block known-exploit-pattern traffic at the edge before it ever reaches the application's parsing logic.
Remediation — Azure CLI
# Azure CLI 2.x
# Create the Front Door Premium WAF policy.
az network front-door waf-policy create \
--resource-group rg-edge-prod \
--name fdwaf-app-prod \
--sku Premium_AzureFrontDoor \
--mode Prevention
# Add the Microsoft-managed Default Rule Set 2.1 in Block mode.
az network front-door waf-policy managed-rules add \
--resource-group rg-edge-prod \
--policy-name fdwaf-app-prod \
--type Microsoft_DefaultRuleSet \
--version 2.1 \
--action Block
# Add the Bot Manager rule set.
az network front-door waf-policy managed-rules add \
--resource-group rg-edge-prod \
--policy-name fdwaf-app-prod \
--type Microsoft_BotManagerRuleSet \
--version 1.0 \
--action Block
# Associate the policy with the Front Door Premium security policy.
az afd security-policy create \
--resource-group rg-edge-prod \
--profile-name afd-app-prod \
--security-policy-name sp-app-prod \
--domains "/subscriptions/.../customDomains/app-example-com" \
--waf-policy "/subscriptions/.../frontDoorWebApplicationFirewallPolicies/fdwaf-app-prod"
AzureActivity Microsoft.Network/frontdoorwebapplicationfirewallpolicies/write where policySettings.mode flips from Prevention to Detection — disarms blocking even though logging continues.
AzureActivity writes that remove a managed-rule-set assignment (DRS, BotManagerRuleSet) or that mass-disable rule overrides via the managedRuleOverrides array.
AzureDiagnostics ResourceProvider = "MICROSOFT.NETWORK" Category FrontDoorWebApplicationFirewallLog showing action = "Allow" on rule IDs that the baseline classifies as Block — confirmation the change took effect.
Query
AzureActivity
| where OperationNameValue == "Microsoft.Network/frontdoorwebapplicationfirewallpolicies/write"
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "\"mode\":\"Detection\"" or body has "\"action\":\"Allow\"" or body has "managedRuleOverrides"
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. The Detection-mode flip is the canonical WAF-bypass setup: keeps the SOC dashboards filling while removing the actual block. Persist as a Sentinel analytics rule with severity High.
Alert threshold
Any flip of a production WAF policy from Prevention to Detection — page immediately.
Removal of a managed rule set from a production policy — page; the entire DRS or BotManager surface just stopped enforcing.
Initial response
Restore the policy to Prevention via the IaC baseline; confirm the next batch of FrontDoorWebApplicationFirewallLog rows shows action = "Block" for the previously-overridden rule IDs.
Walk the FrontDoorAccessLog for the exposure window for requests matching the disabled rule signatures — successful 2xx responses to those requests are candidate compromise events for the origin.
Escalate per general/ir.html — confirm the Azure Policy Web Application Firewall should be enabled for Front Door set is in deny mode at the management-group root.
Enable Azure DDoS Protection Standard on VNets that host internet-facing public IPs (Load Balancer frontends, Application Gateway frontends, Bastion hosts, Network Virtual Appliances) and pair it with Azure Monitor metrics + alerts for traffic spikes and mitigation events (Microsoft Learn — Azure DDoS Protection Standard overview (accessed 2026-05)). DDoS Protection Basic is automatic and free at the Azure platform level — it protects every public IP against the largest volumetric attacks Microsoft sees on the global network. DDoS Protection Standard is the per-VNet paid tier (flat per-tenant per-month price plus per-resource overage) that adds tunable mitigation policies, attack analytics, cost-protection (DDoS-related scale-out is credited back), and rapid-response engagement with the Microsoft DDoS Rapid Response team. Anti-conflation: Standard plan is L3/L4 volumetric mitigation attached to VNets — the network-layer inverse of Front Door WAF (azure-net-05), which is L7 and edge-deployed. They are layered, not substitutes. MEDIUM RESPONSIVE because Standard's value-add over Basic is in the mitigation feedback loop during a sustained attack, not in preventing volumetric traffic from ever arriving — and most workloads have viable cheaper alternatives (Front Door + DDoS Basic suffices for the L3/L4 attacks mid-sized workloads see).
Remediation — Azure CLI
# Azure CLI 2.x
# Create the DDoS Protection Plan (one per tenant typically; reference from many VNets).
az network ddos-protection create \
--resource-group rg-edge-prod \
--name ddosplan-prod \
--location westeurope
# Attach the plan to a VNet (must be done at VNet level; subnet attachment is implicit).
az network vnet update \
--resource-group rg-net-prod-westeu \
--name vnet-app-prod-westeu \
--ddos-protection-plan ddosplan-prod \
--ddos-protection true
# Verify the plan is active.
az network ddos-protection show \
--resource-group rg-edge-prod \
--name ddosplan-prod \
--query '{name:name, vnets:virtualNetworks[].id}' -o table
Remediation — Terraform
# Terraform AzureRM provider ~> 3.0
# Source: Microsoft Learn (accessed 2026-05)
resource "azurerm_network_ddos_protection_plan" "prod" {
name = "ddosplan-prod"
resource_group_name = azurerm_resource_group.edge.name
location = azurerm_resource_group.edge.location
tags = { tier = "revenue-bearing" }
}
# Wire the plan into the workload VNet via the ddos_protection_plan block.
resource "azurerm_virtual_network" "workload_ddos" {
name = "vnet-app-prod-westeu"
resource_group_name = azurerm_resource_group.net.name
location = azurerm_resource_group.net.location
address_space = ["10.40.0.0/16"]
ddos_protection_plan {
id = azurerm_network_ddos_protection_plan.prod.id
enable = true
}
}
# Azure Monitor alert on the DDoS mitigation-triggered metric.
resource "azurerm_monitor_metric_alert" "ddos_mitigation" {
name = "ddos-mitigation-triggered"
resource_group_name = azurerm_resource_group.edge.name
scopes = [azurerm_public_ip.app.id]
description = "DDoS mitigation engaged on app public IP"
criteria {
metric_namespace = "Microsoft.Network/publicIPAddresses"
metric_name = "IfUnderDDoSAttack"
aggregation = "Maximum"
operator = "GreaterThan"
threshold = 0
}
action {
action_group_id = azurerm_monitor_action_group.security_oncall.id
}
}
AzureActivity Microsoft.Network/ddosProtectionPlans/delete targeting a plan referenced by production VNets — silently drops the volumetric-defence layer.
AzureActivity Microsoft.Network/virtualNetworks/write where enableDdosProtection flips to false or where ddosProtectionPlan.id is removed.
AzureDiagnostics ResourceProvider = "MICROSOFT.NETWORK" Category DDoSProtectionNotifications abrupt drop to zero — downstream signal the plan is no longer attached.
Query
AzureActivity
| where OperationNameValue in ("Microsoft.Network/ddosProtectionPlans/delete", "Microsoft.Network/virtualNetworks/write")
| extend body = tostring(parse_json(Properties).requestbody)
| where OperationNameValue endswith "ddosProtectionPlans/delete" or body has "\"enableDdosProtection\":false"
| project TimeGenerated, Caller, ResourceId, OperationNameValue, body
| order by TimeGenerated desc
| take 100
Run as a KQL query in Log Analytics. The DDoS Standard plan is one resource per region per subscription and rarely changes; persist as a Sentinel analytics rule with severity High.
Alert threshold
Delete of a DDoS protection plan referenced by any production VNet — page on first occurrence.
VNet update detaching the plan reference — page; treat as same severity as plan delete.
Initial response
Reapply the DDoS plan and VNet binding via the IaC baseline; confirm the next AzureDiagnostics DDoSProtectionNotifications batch resumes within 10 minutes.
Walk the VNet-attached public IPs for any traffic anomalies during the exposure window — sudden L3/L4 volume spikes that would normally have been mitigated should be reviewed for origin and persistence.
Escalate per general/ir.html — confirm the Azure Policy Azure DDoS Protection should be enabled remains assigned in deny mode.
Enable DNSSEC signing on every Azure DNS public zone the organisation owns. DNSSEC binds each DNS record to a cryptographic signature that validating resolvers (and intermediate caches) check, defeating cache-poisoning and on-path response-rewrite attacks (Microsoft Learn — Azure DNS DNSSEC overview (accessed 2026-05)). The principle is reinforced in General Network — DNS security. DNSSEC for Azure DNS public zones went GA in 2024; the per-zone setting is exposed by the az network dns dnssec-config command group, and the upstream DS record must be lodged at the parent registrar. Two Azure-specific constraints to flag in advance: (1) DNSSEC applies to public zones only — Azure DNS private zones do not support DNSSEC and never will, because the validation chain assumes a public root and key signing key tied to ICANN's root trust anchor; (2) the AzureRM provider's first-class DNSSEC resource (azurerm_dns_dnssec_config) shipped in the 4.x line; on the Phase 5/6-pinned 3.x line we drive DNSSEC enablement through the az CLI as the primary remediation and document a 4.x convergence path. CIS Microsoft Azure Foundations Benchmark v3.0.0 (Feb 2025) predates the feature; re-verify at writing time in case a benchmark patch has added a sub-ID.
Remediation — Azure CLI
# Azure CLI 2.x
# Enable DNSSEC on a public zone (creates the KSK + ZSK and signs records).
az network dns dnssec-config create \
--resource-group rg-dns-prod \
--zone-name example.com
# Read back the DS record values to lodge at the parent registrar.
az network dns dnssec-config show \
--resource-group rg-dns-prod \
--zone-name example.com \
--query '{algorithm:signingKeys[].algorithm, digests:signingKeys[].digestValue, keyTag:signingKeys[].keyTag}' \
-o json
# Audit: list every public zone in the tenant that does NOT have DNSSEC enabled.
for sub in $(az account list --query '[].id' -o tsv); do
az network dns zone list --subscription "$sub" \
--query "[?zoneType=='Public'].{sub:'$sub', name:name, rg:resourceGroup}" -o tsv
done
# Cross-reference each zone with `az network dns dnssec-config show` to find gaps.
Remediation — Terraform
# Terraform AzureRM provider ~> 3.0
# Source: Microsoft Learn (accessed 2026-05)
# Note: the first-class azurerm_dns_dnssec_config resource shipped in AzureRM 4.x.
# On the 3.x pin used by this corpus the canonical remediation is the az CLI
# block above; the public-zone resource is declared here and the DNSSEC config
# is reconciled via an azapi_resource block until corpus-wide 4.x lift.
resource "azurerm_dns_zone" "example_com" {
name = "example.com"
resource_group_name = azurerm_resource_group.dns.name
tags = { tier = "prod", "dnssec-required" = "true" }
}
# AzAPI bridge for DNSSEC config on AzureRM 3.x.
resource "azapi_resource" "dnssec_example_com" {
type = "Microsoft.Network/dnsZones/dnssecConfigs@2023-07-01-preview"
name = "default"
parent_id = azurerm_dns_zone.example_com.id
body = jsonencode({ properties = {} })
}
Remediation — Bicep
targetScope = 'resourceGroup'
@description('Public DNS zone with DNSSEC enabled (GA Azure DNS).')
param zoneName string
resource zone 'Microsoft.Network/dnsZones@2023-07-01-preview' = {
name: zoneName
location: 'global'
properties: {
zoneType: 'Public'
}
}
resource dnssec 'Microsoft.Network/dnsZones/dnssecConfigs@2023-07-01-preview' = {
parent: zone
name: 'default'
}
Compliance mapping
CIS AWS Foundations v3.0.0
CIS Microsoft Azure Foundations v3.0.0
CIS GCP Foundation v4.0.0
CIS OCI Foundation v2.0.0
NIST SP 800-53 rev5
ISO/IEC 27001:2022
ISO/IEC 27017:2015
n/a
n/a (post-v3.0.0)
n/a
n/a
SC-20; SC-21
A.8.21
CLD.9.5.1
Log signals
AzureActivity Microsoft.Network/dnszones/write editing high-value records (apex A/AAAA, MX, NS, TXT used for SPF/DKIM) on a production zone — DNS-hijack pivot for credential phishing or BGP-style traffic redirection.
AzureActivity Microsoft.Network/dnsResolverPolicies/write adding a forwarding rule to an external resolver outside the documented set — silently exfiltrates DNS query telemetry.
AzureDiagnostics ResourceProvider = "MICROSOFT.NETWORK" Category DnsResponse showing answers that no longer match the published apex — downstream confirmation of poisoning.
Query
AzureActivity
| where OperationNameValue startswith "Microsoft.Network/dnszones/" and OperationNameValue endswith "/write"
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "\"A\"" or body has "\"MX\"" or body has "\"NS\"" or body has "\"TXT\""
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. DNS edits should be ticket-bound and four-eyes-approved; persist as a Sentinel analytics rule with severity High and pair with an automation playbook that snapshots the prior record value to the audit archive.
Alert threshold
Any apex A/AAAA/MX/NS edit on a production zone — page on first occurrence.
Forwarding rule addition to an external resolver — page; treat as data-exfiltration indicator.
Initial response
Restore the prior record value from the audit archive; capture the AzureActivity Caller as the mutator-of-record and freeze the zone via RBAC scope reduction until the change is reconciled.
Walk DnsResponse telemetry for the exposure window — any client that resolved the mutated record should be assumed to have been redirected, and inbound credentials submitted in that window are candidate phishing harvest.
Escalate per general/ir.html — confirm Azure Policy DNS Zone changes require approval custom assignment remains active and that the zone-write RBAC scope is bound to the change-management service principal only.
Deploy Azure Firewall Premium in the hub VNet of a hub-and-spoke (or in a Virtual WAN hub), front it with a Firewall Policy hierarchy (root → tenant → workload), default-deny egress, and use forced tunneling so spoke workloads cannot bypass the firewall for direct internet egress (Microsoft Learn — Azure Firewall Premium features (accessed 2026-05)). Premium adds the features that distinguish Azure Firewall from an NSG: FQDN filtering on TLS SNI (without TLS inspection), full TLS inspection (with a customer-managed CA in Key Vault), IDPS with Microsoft-managed signature sets, and URL filtering with web-category lists. The general principle — workloads talk only to destinations they need — is documented at General Network — egress control. Anti-conflation (three-way distinction): Azure Firewall is the centralised L4/L7 hub control; NSGs (azure-net-02) are L4-only and scoped at the subnet/NIC; Front Door WAF (azure-net-05) is the global edge L7 ingress filter; Application Gateway WAF is the regional in-VNet alternative for backends that cannot front through the public edge. Different scopes, different traffic directions, complementary not alternative. HIGH PREVENTIVE because egress filtering is the single largest exfiltration-path mitigation Azure exposes once east-west is constrained — it is the missing complement to Private Endpoints, which only covers Azure-service traffic.
AzureActivity Microsoft.Network/azureFirewalls/write or Microsoft.Network/firewallPolicies/ruleCollectionGroups/write where the request body broadens an Allow application rule to * or adds an Allow network rule with destinationAddresses = [*].
AzureActivity Microsoft.Network/routeTables/routes/write editing the 0.0.0.0/0 route to bypass the firewall — silently moves egress around the inspection plane.
AzureDiagnostics Category AzureFirewallApplicationRule showing Allow on previously blocked FQDNs — downstream confirmation the rule edit took effect.
Query
AzureActivity
| where OperationNameValue in ("Microsoft.Network/azureFirewalls/write", "Microsoft.Network/firewallPolicies/ruleCollectionGroups/write", "Microsoft.Network/routeTables/routes/write")
| extend body = tostring(parse_json(Properties).requestbody)
| where body has "\"action\":\"Allow\"" and (body has "\"targetFqdns\":[\"*\"]" or body has "\"destinationAddresses\":[\"*\"]" or body has "\"nextHopType\":\"Internet\"")
| project TimeGenerated, Caller, ResourceId, body
| order by TimeGenerated desc
| take 200
Run as a KQL query in Log Analytics. Egress-bypass route insertion is the highest-fidelity exfiltration-setup pattern; persist as a Sentinel analytics rule with severity High and pair with an automation playbook that reverts the route within 5 minutes.
Alert threshold
Allow-all FQDN target in an application rule on a production firewall policy — page on first occurrence.
Default route edit setting nextHopType = Internet on a subnet previously routed to the firewall — page; treat as exfiltration-preparation incident.
Initial response
Restore the rule and route via the IaC baseline; capture the AzureActivity Caller and the prior firewall-policy revision as the rollback ledger.
Walk AzureFirewallApplicationRule and AzureFirewallNetworkRule logs for the exposure window for any traffic that would have been blocked under the prior policy — high-value outbound destinations (TOR exit nodes, raw-paste services, IPFS gateways) take priority.
Escalate per general/ir.html — confirm Azure Policy Subnets should be associated with a Network Security Group and Egress should be routed through Azure Firewall remain in deny mode.