From 4067ebf0f0f2ecfbf7236d43697fb95ffccb858f Mon Sep 17 00:00:00 2001 From: Bernie White Date: Wed, 3 Jan 2024 11:23:04 +1000 Subject: [PATCH] Add option for excluding subnets from NSG #2572 (#2624) * Add option for excluding subnets from NSG #2572 * Fix typo --- .vscode/settings.json | 3 +- docs/CHANGELOG-v1.md | 3 ++ docs/en/rules/Azure.VNET.UseNSGs.md | 24 +++++++++-- docs/setup/configuring-rules.md | 35 ++++++++++++++++ .../rules/Azure.VNET.Rule.ps1 | 5 ++- src/PSRule.Rules.Azure/rules/Config.Rule.yaml | 16 ++++++-- .../Azure.VNET.Tests.ps1 | 35 ++++++++-------- .../Resources.VirtualNetwork.json | 40 +++++++++++++++++++ 8 files changed, 136 insertions(+), 25 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 2c1dcba3348..944b5748e7c 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -119,7 +119,8 @@ "VNET", "VNETs", "webhook", - "webhooks" + "webhooks", + "xunit" ], "cSpell.enabledLanguageIds": [ "csharp", diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index db3c1106c2c..fd75423af45 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -37,6 +37,9 @@ What's changed since v1.32.1: - General improvements: - Quality updates to rules and documentation by @BernieWhite. [#1772](https://github.com/Azure/PSRule.Rules.Azure/issues/1772) + - Added option for excluding subnets to `Azure.VNET.UseNSGs` by @BernieWhite. + [#2572](https://github.com/Azure/PSRule.Rules.Azure/issues/2572) + - To add a subnet exclusion, set the `AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG` option. - Engineering: - Bump xunit to v2.6.4. [#2618](https://github.com/Azure/PSRule.Rules.Azure/pull/2618) diff --git a/docs/en/rules/Azure.VNET.UseNSGs.md b/docs/en/rules/Azure.VNET.UseNSGs.md index e900fbc1695..350dfe1ee0f 100644 --- a/docs/en/rules/Azure.VNET.UseNSGs.md +++ b/docs/en/rules/Azure.VNET.UseNSGs.md @@ -1,8 +1,8 @@ --- -reviewed: 2023-09-10 +reviewed: 2024-01-02 severity: Critical pillar: Security -category: Network segmentation +category: SE:06 Network controls resource: Virtual Network online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.VNET.UseNSGs/ --- @@ -146,13 +146,31 @@ $nsg = Get-AzNetworkSecurityGroup -Name '' -ResourceGroupName '' -VirtualNetwork $vnet -AddressPrefix '10.0.1.0/24' -NetworkSecurityGroup $nsg ``` +## NOTES + +If you identify a false postive for an Azure service that does not support NSGs, +please [open an issue](https://github.com/Azure/PSRule.Rules.Azure/issues/new) to help us improve this rule. + +To exclude subnets that are specific to your environment, use the `AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG` configuration option. +Any subnet names specified by this option will be ignored by this rule. + +For example: + +```yaml +configuration: + AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: + - subnet-1 + - subnet-2 +``` + ## LINKS -- [Implement network segmentation patterns on Azure](https://learn.microsoft.com/azure/well-architected/security/design-network-segmentation) +- [SE:06 Network controls](https://learn.microsoft.com/azure/well-architected/security/networking) - [Network Security Best Practices](https://learn.microsoft.com/azure/security/fundamentals/network-best-practices#logically-segment-subnets) - [Azure Firewall FAQ](https://learn.microsoft.com/azure/firewall/firewall-faq#are-network-security-groups--nsgs--supported-on-the-azurefirewallsubnet) - [Forced tunneling configuration](https://learn.microsoft.com/azure/firewall/forced-tunneling#forced-tunneling-configuration) - [Azure Route Server FAQ](https://learn.microsoft.com/azure/route-server/route-server-faq#can-i-associate-a-network-security-group-nsg-to-the-routeserversubnet) - [Azure Dedicated HSM networking](https://learn.microsoft.com/azure/dedicated-hsm/networking#subnets) +- [NS-1: Establish network segmentation boundaries](https://learn.microsoft.com/security/benchmark/azure/baselines/virtual-network-security-baseline#ns-1-establish-network-segmentation-boundaries) - [Azure VNET deployment reference](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks?pivots=deployment-language-bicep) - [Azure NSG deployment reference](https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups) diff --git a/docs/setup/configuring-rules.md b/docs/setup/configuring-rules.md index 4896b5a67ec..6a8c5de6461 100644 --- a/docs/setup/configuring-rules.md +++ b/docs/setup/configuring-rules.md @@ -665,3 +665,38 @@ Example: configuration: AZURE_VNET_DNS_WITH_IDENTITY: true ``` + +### AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG + +:octicons-milestone-24: v1.33.0 + +> Applies to [Azure.VNET.UseNSGs](../en/rules/Azure.VNET.UseNSGs.md). + +This configuration option excludes subnets from requiring a Network Security Group (NSG). +You can use this configuration option to exclude subnets that are specific to your environment. +To configure this option, specify a list of subnet names to exclude. + +Syntax: + +```yaml +configuration: + AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: array +``` + +Default: + +```yaml +# YAML: The default AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG configuration option +configuration: + AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: [] +``` + +Example: + +```yaml +# YAML: Set the AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG configuration option with two user defined subnets. +configuration: + AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: + - subnet-1 + - subnet-2 +``` diff --git a/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 b/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 index f3221df3b9d..aae589c4d68 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 +++ b/src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1 @@ -10,18 +10,19 @@ # Synopsis: Virtual network (VNET) subnets should have Network Security Groups (NSGs) assigned. Rule 'Azure.VNET.UseNSGs' -Ref 'AZR-000263' -Type 'Microsoft.Network/virtualNetworks', 'Microsoft.Network/virtualNetworks/subnets' -Tag @{ release = 'GA'; ruleSet = '2020_06'; 'Azure.WAF/pillar' = 'Security'; 'Azure.MCSB.v1/control' = 'NS-1' } { $excludedSubnets = @('GatewaySubnet', 'AzureFirewallSubnet', 'AzureFirewallManagementSubnet', 'RouteServerSubnet'); + $customExcludedSubnets = $Configuration.GetStringValues('AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG'); $subnet = @($TargetObject); if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') { # Get subnets $subnet = @($TargetObject.properties.subnets | Where-Object { - $_.Name -notin $excludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0 + $_.Name -notin $excludedSubnets -and $_.Name -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0 }); if ($subnet.Length -eq 0 -or !$Assert.HasFieldValue($TargetObject, 'properties.subnets').Result) { return $Assert.Pass(); } } elseif ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks/subnets' -and - ($PSRule.TargetName -in $excludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) { + ($PSRule.TargetName -in $excludedSubnets -or $PSRule.TargetName -in $customExcludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) { return $Assert.Pass(); } foreach ($sn in $subnet) { diff --git a/src/PSRule.Rules.Azure/rules/Config.Rule.yaml b/src/PSRule.Rules.Azure/rules/Config.Rule.yaml index d286c933da6..81a8500c876 100644 --- a/src/PSRule.Rules.Azure/rules/Config.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Config.Rule.yaml @@ -25,14 +25,21 @@ spec: subscriptionId: [ 'subscriptionId' ] resourceGroupName: [ 'resourceGroupName' ] configuration: + # Enable expansion from .json files. AZURE_PARAMETER_FILE_EXPANSION: false AZURE_PARAMETER_FILE_METADATA_LINK: false + + # Enable expansion from .bicep files. AZURE_BICEP_FILE_EXPANSION: false + + # Enable expansion from .bicepparam files. AZURE_BICEP_PARAMS_FILE_EXPANSION: false + + # Check for a minimum version of the Bicep CLI. AZURE_BICEP_MINIMUM_VERSION: '0.4.451' AZURE_BICEP_CHECK_TOOL: false - # Configure minimum AKS cluster version + # Configure minimum AKS cluster version. AZURE_AKS_CLUSTER_MINIMUM_VERSION: '1.27.7' AZURE_DEPLOYMENT_SENSITIVE_PROPERTY_NAMES: @@ -42,12 +49,15 @@ spec: AZURE_DEPLOYMENT_NONSENSITIVE_PARAMETER_NAMES: [] - # Configure Container Apps external ingress + # Configure Container Apps external ingress. AZURE_CONTAINERAPPS_RESTRICT_INGRESS: false - # Configure DNS is within the identity subscription + # Configure DNS is within the identity subscription. AZURE_VNET_DNS_WITH_IDENTITY: false + # Exclude subnets by name from requiring and NSG. + AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: [] + convention: include: - Azure.Context diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 index ae33242925f..823fd65c3d8 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1 @@ -31,6 +31,9 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { Module = 'PSRule.Rules.Azure' WarningAction = 'Ignore' ErrorAction = 'Stop' + Option = @{ + 'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ') + } } $dataPath = Join-Path -Path $here -ChildPath 'Resources.VirtualNetwork.json'; $result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All; @@ -71,8 +74,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G'; } It 'Azure.VNET.SingleDNS' { @@ -87,8 +90,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 5; - $ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 6; + $ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; } It 'Azure.VNET.LocalDNS' { @@ -97,8 +100,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # Fail $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 4; - $ruleResult.TargetName | Should -Be 'vnet-B', 'vnet-D', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 5; + $ruleResult.TargetName | Should -Be 'vnet-B', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); @@ -125,8 +128,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 3; - $ruleResult.TargetName | Should -Be 'vnet-D', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 4; + $ruleResult.TargetName | Should -Be 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; } It 'Azure.VNET.Name' { @@ -139,8 +142,8 @@ 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-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 7; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; } It 'Azure.VNET.SubnetName' { @@ -153,8 +156,8 @@ 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-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F'; + $ruleResult.Length | Should -Be 7; + $ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G'; } It 'Azure.VNET.BastionSubnet' { @@ -178,8 +181,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -BeIn 'vnet-F'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'vnet-F', 'vnet-G'; } It 'Azure.VNET.FirewallSubnet' { @@ -203,8 +206,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' { # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -BeIn 'vnet-F'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'vnet-F', 'vnet-G'; } } diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json index 20ac454ab77..d19a0e63ee1 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json @@ -760,6 +760,46 @@ "Tags": null, "SubscriptionId": "00000000-0000-0000-0000-000000000000" }, + { + "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G", + "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G", + "Location": "region", + "ResourceName": "vnet-G", + "Name": "vnet-G", + "Properties": { + "addressSpace": { + "addressPrefixes": [ + "10.6.0.0/24" + ] + }, + "dhcpOptions": { + "dnsServers": [ + "10.99.0.36", + "10.99.0.37" + ] + }, + "virtualNetworkPeerings": [], + "enableDdosProtection": false, + "enableVmProtection": false, + "subnets": [ + { + "name": "subnet-ZZ", + "id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G/subnets/subnet-ZZ", + "properties": { + "addressPrefix": "10.6.0.32/28", + "serviceEndpoints": [], + "delegations": [] + }, + "type": "Microsoft.Network/virtualNetworks/subnets" + } + ] + }, + "ResourceGroupName": "test-rg", + "Type": "Microsoft.Network/virtualNetworks", + "ResourceType": "Microsoft.Network/virtualNetworks", + "Tags": null, + "SubscriptionId": "00000000-0000-0000-0000-000000000000" + }, { "ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/nsg-A", "Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/nsg-A",