diff --git a/infrastructure/afd-apim-pe/main.bicep b/infrastructure/afd-apim-pe/main.bicep index d9ca9bce..62d7ea52 100644 --- a/infrastructure/afd-apim-pe/main.bicep +++ b/infrastructure/afd-apim-pe/main.bicep @@ -72,17 +72,42 @@ 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 NSG Flow Logs +module storageFlowLogsModule '../../shared/bicep/modules/vnet/v1/storage-flowlogs.bicep' = { + name: 'storageFlowLogsModule' + params: { + location: location + resourceSuffix: resourceSuffix + } +} + +// 4. Network Security Groups -// 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. +// NSG for API Management with Private Link from Front Door +module nsgApimModule '../../shared/bicep/modules/vnet/v1/nsg-apim.bicep' = { + name: 'nsgApimModule' + params: { + location: location + nsgName: 'nsg-apim' + apimSubnetPrefix: apimSubnetPrefix + allowFrontDoorBackend: true + apimSku: apimSku + vnetMode: 'integration' + } +} -// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups -resource nsg 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { - name: 'nsg-default' - location: location +// NSG for Container Apps - only allow traffic from APIM +module nsgAcaModule '../../shared/bicep/modules/vnet/v1/nsg-aca.bicep' = if (useACA) { + name: 'nsgAcaModule' + params: { + location: location + nsgName: 'nsg-aca' + acaSubnetPrefix: acaSubnetPrefix + apimSubnetPrefix: apimSubnetPrefix + } } +// 5. Virtual Network and Subnets module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { name: 'vnetModule' params: { @@ -95,7 +120,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: apimSubnetPrefix networkSecurityGroup: { - id: nsg.id + id: nsgApimModule.outputs.nsgId } delegations: [ { @@ -113,7 +138,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: acaSubnetPrefix networkSecurityGroup: { - id: nsg.id + id: useACA ? nsgAcaModule!.outputs.nsgId : nsgApimModule.outputs.nsgId } delegations: [ { @@ -150,7 +175,37 @@ resource acaSubnetResource 'Microsoft.Network/virtualNetworks/subnets@2024-05-01 var apimSubnetResourceId = apimSubnetResource.id var acaSubnetResourceId = acaSubnetResource.id -// 4. Azure Container App Environment (ACAE) +// 6. NSG Flow Logs and Traffic Analytics + +// NSG Flow Logs for APIM +module nsgFlowLogsApimModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = { + name: 'nsgFlowLogsApimModule' + params: { + location: location + flowLogName: 'fl-nsg-apim-${resourceSuffix}' + nsgResourceId: nsgApimModule.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// NSG Flow Logs for ACA +module nsgFlowLogsAcaModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (useACA) { + name: 'nsgFlowLogsAcaModule' + params: { + location: location + flowLogName: 'fl-nsg-aca-${resourceSuffix}' + nsgResourceId: nsgAcaModule!.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: { @@ -161,7 +216,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: { @@ -180,7 +235,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: { @@ -193,7 +248,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:{ @@ -207,7 +262,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: { @@ -256,7 +311,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: { @@ -275,7 +330,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: { @@ -288,7 +343,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: { @@ -298,7 +353,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: { diff --git a/infrastructure/appgw-apim-pe/main.bicep b/infrastructure/appgw-apim-pe/main.bicep index ab705a8d..9991ef46 100644 --- a/infrastructure/appgw-apim-pe/main.bicep +++ b/infrastructure/appgw-apim-pe/main.bicep @@ -96,61 +96,51 @@ 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 NSG Flow Logs +module storageFlowLogsModule '../../shared/bicep/modules/vnet/v1/storage-flowlogs.bicep' = { + name: 'storageFlowLogsModule' + params: { + location: location + resourceSuffix: resourceSuffix + } +} + +// 4. Virtual Network and Subnets resource nsgDefault 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { name: 'nsg-default' location: location } -// App Gateway needs a specific NSG -resource nsgAppGw 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { - name: 'nsg-appgw' - location: location - properties: { - securityRules: [ - { - name: 'AllowGatewayManagerInbound' - properties: { - description: 'Allow Azure infrastructure communication' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '65200-65535' - sourceAddressPrefix: 'GatewayManager' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 100 - direction: 'Inbound' - } - } - { - name: 'AllowHTTPSInbound' - properties: { - description: 'Allow HTTPS traffic' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 110 - direction: 'Inbound' - } - } - { - name: 'AllowAzureLoadBalancerInbound' - properties: { - description: 'Allow Azure Load Balancer' - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 120 - direction: 'Inbound' - } - } - ] +module nsgAppGwModule '../../shared/bicep/modules/vnet/v1/nsg-appgw.bicep' = { + name: 'nsgAppGwModule' + params: { + location: location + nsgName: 'nsg-appgw' + } +} + +// NSG for APIM with Private Link from Application Gateway +module nsgApimModule '../../shared/bicep/modules/vnet/v1/nsg-apim.bicep' = { + name: 'nsgApimModule' + params: { + location: location + nsgName: 'nsg-apim' + apimSubnetPrefix: apimSubnetPrefix + allowAppGateway: true + appgwSubnetPrefix: appgwSubnetPrefix + apimSku: apimSku + vnetMode: 'integration' + } +} + +// NSG for Container Apps - only allow traffic from APIM +module nsgAcaModule '../../shared/bicep/modules/vnet/v1/nsg-aca.bicep' = if (useACA) { + name: 'nsgAcaModule' + params: { + location: location + nsgName: 'nsg-aca' + acaSubnetPrefix: acaSubnetPrefix + apimSubnetPrefix: apimSubnetPrefix } } @@ -166,7 +156,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: apimSubnetPrefix networkSecurityGroup: { - id: nsgDefault.id + id: nsgApimModule.outputs.nsgId } delegations: [ { @@ -184,7 +174,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: acaSubnetPrefix networkSecurityGroup: { - id: nsgDefault.id + id: useACA ? nsgAcaModule!.outputs.nsgId : nsgDefault.id } delegations: [ { @@ -202,7 +192,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: appgwSubnetPrefix networkSecurityGroup: { - id: nsgAppGw.id + id: nsgAppGwModule.outputs.nsgId } } } @@ -226,7 +216,51 @@ var acaSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${acaSubnetNam var appgwSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${appgwSubnetName}' var peSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${privateEndpointSubnetName}' -// 4. User Assigned Managed Identity +// 5. NSG Flow Logs and Traffic Analytics + +// NSG Flow Logs for Application Gateway +module nsgFlowLogsAppGwModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = { + name: 'nsgFlowLogsAppGwModule' + params: { + location: location + flowLogName: 'fl-nsg-appgw-${resourceSuffix}' + nsgResourceId: nsgAppGwModule.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// NSG Flow Logs for APIM +module nsgFlowLogsApimModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = { + name: 'nsgFlowLogsApimModule' + params: { + location: location + flowLogName: 'fl-nsg-apim-${resourceSuffix}' + nsgResourceId: nsgApimModule.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// NSG Flow Logs for ACA +module nsgFlowLogsAcaModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (useACA) { + name: 'nsgFlowLogsAcaModule' + params: { + location: location + flowLogName: 'fl-nsg-aca-${resourceSuffix}' + nsgResourceId: nsgAcaModule!.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// 6. User Assigned Managed Identity // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/managed-identity/user-assigned-identity module uamiModule 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.2' = { name: 'uamiModule' @@ -236,7 +270,7 @@ module uamiModule 'br/public:avm/res/managed-identity/user-assigned-identity:0.4 } } -// 5. Key Vault +// 7. Key Vault // https://learn.microsoft.com/azure/templates/microsoft.keyvault/vaults // This assignment is helpful for testing to allow you to examine and administer the Key Vault. Adjust accordingly for real workloads! var keyVaultAdminRoleAssignment = setCurrentUserAsKeyVaultAdmin && !empty(currentUserId) ? [ @@ -269,7 +303,7 @@ module keyVaultModule 'br/public:avm/res/key-vault/vault:0.13.3' = { } } -// 6. Public IP for Application Gateway +// 8. Public IP for Application Gateway // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/public-ip-address module appgwPipModule 'br/public:avm/res/network/public-ip-address:0.9.1' = { name: 'appgwPipModule' @@ -282,7 +316,7 @@ module appgwPipModule 'br/public:avm/res/network/public-ip-address:0.9.1' = { } } -// 7. WAF Policy for Application Gateway +// 9. WAF Policy for Application Gateway // https://learn.microsoft.com/azure/templates/microsoft.network/applicationgatewaywebapplicationfirewallpolicies resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2025-01-01' = { name: 'waf-${resourceSuffix}' @@ -308,7 +342,7 @@ resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPo } } -// 8. Azure Container App Environment (ACAE) +// 10. Azure Container App Environment (ACAE) module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if (useACA) { name: 'acaEnvModule' params: { @@ -319,7 +353,7 @@ module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if ( } } -// 9. Azure Container Apps (ACA) for Mock Web API +// 11. Azure Container Apps (ACA) for Mock Web API module acaModule1 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (useACA) { name: 'acaModule-1' params: { @@ -338,7 +372,7 @@ module acaModule2 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (u } } -// 10. API Management +// 12. API Management module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = { name: 'apimModule' params: { @@ -352,7 +386,7 @@ module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = { } } -// 11. APIM Policy Fragments +// 13. APIM Policy Fragments module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: { name: 'pf-${pf.name}' params:{ @@ -366,7 +400,7 @@ module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment. ] }] -// 12. APIM Backends for ACA +// 14. APIM Backends for ACA module backendModule1 '../../shared/bicep/modules/apim/v1/backend.bicep' = if (useACA) { name: 'aca-backend-1' params: { @@ -415,7 +449,7 @@ module backendPoolModule '../../shared/bicep/modules/apim/v1/backend-pool.bicep' ] } -// 13. APIM APIs +// 15. APIM APIs module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: if(length(apis) > 0) { name: 'api-${api.name}' params: { @@ -434,7 +468,7 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a ] }] -// 14. Private Endpoint for APIM +// 16. Private Endpoint for APIM // https://learn.microsoft.com/azure/templates/microsoft.network/privateendpoints resource apimPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { name: 'pe-apim-${resourceSuffix}' @@ -457,7 +491,7 @@ resource apimPrivateEndpoint 'Microsoft.Network/privateEndpoints@2024-05-01' = { } } -// 15. Private DNS Zone Group for APIM Private Endpoint +// 17. Private DNS Zone Group for APIM Private Endpoint // https://learn.microsoft.com/azure/templates/microsoft.network/privateendpoints/privatednszoneegroups resource apimPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZoneGroups@2024-05-01' = { name: 'apim-dns-zone-group' @@ -474,7 +508,7 @@ resource apimPrivateDnsZoneGroup 'Microsoft.Network/privateEndpoints/privateDnsZ } } -// 16. APIM Private DNS Zone, VNet Link +// 18. APIM Private DNS Zone, VNet Link module apimDnsPrivateLinkModule '../../shared/bicep/modules/dns/v1/dns-private-link.bicep' = { name: 'apimDnsPrivateLinkModule' params: { @@ -487,7 +521,7 @@ module apimDnsPrivateLinkModule '../../shared/bicep/modules/dns/v1/dns-private-l } } -// 17. ACA Private DNS Zone +// 19. ACA Private DNS Zone module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-private-zone.bicep' = if (useACA) { name: 'acaDnsPrivateZoneModule' params: { @@ -497,7 +531,7 @@ module acaDnsPrivateZoneModule '../../shared/bicep/modules/dns/v1/aca-dns-privat } } -// 18. Application Gateway +// 20. Application Gateway // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/application-gateway module appgwModule 'br/public:avm/res/network/application-gateway:0.7.2' = { name: 'appgwModule' diff --git a/infrastructure/appgw-apim/main.bicep b/infrastructure/appgw-apim/main.bicep index 55c1184c..bbc15ab0 100644 --- a/infrastructure/appgw-apim/main.bicep +++ b/infrastructure/appgw-apim/main.bicep @@ -1,10 +1,3 @@ -// ------------------ -// IMPORTS -// ------------------ - -import {nsgsr_denyAllInbound} from '../../shared/bicep/modules/vnet/v1/nsg_rules.bicep' - - // ------------------ // PARAMETERS // ------------------ @@ -67,7 +60,8 @@ var IMG_MOCK_WEB_API = 'simonkurtzmsft/mockwebapi:1.0.0-alpha.1' var CERT_NAME = 'appgw-cert' var DOMAIN_NAME = 'api.apim-samples.contoso.com' var APIM_V1_SKUS = ['Developer', 'Basic', 'Standard', 'Premium'] -var APIM_V2_SKUS = ['BasicV2', 'StandardV2', 'PremiumV2'] +var APIM_V2_SKUS = ['Basicv2', 'Standardv2', 'Premiumv2'] +var NO_VNET_SKUS = ['Basic', 'Standard'] // ------------------------------ @@ -75,6 +69,8 @@ var APIM_V2_SKUS = ['BasicV2', 'StandardV2', 'PremiumV2'] // ------------------------------ var azureRoles = loadJsonContent('../../shared/azure-roles.json') +var hasVNetSupport = !contains(NO_VNET_SKUS, apimSku) +var apimVnetMode = is_apim_sku_v2(apimSku) ? 'integration' : 'injection' // ------------------ // FUNCTIONS @@ -106,180 +102,53 @@ 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 NSG Flow Logs +module storageFlowLogsModule '../../shared/bicep/modules/vnet/v1/storage-flowlogs.bicep' = { + name: 'storageFlowLogsModule' + params: { + location: location + resourceSuffix: resourceSuffix + } +} + +// 4. Virtual Network and Subnets // https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups resource nsgDefault 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { name: 'nsg-default' location: location } -// App Gateway needs a specific NSG -// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups -resource nsgAppGw 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { - name: 'nsg-appgw' - location: location - properties: { - securityRules: [ - { - name: 'AllowGatewayManagerInbound' - properties: { - description: 'Allow Azure infrastructure communication' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '65200-65535' - sourceAddressPrefix: 'GatewayManager' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 100 - direction: 'Inbound' - } - } - { - name: 'AllowHTTPSInbound' - properties: { - description: 'Allow HTTPS traffic' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: '*' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 110 - direction: 'Inbound' - } - } - { - name: 'AllowAzureLoadBalancerInbound' - properties: { - description: 'Allow Azure Load Balancer' - protocol: '*' - sourcePortRange: '*' - destinationPortRange: '*' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: '*' - access: 'Allow' - priority: 120 - direction: 'Inbound' - } - } - ] +module nsgAppGwModule '../../shared/bicep/modules/vnet/v1/nsg-appgw.bicep' = { + name: 'nsgAppGwModule' + params: { + location: location + nsgName: 'nsg-appgw' } } -// APIM V1 needs a specific NSG: https://learn.microsoft.com/en-us/azure/api-management/api-management-using-with-internal-vnet#configure-nsg-rules -// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups -resource nsgApimV1 'Microsoft.Network/networkSecurityGroups@2025-01-01' = if (is_apim_sku_v1(apimSku)) { -// resource nsgApimV1 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { - name: 'nsg-apim' - location: location - properties: { - securityRules: [ - // INBOUND Security Rules - { - name: 'AllowApimInbound' - properties: { - description: 'Allow Management endpoint for Azure portal and Powershell traffic' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '3443' - sourceAddressPrefix: 'ApiManagement' - destinationAddressPrefix: 'VirtualNetwork' - access: 'Allow' - priority: 100 - direction: 'Inbound' - } - } - { - name: 'AllowAzureLoadBalancerInbound' - properties: { - description: 'Allow Azure Load Balancer' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '6390' - sourceAddressPrefix: 'AzureLoadBalancer' - destinationAddressPrefix: apimSubnetPrefix - access: 'Allow' - priority: 110 - direction: 'Inbound' - } - } - // Limit ingress to traffic from App Gateway subnet, forcing both internal and external traffic to traverse App Gateway - { - name: 'AllowAppGatewayToApim' - properties: { - description: 'Allows inbound App Gateway traffic to APIM' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: appgwSubnetPrefix - destinationAddressPrefix: apimSubnetPrefix - access: 'Allow' - priority: 120 - direction: 'Inbound' - } - } - nsgsr_denyAllInbound - // OUTBOUND Security Rules - { - name: 'AllowApimToStorage' - properties: { - description: 'Allow APIM to reach Azure Storage endpoints for core service functionality (i.e. pull binaries to provision units, etc.)' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'Storage' - access: 'Allow' - priority: 100 - direction: 'Outbound' - } - } - { - name: 'AllowApimToSql' - properties: { - description: 'Allow APIM to reach Azure SQL endpoints for core service functionality' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '1433' - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'SQL' - access: 'Allow' - priority: 110 - direction: 'Outbound' - } - } - { - name: 'AllowApimToKeyVault' - properties: { - description: 'Allow APIM to reach Azure Key Vault endpoints for core service functionality' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRange: '443' - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'AzureKeyVault' - access: 'Allow' - priority: 120 - direction: 'Outbound' - } - } - { - name: 'AllowApimToMonitor' - properties: { - description: 'Allow APIM to reach Azure Monitor to publish diagnostics logs, metrics, resource health, and application insights' - protocol: 'TCP' - sourcePortRange: '*' - destinationPortRanges: [ - '1886' - '443' - ] - sourceAddressPrefix: 'VirtualNetwork' - destinationAddressPrefix: 'AzureMonitor' - access: 'Allow' - priority: 130 - direction: 'Outbound' - } - } - ] +// APIM V1 needs a specific NSG: https://learn.microsoft.com/azure/api-management/api-management-using-with-internal-vnet#configure-nsg-rules +// V2 tiers need outbound rules for Storage and Key Vault: https://learn.microsoft.com/azure/api-management/inject-vnet-v2#network-security-group +module nsgApimModule '../../shared/bicep/modules/vnet/v1/nsg-apim.bicep' = if (hasVNetSupport) { + name: 'nsgApimModule' + params: { + location: location + nsgName: 'nsg-apim' + apimSubnetPrefix: apimSubnetPrefix + allowAppGateway: true + appgwSubnetPrefix: appgwSubnetPrefix + apimSku: apimSku + vnetMode: apimVnetMode + } +} + +// NSG for Container Apps - only allow traffic from APIM +module nsgAcaModule '../../shared/bicep/modules/vnet/v1/nsg-aca.bicep' = if (useACA) { + name: 'nsgAcaModule' + params: { + location: location + nsgName: 'nsg-aca' + acaSubnetPrefix: acaSubnetPrefix + apimSubnetPrefix: apimSubnetPrefix } } @@ -295,7 +164,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: apimSubnetPrefix networkSecurityGroup: { - id: is_apim_sku_v1(apimSku) ? nsgApimV1.id : nsgDefault.id + id: hasVNetSupport ? nsgApimModule!.outputs.nsgId : nsgDefault.id } // Delegations need to be conditional. If using V1 SKU (Developer), then we cannot delegate the subnet, so we need to check for V2. delegations: is_apim_sku_v2(apimSku) ? [ @@ -314,7 +183,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: acaSubnetPrefix networkSecurityGroup: { - id: nsgDefault.id + id: useACA ? nsgAcaModule!.outputs.nsgId : nsgDefault.id } delegations: [ { @@ -332,7 +201,7 @@ module vnetModule '../../shared/bicep/modules/vnet/v1/vnet.bicep' = { properties: { addressPrefix: appgwSubnetPrefix networkSecurityGroup: { - id: nsgAppGw.id + id: nsgAppGwModule.outputs.nsgId } } } @@ -344,7 +213,51 @@ var apimSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${apimSubnetNa var acaSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${acaSubnetName}' var appgwSubnetResourceId = '${vnetModule.outputs.vnetId}/subnets/${appgwSubnetName}' -// 4. User Assigned Managed Identity +// 5. NSG Flow Logs and Traffic Analytics + +// NSG Flow Logs for Application Gateway +module nsgFlowLogsAppGwModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = { + name: 'nsgFlowLogsAppGwModule' + params: { + location: location + flowLogName: 'fl-nsg-appgw-${resourceSuffix}' + nsgResourceId: nsgAppGwModule.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// NSG Flow Logs for APIM +module nsgFlowLogsApimModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (hasVNetSupport) { + name: 'nsgFlowLogsApimModule' + params: { + location: location + flowLogName: 'fl-nsg-apim-${resourceSuffix}' + nsgResourceId: nsgApimModule!.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// NSG Flow Logs for ACA +module nsgFlowLogsAcaModule '../../shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep' = if (useACA) { + name: 'nsgFlowLogsAcaModule' + params: { + location: location + flowLogName: 'fl-nsg-aca-${resourceSuffix}' + nsgResourceId: nsgAcaModule!.outputs.nsgId + storageAccountResourceId: storageFlowLogsModule.outputs.storageAccountId + logAnalyticsWorkspaceResourceId: lawId + retentionDays: 7 + enableTrafficAnalytics: true + } +} + +// 6. User Assigned Managed Identity // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/managed-identity/user-assigned-identity module uamiModule 'br/public:avm/res/managed-identity/user-assigned-identity:0.4.2' = { name: 'uamiModule' @@ -354,7 +267,7 @@ module uamiModule 'br/public:avm/res/managed-identity/user-assigned-identity:0.4 } } -// 5. Key Vault +// 7. Key Vault // https://learn.microsoft.com/azure/templates/microsoft.keyvault/vaults // This assignment is helpful for testing to allow you to examine and administer the Key Vault. Adjust accordingly for real workloads! var keyVaultAdminRoleAssignment = setCurrentUserAsKeyVaultAdmin && !empty(currentUserId) ? [ @@ -387,7 +300,7 @@ module keyVaultModule 'br/public:avm/res/key-vault/vault:0.13.3' = { } } -// 6. Public IP for Application Gateway +// 8. Public IP for Application Gateway // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/public-ip-address module appgwPipModule 'br/public:avm/res/network/public-ip-address:0.9.1' = { name: 'appgwPipModule' @@ -400,7 +313,7 @@ module appgwPipModule 'br/public:avm/res/network/public-ip-address:0.9.1' = { } } -// 7. WAF Policy for Application Gateway +// 9. WAF Policy for Application Gateway // https://learn.microsoft.com/azure/templates/microsoft.network/applicationgatewaywebapplicationfirewallpolicies resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies@2025-01-01' = { name: 'waf-${resourceSuffix}' @@ -426,7 +339,7 @@ resource wafPolicy 'Microsoft.Network/ApplicationGatewayWebApplicationFirewallPo } } -// 8. Azure Container App Environment (ACAE) +// 10. Azure Container App Environment (ACAE) module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if (useACA) { name: 'acaEnvModule' params: { @@ -437,7 +350,7 @@ module acaEnvModule '../../shared/bicep/modules/aca/v1/environment.bicep' = if ( } } -// 9. Azure Container Apps (ACA) for Mock Web API +// 11. Azure Container Apps (ACA) for Mock Web API module acaModule1 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (useACA) { name: 'acaModule-1' params: { @@ -455,7 +368,7 @@ module acaModule2 '../../shared/bicep/modules/aca/v1/containerapp.bicep' = if (u } } -// 10. API Management (VNet Internal) +// 12. API Management (VNet Internal) module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = { name: 'apimModule' params: { @@ -470,7 +383,7 @@ module apimModule '../../shared/bicep/modules/apim/v1/apim.bicep' = { } } -// 11. APIM Policy Fragments +// 13. APIM Policy Fragments module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment.bicep' = [for pf in policyFragments: { name: 'pf-${pf.name}' params:{ @@ -484,7 +397,7 @@ module policyFragmentModule '../../shared/bicep/modules/apim/v1/policy-fragment. ] }] -// 12. APIM Backends for ACA +// 14. APIM Backends for ACA module backendModule1 '../../shared/bicep/modules/apim/v1/backend.bicep' = if (useACA) { name: 'aca-backend-1' params: { @@ -533,8 +446,8 @@ module backendPoolModule '../../shared/bicep/modules/apim/v1/backend-pool.bicep' ] } -// 13. APIM APIs -module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: if(length(apis) > 0) { +// 15. APIM APIs +module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in apis: { name: 'api-${api.name}' params: { apimName: apimName @@ -544,15 +457,15 @@ module apisModule '../../shared/bicep/modules/apim/v1/api.bicep' = [for api in a } dependsOn: useACA ? [ apimModule - backendModule1 - backendModule2 - backendPoolModule + backendModule1! + backendModule2! + backendPoolModule! ] : [ apimModule ] }] -// 14. Application Gateway +// 16. Application Gateway // https://github.com/Azure/bicep-registry-modules/tree/main/avm/res/network/application-gateway module appgwModule 'br/public:avm/res/network/application-gateway:0.7.2' = { name: 'appgwModule' diff --git a/shared/bicep/modules/vnet/v1/nsg-aca.bicep b/shared/bicep/modules/vnet/v1/nsg-aca.bicep new file mode 100644 index 00000000..c8836d43 --- /dev/null +++ b/shared/bicep/modules/vnet/v1/nsg-aca.bicep @@ -0,0 +1,108 @@ +/** + * @module nsg-aca-v1 + * @description Network Security Group for Azure Container Apps allowing traffic only from API Management + */ + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@description('Location for the NSG') +param location string = resourceGroup().location + +@description('Name of the NSG') +param nsgName string = 'nsg-aca' + +@description('ACA subnet prefix for destination filtering') +param acaSubnetPrefix string + +@description('APIM subnet prefix for source filtering') +param apimSubnetPrefix string + +// Import the deny all inbound rule +import {nsgsr_denyAllInbound} from './nsg_rules.bicep' + +// ------------------------------ +// RESOURCES +// ------------------------------ + +// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups +resource nsgAca 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { + name: nsgName + location: location + properties: { + securityRules: [ + // INBOUND Security Rules + { + name: 'AllowApimToAca' + properties: { + description: 'Allow inbound HTTPS traffic from APIM to Container Apps' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: apimSubnetPrefix + destinationAddressPrefix: acaSubnetPrefix + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'AllowAzureLoadBalancerInbound' + properties: { + description: 'Allow Azure Load Balancer health probes for Container Apps' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: acaSubnetPrefix + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + { + name: 'AllowAcaControlPlane' + properties: { + description: 'Allow Container Apps control plane communication' + protocol: '*' + sourcePortRange: '*' + destinationPortRanges: [ + '443' + '4789' + '5671' + '5672' + ] + sourceAddressPrefix: 'MicrosoftContainerRegistry' + destinationAddressPrefix: acaSubnetPrefix + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + nsgsr_denyAllInbound + // OUTBOUND Security Rules + { + name: 'AllowAcaToInternet' + properties: { + description: 'Allow Container Apps to reach internet for container image pulls and other dependencies' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: acaSubnetPrefix + destinationAddressPrefix: 'Internet' + access: 'Allow' + priority: 100 + direction: 'Outbound' + } + } + ] + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + +output nsgId string = nsgAca.id +output nsgName string = nsgAca.name diff --git a/shared/bicep/modules/vnet/v1/nsg-apim.bicep b/shared/bicep/modules/vnet/v1/nsg-apim.bicep new file mode 100644 index 00000000..2d4afaa9 --- /dev/null +++ b/shared/bicep/modules/vnet/v1/nsg-apim.bicep @@ -0,0 +1,255 @@ +/** + * @module nsg-apim-v1 + * @description Network Security Group for Azure API Management in VNet mode. + * Supports inbound traffic from Application Gateway, Azure Front Door (via Private Link), or both. + * Inbound rules (management, load balancer, deny all) always apply. + * App Gateway and Front Door inbound rules are conditionally included via parameters. + * Outbound rules are conditionally included based on the VNet mode and APIM SKU: + * - Storage and Key Vault are required for all tiers (injection and integration alike). + * - SQL and Monitor are additionally required for classic VNet-injection tiers (Developer, Premium). + * + * INBOUND NSG rule matrix (as of 03/02/2026): + * PE = Private Endpoint-backed inbound path. + * If allowAppGateway = Application Gateway scenario. + * If allowFrontDoorBackend = Azure Front Door via PE scenario. + * + * SKU | VNet Mode | APIM Mgmt (3443) | Load Balancer (6390) | App Gateway (443) | Front Door PE (443) | Deny All + * -------------|-------------|------------------|----------------------|--------------------|--------------------------|--------- + * Developer | injection | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Basic | (none) | - | - | - | - | - + * Standard | (none) | - | - | - | - | - + * Premium | injection | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Basicv2 | integration | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Standardv2 | injection | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Standardv2 | integration | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Premiumv2 | injection | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * Premiumv2 | integration | Yes | Yes | If allowAppGateway | If allowFrontDoorBackend | Yes + * + * OUTBOUND NSG rule matrix (as of 03/02/2026): + * + * SKU | VNet Mode | Storage | Key Vault | SQL | Monitor + * -------------|-------------|------------------|----------------------|--------------------|---------- + * Developer | injection | Yes | Yes | Yes | Yes + * Basic | (none) | - | - | - | - + * Standard | (none) | - | - | - | - + * Premium | injection | Yes | Yes | Yes | Yes + * Basicv2 | integration | Yes | Yes | No | No + * Standardv2 | injection | Yes | Yes | No | No + * Standardv2 | integration | Yes | Yes | No | No + * Premiumv2 | injection | Yes | Yes | No | No + * Premiumv2 | integration | Yes | Yes | No | No + * + * @see Classic tiers - required NSG rules for VNet injection: + * https://learn.microsoft.com/azure/api-management/api-management-using-with-internal-vnet#configure-nsg-rules + * @see V2 tiers - VNet integration (outbound only, no specific NSG rules required): + * https://learn.microsoft.com/azure/api-management/integrate-vnet-outbound#network-security-group + * @see V2 tiers - VNet injection (outbound rules for Storage and Key Vault, but not SQL or Monitor): + * https://learn.microsoft.com/azure/api-management/inject-vnet-v2#network-security-group + * @see Comprehensive VNet reference (all tiers): + * https://learn.microsoft.com/azure/api-management/virtual-network-reference + */ + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@description('Location for the NSG') +param location string = resourceGroup().location + +@description('Name of the NSG') +param nsgName string = 'nsg-apim' + +@description('APIM subnet prefix for destination filtering') +param apimSubnetPrefix string + +@description('Whether to allow inbound HTTPS traffic from an Application Gateway subnet') +param allowAppGateway bool = false + +@description('Application Gateway subnet prefix for source filtering (required when allowAppGateway is true)') +param appgwSubnetPrefix string = '' + +@description('Whether to allow inbound HTTPS traffic from Azure Front Door Backend service tag (via Private Link)') +param allowFrontDoorBackend bool = false + +@description('APIM SKU name. Classic tiers (Developer, Premium) with injection require additional outbound NSG rules for SQL and Monitor.') +param apimSku string + +@allowed([ + 'injection' + 'integration' +]) +@description('VNet mode for the APIM instance. Classic tiers with injection require SQL and Monitor outbound rules beyond the baseline Storage and Key Vault rules.') +param vnetMode string + +// Import the deny all inbound rule +import {nsgsr_denyAllInbound} from './nsg_rules.bicep' + + +// ------------------------------ +// CONSTANTS +// ------------------------------ + +// Classic tiers that additionally require SQL and Monitor outbound rules when VNet-injected +var CLASSIC_SKUS = ['Developer', 'Premium'] + + +// ------------------------------ +// VARIABLES +// ------------------------------ + +var isClassicInjection = vnetMode == 'injection' && contains(CLASSIC_SKUS, apimSku) + + +// ------------------------------ +// RESOURCES +// ------------------------------ + +// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups +resource nsgApim 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { + name: nsgName + location: location + properties: { + securityRules: concat( + // INBOUND Security Rules + [ + { + name: 'AllowApimManagement' + properties: { + description: 'Allow Management endpoint for Azure portal and PowerShell traffic' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '3443' + sourceAddressPrefix: 'ApiManagement' + destinationAddressPrefix: 'VirtualNetwork' + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'AllowAzureLoadBalancerInbound' + properties: { + description: 'Allow Azure Load Balancer health probes' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '6390' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: apimSubnetPrefix + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + ], + // INBOUND: Application Gateway (conditional) + allowAppGateway && !empty(appgwSubnetPrefix) ? [ + { + name: 'AllowAppGatewayToApim' + properties: { + description: 'Allow inbound HTTPS traffic from Application Gateway to APIM' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: appgwSubnetPrefix + destinationAddressPrefix: apimSubnetPrefix + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + ] : [], + // INBOUND: Azure Front Door Backend via Private Link (conditional) + allowFrontDoorBackend ? [ + { + name: 'AllowFrontDoorBackendToApim' + properties: { + description: 'Allow inbound HTTPS traffic from Azure Front Door Backend to APIM via Private Link' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'AzureFrontDoor.Backend' + destinationAddressPrefix: apimSubnetPrefix + access: 'Allow' + priority: 130 + direction: 'Inbound' + } + } + ] : [], + [ + nsgsr_denyAllInbound + ], + // OUTBOUND: Storage + Key Vault — required for all tiers (injection and integration) + [ + { + name: 'AllowApimToStorage' + properties: { + description: 'Allow APIM to reach Azure Storage for core service functionality' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'Storage' + access: 'Allow' + priority: 100 + direction: 'Outbound' + } + } + { + name: 'AllowApimToKeyVault' + properties: { + description: 'Allow APIM to reach Azure Key Vault for core service functionality' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'AzureKeyVault' + access: 'Allow' + priority: 110 + direction: 'Outbound' + } + } + ], + // OUTBOUND: SQL + Monitor — additionally required for classic VNet-injected tiers (Developer, Premium) + isClassicInjection ? [ + { + name: 'AllowApimToSql' + properties: { + description: 'Allow APIM to reach Azure SQL for core service functionality' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '1433' + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'Sql' + access: 'Allow' + priority: 120 + direction: 'Outbound' + } + } + { + name: 'AllowApimToMonitor' + properties: { + description: 'Allow APIM to reach Azure Monitor for diagnostics logs, metrics, and Application Insights' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRanges: [ + '1886' + '443' + ] + sourceAddressPrefix: 'VirtualNetwork' + destinationAddressPrefix: 'AzureMonitor' + access: 'Allow' + priority: 130 + direction: 'Outbound' + } + } + ] : [] + ) + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + +output nsgId string = nsgApim.id +output nsgName string = nsgApim.name diff --git a/shared/bicep/modules/vnet/v1/nsg-appgw.bicep b/shared/bicep/modules/vnet/v1/nsg-appgw.bicep new file mode 100644 index 00000000..41e93d7f --- /dev/null +++ b/shared/bicep/modules/vnet/v1/nsg-appgw.bicep @@ -0,0 +1,87 @@ +/** + * @module nsg-appgw-v1 + * @description Network Security Group for Azure Application Gateway subnets. + * Includes the required inbound rules for GatewayManager, HTTPS listener traffic, + * Azure Load Balancer probes, and a final deny-all inbound rule. + * + * @see Application Gateway infrastructure configuration: + * https://learn.microsoft.com/azure/application-gateway/configuration-infrastructure + */ + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@description('Location for the NSG') +param location string = resourceGroup().location + +@description('Name of the NSG') +param nsgName string = 'nsg-appgw' + +// Import the deny all inbound rule +import {nsgsr_denyAllInbound} from './nsg_rules.bicep' + + +// ------------------------------ +// RESOURCES +// ------------------------------ + +// https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups +resource nsgAppGw 'Microsoft.Network/networkSecurityGroups@2025-01-01' = { + name: nsgName + location: location + properties: { + securityRules: [ + { + name: 'AllowGatewayManagerInbound' + properties: { + description: 'Allow Azure infrastructure communication' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '65200-65535' + sourceAddressPrefix: 'GatewayManager' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 100 + direction: 'Inbound' + } + } + { + name: 'AllowHTTPSInbound' + properties: { + description: 'Allow HTTPS listener traffic to Application Gateway' + protocol: 'Tcp' + sourcePortRange: '*' + destinationPortRange: '443' + sourceAddressPrefix: '*' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 110 + direction: 'Inbound' + } + } + { + name: 'AllowAzureLoadBalancerInbound' + properties: { + description: 'Allow Azure Load Balancer health probes' + protocol: '*' + sourcePortRange: '*' + destinationPortRange: '*' + sourceAddressPrefix: 'AzureLoadBalancer' + destinationAddressPrefix: '*' + access: 'Allow' + priority: 120 + direction: 'Inbound' + } + } + nsgsr_denyAllInbound + ] + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + +output nsgId string = nsgAppGw.id +output nsgName string = nsgAppGw.name diff --git a/shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep b/shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep new file mode 100644 index 00000000..c2a8a65d --- /dev/null +++ b/shared/bicep/modules/vnet/v1/nsg-flow-logs.bicep @@ -0,0 +1,82 @@ +/** + * @module nsg-flow-logs-v1 + * @description Enable NSG Flow Logs and Traffic Analytics for Network Security Groups + */ + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@description('Location for resources') +param location string = resourceGroup().location + +@description('Name of the NSG Flow Log') +param flowLogName string + +@description('NSG Resource ID to enable flow logs on') +param nsgResourceId string + +@description('Storage Account Resource ID for flow log storage') +param storageAccountResourceId string + +@description('Log Analytics Workspace Resource ID for Traffic Analytics') +param logAnalyticsWorkspaceResourceId string + +@description('Flow log retention in days (0 = indefinite)') +@minValue(0) +@maxValue(365) +param retentionDays int = 7 + +@description('Flow log version (1 or 2)') +@allowed([1, 2]) +param flowLogVersion int = 2 + +@description('Enable Traffic Analytics') +param enableTrafficAnalytics bool = true + +@description('Traffic Analytics interval in minutes') +@allowed([10, 60]) +param trafficAnalyticsInterval int = 60 + +// ------------------------------ +// RESOURCES +// ------------------------------ + +// Network Watcher - using existing instance in the region +resource networkWatcher 'Microsoft.Network/networkWatchers@2025-01-01' existing = { + name: 'NetworkWatcher_${location}' +} + +// https://learn.microsoft.com/azure/templates/microsoft.network/networkwatchers/flowlogs +resource flowLog 'Microsoft.Network/networkWatchers/flowLogs@2025-01-01' = { + name: flowLogName + parent: networkWatcher + location: location + properties: { + targetResourceId: nsgResourceId + storageId: storageAccountResourceId + enabled: true + retentionPolicy: { + days: retentionDays + enabled: retentionDays > 0 + } + format: { + type: 'JSON' + version: flowLogVersion + } + flowAnalyticsConfiguration: enableTrafficAnalytics ? { + networkWatcherFlowAnalyticsConfiguration: { + enabled: true + workspaceResourceId: logAnalyticsWorkspaceResourceId + trafficAnalyticsInterval: trafficAnalyticsInterval + } + } : null + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + +output flowLogId string = flowLog.id +output flowLogName string = flowLog.name diff --git a/shared/bicep/modules/vnet/v1/storage-flowlogs.bicep b/shared/bicep/modules/vnet/v1/storage-flowlogs.bicep new file mode 100644 index 00000000..2ff9bad7 --- /dev/null +++ b/shared/bicep/modules/vnet/v1/storage-flowlogs.bicep @@ -0,0 +1,69 @@ +/** + * @module storage-account-flowlogs-v1 + * @description Storage Account for NSG Flow Logs + */ + +// ------------------------------ +// PARAMETERS +// ------------------------------ + +@description('Location for the storage account') +param location string = resourceGroup().location + +@description('The unique suffix to append') +param resourceSuffix string = uniqueString(subscription().id, resourceGroup().id) + +@description('Storage account name (must be globally unique, 3-24 chars, lowercase alphanumeric)') +@minLength(3) +@maxLength(24) +param storageAccountName string = 'stflowlogs${take(resourceSuffix, 13)}' + +@description('Storage account SKU') +@allowed([ + 'Standard_LRS' + 'Standard_GRS' + 'Standard_RAGRS' + 'Standard_ZRS' +]) +param skuName string = 'Standard_LRS' + +// ------------------------------ +// RESOURCES +// ------------------------------ + +// https://learn.microsoft.com/azure/templates/microsoft.storage/storageaccounts +resource storageAccount 'Microsoft.Storage/storageAccounts@2023-05-01' = { + name: storageAccountName + location: location + sku: { + name: skuName + } + kind: 'StorageV2' + properties: { + minimumTlsVersion: 'TLS1_2' + allowBlobPublicAccess: false + supportsHttpsTrafficOnly: true + encryption: { + services: { + blob: { + enabled: true + } + file: { + enabled: true + } + } + keySource: 'Microsoft.Storage' + } + networkAcls: { + bypass: 'AzureServices' + defaultAction: 'Allow' + } + } +} + +// ------------------------------ +// OUTPUTS +// ------------------------------ + +output storageAccountId string = storageAccount.id +output storageAccountName string = storageAccount.name