Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Azure NAT Gateway in Hub Network for Explicit Outbound Connectivity #1140

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 3 additions & 1 deletion docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ Mission LZ has the following scope:

## Networking

Networking is set up in a hub and spoke design, separated by tiers: T0 (Identity and Authorization), T1 (Infrastructure Operations), T2 (DevSecOps and Shared Services), and multiple T3s (Workloads). Access control can be configured to allow separation of duties between all tiers.
Networking is set up in a [hub and spoke design](https://learn.microsoft.com/en-us/azure/architecture/networking/architecture/hub-spoke), separated by tiers: T0 (Identity and Authorization), T1 (Infrastructure Operations), T2 (DevSecOps and Shared Services), and multiple T3s (Workloads). Access control can be configured to allow separation of duties between all tiers.

<!-- markdownlint-disable MD033 -->
<!-- allow html for images so that they can be sized -->
Expand All @@ -30,6 +30,8 @@ Networking is set up in a hub and spoke design, separated by tiers: T0 (Identity

Each virtual network has been given a default address prefix to ensure they fall within the default super network. Refer to the [Networking page](./networking.md) for all the default address prefixes.

The deployment of an Azure NAT Gateway in the Hub Network has been implemented to enable explicit outbound connectivity to align with Azure Roadmap guidance that [default outbound access will be retired September 30 2025](https://azure.microsoft.com/en-us/updates?id=default-outbound-access-for-vms-in-azure-will-be-retired-transition-to-a-new-method-of-internet-access). It is implemented with an [Azure Public IP Prefix to prevent SNAT port exhaustion](https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/configure-public-ip-nat-gateway#add-public-ip-prefix), allowing deployment of 2, 4, 8 or 16 Public IPs in the Prefix via the `natGatewayPublicIpPrefixLength` paramter.

## Subscriptions

Most customers will deploy each tier to a separate Azure subscription, but multiple subscriptions are not required. A single subscription deployment is good for a testing and evaluation, or possibly a small IT Admin team.
Expand Down
2 changes: 2 additions & 0 deletions src/bicep/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ Parameter name | Required | Description
`policy` | No | [NIST/IL5/CMMC] Built-in policy assignments to assign, it defaults to "NIST". IL5 is only available for AzureUsGovernment and will switch to NIST if tried in AzureCloud.
`deployDefender` | No | When set to "true", enables Microsoft Defender for Cloud for the subscriptions used in the deployment. It defaults to "false".
`emailSecurityContact` | No | Email address of the contact, in the form of [email protected]
`deployAzureNATGateway` | No | When set to "true", provisions Azure NAT Gateway with Private IP Prefix. It defaults to "true" to align to Azure retiring default outbound access September 30 2025.
`natGatewayPublicIpPrefixLength` | No | Length of the Public IP Prefix for the Azure NAT Gateway. A NAT gateway can support the following prefix sizes: /28 (16 addresses), /29 (8 addresses), /30 (4 addresses), and /31 (2 addresses). Defaults to 31.
<!-- markdownlint-enable MD034 -->

## Outputs
Expand Down
2 changes: 2 additions & 0 deletions src/bicep/data/resourceAbbreviations.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"ipConfigurations": "ipconf",
"keyVaults": "kv",
"logAnalyticsWorkspaces": "log",
"natGateway": "ngw",
"netAppAccounts": "naa",
"netAppCapacityPools": "nacp",
"networkInterfaces": "nic",
Expand All @@ -29,6 +30,7 @@
"privateEndpoints": "pe",
"privateLinkScopes": "pls",
"publicIPAddresses": "pip",
"publicIpPrefixes": "pipr",
"recoveryServicesVaults": "rsv",
"remoteApplicationGroups": "vdag",
"resourceGroups": "rg",
Expand Down
8 changes: 8 additions & 0 deletions src/bicep/mlz.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -336,6 +336,12 @@ param deployBastion bool = false
@description('When set to "true", provisions Azure Gateway Subnet only. It defaults to "false".')
param deployAzureGatewaySubnet bool = false

@description('When set to "true", provisions Azure NAT Gateway with Private IP Prefix. It defaults to "true" to align to Azure retiring default outbound access September 30 2025.')
param deployAzureNATGateway bool = true

@description('Length of the Public IP Prefix for the Azure NAT Gateway. A NAT gateway can support the following prefix sizes: /28 (16 addresses), /29 (8 addresses), /30 (4 addresses), and /31 (2 addresses)')
param natGatewayPublicIpPrefixLength int = 31

@description('When set to "true", provisions Windows Virtual Machine Host only. It defaults to "false".')
param deployWindowsVirtualMachine bool = false

Expand Down Expand Up @@ -605,6 +611,7 @@ module networking 'modules/networking.bicep' = {
deployNetworkWatcher: deployNetworkWatcher
deployBastion: deployBastion
deployAzureGatewaySubnet: deployAzureGatewaySubnet
deployAzureNATGateway: deployAzureNATGateway
dnsServers: dnsServers
enableProxy: enableProxy
firewallSettings: {
Expand All @@ -620,6 +627,7 @@ module networking 'modules/networking.bicep' = {
}
location: location
mlzTags: logic.outputs.mlzTags
natGatewayPublicIpPrefixLength: natGatewayPublicIpPrefixLength
privateDnsZoneNames: logic.outputs.privateDnsZones
resourceGroupNames: resourceGroups.outputs.names
tags: tags
Expand Down
37 changes: 36 additions & 1 deletion src/bicep/modules/hub-network.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ param azureGatewaySubnetAddressPrefix string
param deployNetworkWatcher bool
param deployBastion bool
param deployAzureGatewaySubnet bool
param deployAzureNATGateway bool
param dnsServers array
param enableProxy bool
param firewallClientPrivateIpAddress string
Expand Down Expand Up @@ -36,6 +37,9 @@ param firewallSupernetIPAddress string
param firewallThreatIntelMode string
param location string
param mlzTags object
param natGatewayName string
param natGatewayPublicIpPrefixName string
param natGatewayPublicIpPrefixLength int
param networkSecurityGroupName string
param networkSecurityGroupRules array
param networkWatcherName string
Expand All @@ -52,6 +56,7 @@ var subnets = union([
name: 'AzureFirewallSubnet'
properties: {
addressPrefix: firewallClientSubnetAddressPrefix
natGateway: deployAzureNATGateway ? { id: natGateway.outputs.id } : null
}
}
{
Expand All @@ -64,6 +69,7 @@ var subnets = union([
name: subnetName
properties: {
addressPrefix: subnetAddressPrefix
natGateway: deployAzureNATGateway ? { id: natGateway.outputs.id } : null
networkSecurityGroup: {
id: networkSecurityGroup.outputs.id
}
Expand Down Expand Up @@ -322,6 +328,32 @@ module firewall '../modules/firewall.bicep' = {
}
}

module natGatewayPublicIpPrefix '../modules/public-ip-prefix.bicep' = if (deployAzureNATGateway) {
name: 'natGatewayPublicIpPrefix'
params: {
location: location
mlzTags: mlzTags
name: natGatewayPublicIpPrefixName
prefixLength: natGatewayPublicIpPrefixLength
tags: tags
}
}

module natGateway '../modules/nat-gateway.bicep' = if (deployAzureNATGateway) {
name: 'natGateway'
params: {
location: location
mlzTags: mlzTags
name: natGatewayName
publicIPPrefixResourceIds: [
{
id: natGatewayPublicIpPrefix.outputs.id
}
]
tags: tags
}
}

output bastionHostSubnetResourceId string = deployBastion ? virtualNetwork.outputs.subnets[3].id : ''
output dnsServers array = virtualNetwork.outputs.dnsServers
output firewallName string = firewall.outputs.name
Expand All @@ -334,4 +366,7 @@ output subnetName string = virtualNetwork.outputs.subnets[2].name
output subnetResourceId string = virtualNetwork.outputs.subnets[2].id
output virtualNetworkName string = virtualNetwork.outputs.name
output virtualNetworkResourceId string = virtualNetwork.outputs.id

output natGatewayPublicIpPrefixName string = natGatewayPublicIpPrefix.outputs.name
output natGatewayPublicIpPrefixResourceId string = natGatewayPublicIpPrefix.outputs.id
output natGatewayName string = natGateway.outputs.name
output natGatewayResourceId string = natGateway.outputs.id
2 changes: 2 additions & 0 deletions src/bicep/modules/naming-convention.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ var names = {
keyVaultPrivateEndpoint: replace(replace(namingConvention_Service, tokens.resource, resourceAbbreviations.privateEndpoints), tokens.service, '${resourceAbbreviations.keyVaults}-${tokens.service}')
logAnalyticsWorkspace: replace(namingConvention, tokens.resource, resourceAbbreviations.logAnalyticsWorkspaces)
logAnalyticsWorkspaceDiagnosticSetting: replace(replace(namingConvention_Service, tokens.resource, resourceAbbreviations.diagnosticSettings), tokens.service, resourceAbbreviations.logAnalyticsWorkspaces)
natGateway: replace(namingConvention, tokens.resource, resourceAbbreviations.natGateway)
natGatewayPublicIpPrefix: replace(namingConvention, tokens.resource, resourceAbbreviations.publicIpPrefixes)
netAppAccountCapacityPool: replace(namingConvention, tokens.resource, resourceAbbreviations.netAppCapacityPools)
netAppAccount: replace(namingConvention, tokens.resource, resourceAbbreviations.netAppAccounts)
networkSecurityGroup: replace(namingConvention, tokens.resource, resourceAbbreviations.networkSecurityGroups)
Expand Down
51 changes: 51 additions & 0 deletions src/bicep/modules/nat-gateway.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT License.
*/

@description('Optional. The idle timeout of the NAT gateway.')
param idleTimeoutInMinutes int = 5

@description('Optional. Location for all resources.')
param location string

param mlzTags object

@description('Required. Name of the Azure NAT Gateway resource.')
param name string

@description('Optional. Existing Public IP Address resource IDs to use for the NAT Gateway.')
param publicIpResourceIds array = []

@description('Optional. Existing Public IP Prefixes resource IDs to use for the NAT Gateway.')
param publicIPPrefixResourceIds array = []

@description('Optional. Tags for the resource.')
param tags object = {}

@description('Optional. A list of availability zones denoting the zone in which Nat Gateway should be deployed.')
@allowed([
0
1
2
3
])
param zone int = 0

resource natGateway 'Microsoft.Network/natGateways@2024-05-01' = {
name: name
location: location
tags: union(contains(tags, 'Microsoft.Network/natGateways') ? tags['Microsoft.Network/natGateways'] : {}, mlzTags)
sku: {
name: 'Standard'
}
properties: {
idleTimeoutInMinutes: idleTimeoutInMinutes
publicIpAddresses: publicIpResourceIds
publicIpPrefixes: publicIPPrefixResourceIds
}
zones: zone != 0 ? [string(zone)] : []
}

output name string = natGateway.name
output id string = natGateway.id
6 changes: 6 additions & 0 deletions src/bicep/modules/networking.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ param deploymentNameSuffix string
param deployNetworkWatcher bool
param deployBastion bool
param deployAzureGatewaySubnet bool
param deployAzureNATGateway bool
param dnsServers array
param enableProxy bool
param firewallSettings object
param location string
param mlzTags object
param natGatewayPublicIpPrefixLength int
param privateDnsZoneNames array
param resourceGroupNames array
param tiers array
Expand All @@ -37,6 +39,7 @@ module hubNetwork 'hub-network.bicep' = {
deployNetworkWatcher: deployNetworkWatcher
deployBastion: deployBastion
deployAzureGatewaySubnet: deployAzureGatewaySubnet
deployAzureNATGateway: deployAzureNATGateway
dnsServers: dnsServers
enableProxy: enableProxy
firewallClientPrivateIpAddress: firewallSettings.clientPrivateIpAddress
Expand All @@ -54,6 +57,9 @@ module hubNetwork 'hub-network.bicep' = {
firewallThreatIntelMode: firewallSettings.threatIntelMode
location: location
mlzTags: mlzTags
natGatewayName: hub.namingConvention.natGateway
natGatewayPublicIpPrefixName: hub.namingConvention.natGatewayPublicIpPrefix
natGatewayPublicIpPrefixLength: natGatewayPublicIpPrefixLength
networkSecurityGroupName: hub.namingConvention.networkSecurityGroup
networkSecurityGroupRules: hub.nsgRules
networkWatcherName: hub.namingConvention.networkWatcher
Expand Down
68 changes: 68 additions & 0 deletions src/bicep/modules/public-ip-prefix.bicep
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
/*
Copyright (c) Microsoft Corporation.
Licensed under the MIT License.
*/

@description('Optional. The custom IP address prefix that this prefix is associated with. A custom IP address prefix is a contiguous range of IP addresses owned by an external customer and provisioned into a subscription. When a custom IP prefix is in Provisioned, Commissioning, or Commissioned state, a linked public IP prefix can be created. Either as a subset of the custom IP prefix range or the entire range.')
param customIPPrefix object = {}

@description('Optional. Location for all resources.')
param location string

param mlzTags object

@description('Required. The name of the Public IP Prefix.')
@minLength(1)
param name string

@description('Required. Length of the Public IP Prefix.')
@minValue(21)
@maxValue(127)
param prefixLength int

@description('Optional. The public IP address version.')
@allowed([
'IPv4'
'IPv6'
])
param publicIPAddressVersion string = 'IPv4'

param tags object = {}

@description('Optional. Tier of a public IP prefix SKU. If set to `Global`, the `zones` property must be empty.')
@allowed([
'Global'
'Regional'
])
param tier string = 'Regional'

@description('Optional. A list of availability zones denoting the IP allocated for the resource needs to come from. This is only applicable for regional public IP prefixes and must be empty for global public IP prefixes.')
@allowed([
1
2
3
])
param zones int[] = [
1
2
3
]

resource publicIpPrefix 'Microsoft.Network/publicIPPrefixes@2024-05-01' = {
name: name
location: location
tags: union(contains(tags, 'Microsoft.Network/publicIPPrefixes') ? tags['Microsoft.Network/publicIPPrefixes'] : {}, mlzTags)
sku: {
name: 'Standard'
tier: tier
}
zones: map(zones, zone => string(zone))
properties: {
customIPPrefix: !empty(customIPPrefix) ? customIPPrefix : null
publicIPAddressVersion: publicIPAddressVersion
prefixLength: prefixLength
}
}

output name string = publicIpPrefix.name
output id string = publicIpPrefix.id
Loading