Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
d759885
Initial plan
Copilot Jan 29, 2026
9e2a199
Add specific NSG modules and update infrastructure files with NSG flo…
Copilot Jan 29, 2026
7e1551a
Bicep fixes
simonkurtz-MSFT Jan 30, 2026
ce9152a
Format
simonkurtz-MSFT Jan 30, 2026
e713590
Initial plan
Copilot Jan 29, 2026
e3fa5be
Add specific NSG modules and update infrastructure files with NSG flo…
Copilot Jan 29, 2026
ebf196e
Bicep fixes
simonkurtz-MSFT Jan 30, 2026
6acc989
Format
simonkurtz-MSFT Jan 30, 2026
309d543
Merge remote-tracking branch 'origin/copilot/add-nsg-rules-for-azure-…
simonkurtz-MSFT Jan 30, 2026
d7decbe
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Feb 21, 2026
d7d88a1
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Feb 23, 2026
eca3b4f
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Feb 23, 2026
e48b483
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Feb 27, 2026
c4c6dcc
NSG adjustments
simonkurtz-MSFT Feb 27, 2026
d745ba0
Converge onto one smart NSG module
simonkurtz-MSFT Feb 27, 2026
9aabcc3
Add separate nsg-appgw bicep file
simonkurtz-MSFT Mar 2, 2026
842c141
Intermediate check-in
simonkurtz-MSFT Mar 4, 2026
96f77fc
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Mar 5, 2026
094a8df
Merge branch 'main' into copilot/add-nsg-rules-for-azure-services
simonkurtz-MSFT Mar 5, 2026
b9ea1c8
Add note regarding NSG
simonkurtz-MSFT Mar 5, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions infrastructure/afd-apim-pe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Secure architecture that takes all traffic off the public Internet once Azure Fr

Adjust the `user-defined parameters` in this lab's Jupyter Notebook's [Initialize notebook variables][init-notebook-variables] section.

The notebook also includes a `SYSTEM CONFIGURATION` flag named `use_strict_nsg`. It defaults to `False`.

We provide NSG deployment as an option for teams that want to experiment with subnet-level controls, but we intentionally keep it disabled by default. The goal of these samples is to stay approachable and focused on APIM scenarios rather than drifting into full Azure Landing Zone-style network governance complexity.

NSG behavior:
- `nsg-default`: Generic fallback NSG for subnets that do not have a service-specific NSG. It stays intentionally generic.
- `use_strict_nsg = False`: Service subnets get permissive service-aware NSGs: `nsg-apim` and `nsg-aca`. These preserve Azure platform requirements and avoid unnecessary ingress restrictions.
- `use_strict_nsg = True`: Service subnets get strict NSGs: `nsg-apim-strict` and `nsg-aca-strict`. These keep required platform rules but restrict ingress so traffic follows Front Door -> APIM -> ACA.

## ▶️ Execution

👟 **Expected *Run All* runtime: ~13 minutes**
Expand Down
10 changes: 9 additions & 1 deletion infrastructure/afd-apim-pe/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,15 @@
"# SYSTEM CONFIGURATION\n",
"# ------------------------------\n",
"\n",
"inb_helper = InfrastructureNotebookHelper(rg_location, INFRASTRUCTURE.AFD_APIM_PE, index, apim_sku)\n",
"use_strict_nsg = False # Optional: deploy strict NSGs for supported subnets. Disabled by default to keep the sample focused.\n",
"\n",
"inb_helper = InfrastructureNotebookHelper(\n",
" rg_location,\n",
" INFRASTRUCTURE.AFD_APIM_PE,\n",
" index,\n",
" apim_sku,\n",
" use_strict_nsg,\n",
")\n",
"inb_helper.create_infrastructure()\n",
"\n",
"print_ok('All done!')"
Expand Down
28 changes: 25 additions & 3 deletions infrastructure/afd-apim-pe/create_infrastructure.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,14 @@
from infrastructures import AfdApimAcaInfrastructure


