Skip to content

Commit

Permalink
feat(new): Added Azure.VNET.FirewallSubnetNAT (#3006)
Browse files Browse the repository at this point in the history
* feat(new): Added Azure.VNET.FirewallSubnetNAT

* Rule updates

* Fix test

* Fix bug

* Move to configuration block

* Fix tests

---------

Co-authored-by: Bernie White <bewhite@microsoft.com>
  • Loading branch information
BenjaminEngeset and BernieWhite authored Aug 6, 2024
1 parent a8850b7 commit 46487c9
Show file tree
Hide file tree
Showing 10 changed files with 317 additions and 16 deletions.
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@
"SBOM",
"SIEM",
"SKUs",
"SNAT",
"stateful",
"subnet",
"subnets",
Expand Down
14 changes: 7 additions & 7 deletions docs/CHANGELOG-v1.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,21 +29,21 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers

## Unreleased

What's changed since pre-release v1.39.0-B0009:

- New rules:
- Azure Kubernetes Service:
- Verify that clusters have kube-audit logging disabled when not required by @BenjaminEngeset.
[#2450](https://github.com/Azure/PSRule.Rules.Azure/issues/2450)
- Verify that clusters have the customer-controlled maintenance windows 'aksManagedAutoUpgradeSchedule' and 'aksManagedNodeOSUpgradeSchedule' configured by @BenjaminEngeset.
[#2444](https://github.com/Azure/PSRule.Rules.Azure/issues/2444)
- Virtual Network:
- Verify that zonal-deployed Azure firewalls uses Azure NAT Gateway for outbound access by @BenjaminEngeset.
[##3005](https://github.com/Azure/PSRule.Rules.Azure/issues/#3005)
- Updated rules:
- Virtual Network:
- Updated `Azure.VNET.UseNSGs` to correctly handle cases for special purpose and customer-excluded subnets by @BenjaminEngeset.
[#3007](https://github.com/Azure/PSRule.Rules.Azure/issues/3007)

What's changed since pre-release v1.39.0-B0009:

- New rules:
- Azure Kubernetes Service:
- Verify that clusters have kube-audit logging disabled when not required by @BenjaminEngeset.
[#2450](https://github.com/Azure/PSRule.Rules.Azure/issues/2450)
- General improvements:
- Add binding configuration to policy as rules docs by @BernieWhite.
[#2995](https://github.com/Azure/PSRule.Rules.Azure/issues/2995)
Expand Down
144 changes: 144 additions & 0 deletions docs/en/rules/Azure.VNET.FirewallSubnetNAT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
---
severity: Awareness
pillar: Reliability
category: RE:05 Redundancy
resource: Virtual Network
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.VNET.FirewallSubnetNAT/
---

# Use NAT gateway with Azure Firewall for outbound access

## SYNOPSIS

Zonal-deployed Azure Firewalls should consider using an Azure NAT Gateway for outbound access.

## DESCRIPTION

Azure Firewall can be deployed with up to 250 public IP addresses, each providing 2,496 SNAT ports. This setup offers a maximum of 1,248,000 SNAT ports.

Managing a large number of public IP addresses comes with challenges, particularly regarding downstream IP address filtering requirements. When Azure Firewall is associated with multiple public IP addresses, these filtering requirements must be applied to all associated addresses.
Even when using Public IP address prefixes, associating 250 public IP addresses requires managing 16 public IP address prefixes on the downstream side.

A more efficient solution for scaling and dynamically allocating outbound SNAT ports is to use an Azure NAT Gateway:

- High Capacity: Each public IP address on a NAT Gateway provides 64,512 SNAT ports, and up to 16 public IP addresses can be associated, resulting in up to 1,032,192 SNAT ports.
- Dynamic Allocation: SNAT ports are dynamically allocated at the subnet level, making all provided SNAT ports available on demand for outbound connectivity.

This configuration simplifies management for downstream systems, as it requires handling only up to 16 public IP addresses.

When an Azure NAT Gateway is associated with an Azure Firewall subnet:

- All outbound internet traffic uses the NAT Gateway’s public IP addresses.
- Response traffic for outbound flows also passes through the NAT Gateway.
- If multiple public IP addresses are associated with the NAT Gateway, the IP address used is randomly selected, and specific addresses cannot be chosen.

**Important** Azure NAT Gateway supports only zonal deployment. Therefore, only zonal-deployed Azure Firewalls should utilize Azure NAT Gateway.
Azure Firewalls with zone redundancy might face reduced availability if a NAT Gateway is deployed in a single zone that experiences a failure.

## RECOMMENDATION

Consider using an Azure NAT gateway for zonal-deployed Azure Firewalls for outbound access.

## EXAMPLES

### Configure with Azure template

To configure virtual networks that pass this rule:

- For the `AzureFirewallSubnet` subnet in defined the `properties.subnets` property:
- Set the `properties.natGateway.id` property to the resource id of the NAT gateway.

For example:

```json
{
"type": "Microsoft.Network/virtualNetworks",
"apiVersion": "2023-11-01",
"name": "[parameters('name')]",
"location": "[parameters('location')]",
"properties": {
"addressSpace": {
"addressPrefixes": [
"10.0.0.0/16"
]
},
"subnets": [
{
"name": "AzureFirewallSubnet",
"properties": {
"addressPrefix": "10.0.0.0/26",
"natGateway": {
"id": "[parameters('natGatewayResourceId')]",
}
}
}
]
}
}
```

### Configure with Bicep

To configure virtual networks that pass this rule:

- For the `AzureFirewallSubnet` subnet in defined the `properties.subnets` property:
- Set the `properties.natGateway.id` property to the resource id of the NAT gateway.

For example:

```bicep
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
name: name
location: location
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/16'
]
}
subnets: [
{
name: 'AzureFirewallSubnet'
properties: {
addressPrefix: '10.0.0.0/26'
natGateway: {
id: natGatewayResourceId
}
}
}
]
}
}
```

## NOTES

This rule applies if you're environment requires Azure Firewall deployed in a zonal configuration for outbound Internet access.

This rule is not applicable if:

- Azure Firewall is deployed across multiple availability zones.
- Force tunneling mode is configured.

### Rule configuration

<!-- module:config rule AZURE_FIREWALL_IS_ZONAL -->

By default, this rule is ignored.
For this rule to apply, set the `AZURE_FIREWALL_IS_ZONAL` configuration value to `true`.

For example:

```yaml
configuration:
AZURE_FIREWALL_IS_ZONAL: true
```
## LINKS
- [RE:05 Redundancy](https://learn.microsoft.com/azure/well-architected/reliability/redundancy)
- [Scale SNAT ports with Azure NAT Gateway](https://learn.microsoft.com/azure/firewall/integrate-with-nat-gateway)
- [Plan for inbound and outbound internet connectivity](https://learn.microsoft.com/azure/cloud-adoption-framework/ready/azure-best-practices/plan-for-inbound-and-outbound-internet-connectivity)
- [Azure Firewall forced tunneling](https://learn.microsoft.com/azure/firewall/forced-tunneling)
- [Azure deployment reference - Virtual Network](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks)
- [Azure deployment reference - Subnet](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks/subnets)
33 changes: 33 additions & 0 deletions docs/setup/configuring-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,39 @@ configuration:
- loginName
```

### AZURE_FIREWALL_IS_ZONAL

<!-- module:version v1.39.0 -->
<!-- module:rule Azure.VNET.FirewallSubnetNAT -->

This configuration identifies if Azure Firewall deployments are expected to be zonal or zone redundant.
Some specific configurations may require Azure Firewall deployed zonal.
If you environment requires a zonal configuration set this to `true` which will toggle rules that apply to this configuration.
By default, `AZURE_FIREWALL_IS_ZONAL` is set to `false`.

Syntax:

```yaml title="ps-rule.yaml"
configuration:
AZURE_FIREWALL_IS_ZONAL: boolean
```

Default:

```yaml title="ps-rule.yaml"
# YAML: The default AZURE_FIREWALL_IS_ZONAL configuration option
configuration:
AZURE_FIREWALL_IS_ZONAL: false
```

Example:

```yaml title="ps-rule.yaml"
# YAML: Set the AZURE_FIREWALL_IS_ZONAL configuration option to true
configuration:
AZURE_FIREWALL_IS_ZONAL: true
```

### AZURE_RESOURCE_ALLOWED_LOCATIONS

<!-- module:version v1.30.0 -->
Expand Down
1 change: 1 addition & 0 deletions src/PSRule.Rules.Azure/en/PSRule-rules.psd1
Original file line number Diff line number Diff line change
Expand Up @@ -116,4 +116,5 @@
AzureSQLDatabaseMaintenanceWindow = "The {0} ({1}) should have a customer-controlled maintenance window configured."
ASEAvailabilityZoneVersion = "The app service environment ({0}) is not deployed with a version that supports zone-redundancy."
AppServiceAvailabilityZoneSKU = "The app service plan ({0}) is not deployed with a SKU that supports zone-redundancy."
FirewallSubnetNAT = "The firewall should have a NAT gateway associated."
}
22 changes: 22 additions & 0 deletions src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,28 @@ Rule 'Azure.VNET.FirewallSubnet' -Ref 'AZR-000322' -Type 'Microsoft.Network/virt
$Assert.In($subnets, '.', @('AzureFirewallSubnet')).ReasonFrom('properties.subnets', $LocalizedData.SubnetNotFound, 'AzureFirewallSubnet')
}

# Synopsis: Zonal-deployed Azure Firewalls should consider using an Azure NAT Gateway for outbound access.
Rule 'Azure.VNET.FirewallSubnetNAT' -Ref 'AZR-000448' -Level 'Warning' -Type 'Microsoft.Network/virtualNetworks', 'Microsoft.Network/virtualNetworks/subnets' -If { $Configuration.GetBoolOrDefault('AZURE_FIREWALL_IS_ZONAL', $False) } -Tag @{ release = 'GA'; ruleSet = '2024_09'; 'Azure.WAF/pillar' = 'Reliability'; } {
if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') {
$subnets = @(
$TargetObject.properties.subnets | Where-Object { $null -ne $_ -and ($_.name -eq 'AzureFirewallSubnet' -or $_.name -like '*/AzureFirewallSubnet') }
GetSubResources -ResourceType 'Microsoft.Network/virtualNetworks/subnets' | Where-Object { $null -ne $_ -and ($_.name -eq 'AzureFirewallSubnet' -or $_.name -like '*/AzureFirewallSubnet') }
)
}

else {
$subnets = @($TargetObject | Where-Object { $_.name -eq 'AzureFirewallSubnet' -or $_.name -like '*/AzureFirewallSubnet' })
}

if ($subnets.Count -eq 0) {
return $Assert.Pass()
}

foreach ($subnet in $subnets) {
$Assert.HasFieldValue($subnet, 'properties.natGateway.id').Reason($LocalizedData.FirewallSubnetNAT)
}
}

#endregion Virtual Network

#region Helper functions
Expand Down
3 changes: 3 additions & 0 deletions src/PSRule.Rules.Azure/rules/Config.Rule.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ spec:
# Configure Container Apps external ingress.
AZURE_CONTAINERAPPS_RESTRICT_INGRESS: false

# Enable checks when Azure Firewall is deployed in a zonal configuration.
AZURE_FIREWALL_IS_ZONAL: false

# Enabled resource level checks for Defender for Storage.
AZURE_STORAGE_DEFENDER_PER_ACCOUNT: false

Expand Down
50 changes: 42 additions & 8 deletions tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 4;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet';
$ruleResult.Length | Should -Be 6;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet';
}

It 'Azure.VNET.SingleDNS' {
Expand Down Expand Up @@ -159,8 +159,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 9;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-H/excludedSubnet';
$ruleResult.Length | Should -Be 11;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-H/excludedSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet';
}

It 'Azure.VNET.BastionSubnet' {
Expand Down Expand Up @@ -212,6 +212,18 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
$ruleResult.Length | Should -Be 2;
$ruleResult.TargetName | Should -BeIn 'vnet-F', 'vnet-G';
}

It 'Azure.VNET.FirewallSubnetNAT' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.FirewallSubnetNAT' };

# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult | Should -BeNullOrEmpty;

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -BeNullOrEmpty;
}
}

Context 'Resource name - Azure.VNET.Name' {
Expand Down Expand Up @@ -389,12 +401,13 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
WarningAction = 'Ignore'
ErrorAction = 'Stop'
Option = @{
'Configuration.AZURE_VNET_DNS_WITH_IDENTITY' = $true
'Configuration.AZURE_VNET_DNS_WITH_IDENTITY' = $True
'Configuration.AZURE_FIREWALL_IS_ZONAL' = $True
'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ', 'excludedSubnet')
}
}
$dataPath = Join-Path -Path $here -ChildPath 'Resources.VirtualNetwork.json';
$result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All -Name 'Azure.VNET.LocalDNS', 'Azure.VNET.UseNSGs';
$result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All -Name 'Azure.VNET.LocalDNS', 'Azure.VNET.UseNSGs', 'Azure.VNET.FirewallSubnetNAT';
}

It 'Azure.VNET.LocalDNS' {
Expand Down Expand Up @@ -444,8 +457,29 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult | Should -Not -BeNullOrEmpty;
$ruleResult.Length | Should -Be 6;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-H/excludedSubnet';
$ruleResult.Length | Should -Be 8;
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/excludedSubnet';
}

It 'Azure.VNET.FirewallSubnetNAT' {
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.FirewallSubnetNAT' };

# Fail
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
$ruleResult.Length | Should -Be 3;
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet';

$ruleResult[0].Reason | Should -BeExactly @(
"The firewall should have a NAT gateway associated."
"The firewall should have a NAT gateway associated."
);
$ruleResult[1].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";
$ruleResult[2].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";

# Pass
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
$ruleResult.Length | Should -Be 8;
$ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/excludedSubnet', 'vnet-J/AzureFirewallSubnet';
}
}
}
2 changes: 1 addition & 1 deletion tests/PSRule.Rules.Azure.Tests/OptionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ public sealed class OptionsTests
[Fact]
public void GetOptions()
{
var actual = PSRuleOption.FromFileOrDefault(null);
var actual = PSRuleOption.FromFileOrDefault("not-a-file.yaml");
Assert.NotNull(actual);
Assert.Equal("ffffffff-ffff-ffff-ffff-ffffffffffff", actual.Configuration.Subscription.SubscriptionId);
Assert.Equal("PSRule Test Subscription", actual.Configuration.Subscription.DisplayName);
Expand Down
Loading

0 comments on commit 46487c9

Please sign in to comment.