def create_infrastructure(location: str, index: int, apim_sku: APIM_SKU, no_aca: bool = False, rg_exists: bool | None = None) -> None:
def create_infrastructure(
location: str,
index: int,
apim_sku: APIM_SKU,
no_aca: bool = False,
use_strict_nsg: bool = False,
rg_exists: bool | None = None,
) -> None:
"""Create the Azure Front Door + APIM Private Endpoint infrastructure."""
if rg_exists is None:
infrastructure_exists = az.does_resource_group_exist(az.get_infra_rg_name(INFRASTRUCTURE.AFD_APIM_PE, index))
Expand All @@ -23,7 +30,14 @@ def create_infrastructure(location: str, index: int, apim_sku: APIM_SKU, no_aca:
# Create custom APIs for AFD-APIM-PE with optional Container Apps backends
custom_apis = _create_afd_specific_apis(not no_aca)

infra = AfdApimAcaInfrastructure(location, index, apim_sku, infra_apis=custom_apis, rg_exists=rg_exists)
infra = AfdApimAcaInfrastructure(
location,
index,
apim_sku,
infra_apis=custom_apis,
use_strict_nsg=use_strict_nsg,
rg_exists=rg_exists,
)
result = infra.deploy_infrastructure(infrastructure_exists)

raise SystemExit(0 if result.success else 1)
Expand Down Expand Up @@ -85,6 +99,7 @@ def main():
parser.add_argument('--index', type=int, help='Infrastructure index')
parser.add_argument('--sku', choices=['Standardv2', 'Premiumv2'], default='Standardv2', help='APIM SKU (default: Standardv2)')
parser.add_argument('--no-aca', action='store_true', help='Disable Azure Container Apps')
parser.add_argument('--use-strict-nsg', action='store_true', help='Deploy strict NSGs for supported subnets')
parser.add_argument('--rg-exists', action=argparse.BooleanOptionalAction, default=None, help='Pre-checked resource group existence state')
args = parser.parse_args()

Expand All @@ -95,7 +110,14 @@ def main():
print(f"Error: Invalid SKU '{args.sku}'. Valid options are: {', '.join([sku.value for sku in APIM_SKU])}")
sys.exit(1)

create_infrastructure(args.location, args.index, apim_sku, args.no_aca, rg_exists=args.rg_exists)
create_infrastructure(
args.location,
args.index,
apim_sku,
args.no_aca,
use_strict_nsg=args.use_strict_nsg,
rg_exists=args.rg_exists,
)


if __name__ == '__main__': # pragma: no cover
Expand Down
126 changes: 110 additions & 16 deletions infrastructure/afd-apim-pe/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ param policyFragments array = []
@description('Set to true to make APIM publicly accessible. If false, APIM will be deployed into a VNet subnet for egress only.')
param apimPublicAccess bool = true

@description('Whether to deploy strict, service-specific Network Security Groups for supported subnets. When false, a default (empty-rules) NSG is still attached to every subnet.')
param useStrictNsg bool = false

@description('Enable legacy NSG flow logs. Azure blocks creation of new NSG flow logs, so keep this disabled unless updating an existing flow log resource.')
param enableLegacyNsgFlowLogs bool = false

@description('Reveals the backend API information. Defaults to true. *** WARNING: This will expose backend API information to the caller - For learning & testing only! ***')
param revealBackendApiInfo bool = true

Expand Down Expand Up @@ -72,17 +78,71 @@ module appInsightsModule '../../shared/bicep/modules/monitor/v1/appinsights.bice
var appInsightsId = appInsightsModule.outputs.id
var appInsightsInstrumentationKey = appInsightsModule.outputs.instrumentationKey

// 3. Virtual Network and Subnets
// 3. Storage Account for legacy NSG Flow Logs
module storageFlowLogsModule '../../shared/bicep/modules/vnet/v1/storage-flowlogs.bicep' = if (useStrictNsg && enableLegacyNsgFlowLogs) {
name: 'storageFlowLogsModule'
params: {
location: location
resourceSuffix: resourceSuffix
}
}

// We are using a standard NSG for our subnets here. Production workloads should use a relevant, custom NSG for each subnet.
// We also do not presently use a custom route table for the subnets, which is a best practice for production workloads.
// 4. Network Security Groups
// NSG model:
// - nsg-default stays generic and is only used as a fallback for subnets without a service-aware NSG.
// - When useStrictNsg is false, service subnets get permissive NSGs (nsg-apim, nsg-aca).
// - When useStrictNsg is true, service subnets get restrictive NSGs (nsg-apim-strict, nsg-aca-strict).

// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups
resource nsg 'Microsoft.Network/networkSecurityGroups@2025-01-01' = {
resource nsgDefault 'Microsoft.Network/networkSecurityGroups@2025-01-01' = {
name: 'nsg-default'
location: location
}

// NSG for API Management with Private Link from Front Door - strict vs default
module nsgApimStrictModule '../../shared/bicep/modules/vnet/v1/nsg-apim-strict.bicep' = if (useStrictNsg) {
name: 'nsgApimStrictModule'
params: {
location: location
nsgName: 'nsg-apim-strict'
apimSubnetPrefix: apimSubnetPrefix
allowFrontDoorBackend: true // APIM is always accessed through Front Door in this architecture
apimSku: apimSku
vnetMode: 'integration'
}
}

module nsgApimModule '../../shared/bicep/modules/vnet/v1/nsg-apim.bicep' = if (!useStrictNsg) {
name: 'nsgApimModule'
params: {
location: location
nsgName: 'nsg-apim'
apimSubnetPrefix: apimSubnetPrefix
apimSku: apimSku
vnetMode: 'integration'
}
}

// NSG for Container Apps - strict NSG only when using strict NSGs (and only if ACA is enabled)
module nsgAcaStrictModule '../../shared/bicep/modules/vnet/v1/nsg-aca-strict.bicep' = if (useStrictNsg && useACA) {
name: 'nsgAcaStrictModule'
params: {
location: location
nsgName: 'nsg-aca-strict'
acaSubnetPrefix: acaSubnetPrefix
apimSubnetPrefix: apimSubnetPrefix
}
}

module nsgAcaModule '../../shared/bicep/modules/vnet/v1/nsg-aca.bicep' = if (!useStrictNsg && useACA) {
name: 'nsgAcaModule'
params: {
location: location
nsgName: 'nsg-aca'
acaSubnetPrefix: acaSubnetPrefix
}
}

// 5. Virtual Network and Subnets
module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = {
name: 'vnetModule'
params: {
Expand All @@ -95,7 +155,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = {
properties: {
addressPrefix: apimSubnetPrefix
networkSecurityGroup: {
id: nsg.id
id: useStrictNsg ? nsgApimStrictModule!.outputs.nsgId : nsgApimModule!.outputs.nsgId
}
delegations: [
{
Expand All @@ -113,7 +173,9 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = {
properties: {
addressPrefix: acaSubnetPrefix
networkSecurityGroup: {
id: nsg.id
id: useACA
? (useStrictNsg ? nsgAcaStrictModule!.outputs.nsgId : nsgAcaModule!.outputs.nsgId)
: nsgDefault.id
}
delegations: [
{
Expand Down Expand Up @@ -150,7 +212,39 @@ resource acaSubnetResource 'Microsoft.Network/virtualNetworks/subnets@2024-05-01
var apimSubnetResourceId = apimSubnetResource.id
var acaSubnetResourceId = acaSubnetResource.id

// 4. Azure Container App Environment (ACAE)
// 6. Legacy NSG Flow Logs and Traffic Analytics

// NSG flow logs are retired and blocked for new deployments. Keep disabled by default.
module nsgFlowLogsApimModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (useStrictNsg && enableLegacyNsgFlowLogs) {
name: 'nsgFlowLogsApimModule'
scope: resourceGroup(subscription().subscriptionId, 'NetworkWatcherRG')
params: {
location: location
flowLogName: 'fl-nsg-apim-strict-${resourceSuffix}'
nsgResourceId: nsgApimStrictModule!.outputs.nsgId
storageAccountResourceId: storageFlowLogsModule!.outputs.storageAccountId
logAnalyticsWorkspaceResourceId: lawId
retentionDays: 7
enableTrafficAnalytics: true
}
}

// NSG flow logs are retired and blocked for new deployments. Keep disabled by default.
module nsgFlowLogsAcaModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (useACA && useStrictNsg && enableLegacyNsgFlowLogs) {
name: 'nsgFlowLogsAcaModule'
scope: resourceGroup(subscription().subscriptionId, 'NetworkWatcherRG')
params: {
location: location
flowLogName: 'fl-nsg-aca-strict-${resourceSuffix}'
nsgResourceId: nsgAcaStrictModule!.outputs.nsgId
storageAccountResourceId: storageFlowLogsModule!.outputs.storageAccountId
logAnalyticsWorkspaceResourceId: lawId
retentionDays: 7
enableTrafficAnalytics: true
}
}

// 7. Azure Container App Environment (ACAE)
module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if (useACA) {
name: 'acaEnvModule'
params: {
Expand All @@ -161,7 +255,7 @@ module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if (
}
}

// 5. Azure Container Apps (ACA) for Mock Web API
// 8. Azure Container Apps (ACA) for Mock Web API
module acaModule1 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (useACA) {
name: 'acaModule-1'
params: {
Expand All @@ -180,7 +274,7 @@ module acaModule2 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (u
}
}

// 6. API Management
// 9. API Management
module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = {
name: 'apimModule'
params: {
Expand All @@ -193,7 +287,7 @@ module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = {
}
}

// 7. APIM Policy Fragments
// 10. APIM Policy Fragments
module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: {
name: 'pf-${pf.name}'
params:{
Expand All @@ -207,7 +301,7 @@ module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.
]
}]

// 8. APIM Backends for ACA
// 11. APIM Backends for ACA
module backendModule1 '../../shared/bicep/modules/apim/v1/backend.bicep' = if (useACA) {
name: 'aca-backend-1'
params: {
Expand Down Expand Up @@ -256,7 +350,7 @@ module backendPoolModule '../../shared/bicep/modules/apim/v1/backend-pool.bicep'
]
}

// 9. APIM APIs
// 12. APIM APIs
module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: if(length(apis) > 0) {
name: 'api-${api.name}'
params: {
Expand All @@ -275,7 +369,7 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a
]
}]

// 10. APIM Private DNS Zone, VNet Link, and (optional) DNS Zone Group
// 13. APIM Private DNS Zone, VNet Link, and (optional) DNS Zone Group
module apimDnsPrivateLinkModule '../../shared/bicep/modules/dns/v1/dns-private-link.bicep' = {
name: 'apimDnsPrivateLinkModule'
params: {
Expand All @@ -288,7 +382,7 @@ module apimDnsPrivateLinkModule '../../shared/bicep/modules/dns/v1/dns-private-l
}
}

// 11. ACA Private DNS Zone (regional, e.g., eastus2.azurecontainerapps.io), VNet Link, and wildcard A record via shared module
// 14. ACA Private DNS Zone (regional, e.g., eastus2.azurecontainerapps.io), VNet Link, and wildcard A record via shared module
module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-private-zone.bicep' = if (useACA && !empty(acaSubnetResourceId)) {
name: 'acaDnsPrivateZoneModule'
params: {
Expand All @@ -298,7 +392,7 @@ module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-privat
}
}

// 12. Front Door
// 15. Front Door
module afdModule '../../shared/bicep/modules/afd/v1/afd.bicep' = {
name: 'afdModule'
params: {
Expand Down
7 changes: 6 additions & 1 deletion infrastructure/apim-aca/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,12 @@
"# SYSTEM CONFIGURATION\n",
"# ------------------------------\n",
"\n",
"inb_helper = InfrastructureNotebookHelper(rg_location, INFRASTRUCTURE.APIM_ACA, index, apim_sku)\n",
"inb_helper = InfrastructureNotebookHelper(\n",
" rg_location,\n",
" INFRASTRUCTURE.APIM_ACA,\n",
" index,\n",
" apim_sku,\n",
")\n",
"inb_helper.create_infrastructure()\n",
"\n",
"print_ok('All done!')"
Expand Down
9 changes: 9 additions & 0 deletions infrastructure/appgw-apim-pe/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@ Secure architecture that takes all traffic off the public Internet once Azure Ap

Adjust the `user-defined parameters` in this lab's Jupyter Notebook's [Initialize notebook variables][init-notebook-variables] section.

The notebook also includes a `SYSTEM CONFIGURATION` flag named `use_strict_nsg`. It defaults to `False`.

We provide NSG deployment as an option for teams that want to explore subnet-level restrictions, but we intentionally keep it disabled by default. That keeps the sample focused on Application Gateway, private endpoints, and API Management without sliding too far toward Azure Landing Zone-style baseline networking complexity.

NSG behavior:
- `nsg-default`: Generic fallback NSG for subnets that do not have a service-specific NSG. It stays intentionally generic.
- `use_strict_nsg = False`: Service subnets get permissive service-aware NSGs: `nsg-appgw`, `nsg-apim`, and `nsg-aca`. These preserve Azure platform requirements and avoid unnecessary ingress restrictions.
- `use_strict_nsg = True`: Service subnets get strict NSGs: `nsg-appgw-strict`, `nsg-apim-strict`, and `nsg-aca-strict`. These keep required platform rules but restrict ingress so traffic follows App Gateway -> APIM -> ACA.

## ▶️ Execution

👟 **Expected *Run All* runtime: ~13 minutes**
Expand Down
13 changes: 11 additions & 2 deletions infrastructure/appgw-apim-pe/create.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"\n",
"❗️ **Modify entries under _User-defined parameters_**.\n",
"\n",
"**Note:** This infrastructure includes Application Gateway with API Management using private endpoints. The creation process includes two phases: initial deployment with public access, private link approval, and then disabling public access."
"**Note:** This infrastructure includes Application Gateway with API Management using private endpoints. The creation process includes two phases: initial deployment with public access, private link approval, and then disabling public access. \n",
"While we are using private endpoint connections, we still create the same `nsg-apim` that we create for a VNet-injected connection. It keeps things just a bit simpler, albeit not quite as trimmed as it could be."
]
},
{
Expand All @@ -37,7 +38,15 @@
"# SYSTEM CONFIGURATION\n",
"# ------------------------------\n",
"\n",
"inb_helper = InfrastructureNotebookHelper(rg_location, INFRASTRUCTURE.APPGW_APIM_PE, index, apim_sku)\n",
"use_strict_nsg = False # Optional: deploy strict NSGs for supported subnets. Disabled by default to keep the sample focused.\n",
"\n",
"inb_helper = InfrastructureNotebookHelper(\n",
" rg_location,\n",
" INFRASTRUCTURE.APPGW_APIM_PE,\n",
" index,\n",
" apim_sku,\n",
" use_strict_nsg,\n",
")\n",
"inb_helper.create_infrastructure()\n",
"\n",
"print_ok('All done!')"
Expand Down
Loading
Loading