Skip to content

Commit 4067ebf

Browse files
authored
Add option for excluding subnets from NSG Azure#2572 (Azure#2624)
* Add option for excluding subnets from NSG Azure#2572 * Fix typo
1 parent 84e8c48 commit 4067ebf

File tree

8 files changed

+136
-25
lines changed

8 files changed

+136
-25
lines changed

.vscode/settings.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@
119119
"VNET",
120120
"VNETs",
121121
"webhook",
122-
"webhooks"
122+
"webhooks",
123+
"xunit"
123124
],
124125
"cSpell.enabledLanguageIds": [
125126
"csharp",

docs/CHANGELOG-v1.md

+3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ What's changed since v1.32.1:
3737
- General improvements:
3838
- Quality updates to rules and documentation by @BernieWhite.
3939
[#1772](https://github.com/Azure/PSRule.Rules.Azure/issues/1772)
40+
- Added option for excluding subnets to `Azure.VNET.UseNSGs` by @BernieWhite.
41+
[#2572](https://github.com/Azure/PSRule.Rules.Azure/issues/2572)
42+
- To add a subnet exclusion, set the `AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG` option.
4043
- Engineering:
4144
- Bump xunit to v2.6.4.
4245
[#2618](https://github.com/Azure/PSRule.Rules.Azure/pull/2618)

docs/en/rules/Azure.VNET.UseNSGs.md

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
---
2-
reviewed: 2023-09-10
2+
reviewed: 2024-01-02
33
severity: Critical
44
pillar: Security
5-
category: Network segmentation
5+
category: SE:06 Network controls
66
resource: Virtual Network
77
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.VNET.UseNSGs/
88
---
@@ -146,13 +146,31 @@ $nsg = Get-AzNetworkSecurityGroup -Name '<nsg_name>' -ResourceGroupName '<resour
146146
Set-AzVirtualNetworkSubnetConfig -Name '<subnet>' -VirtualNetwork $vnet -AddressPrefix '10.0.1.0/24' -NetworkSecurityGroup $nsg
147147
```
148148
149+
## NOTES
150+
151+
If you identify a false postive for an Azure service that does not support NSGs,
152+
please [open an issue](https://github.com/Azure/PSRule.Rules.Azure/issues/new) to help us improve this rule.
153+
154+
To exclude subnets that are specific to your environment, use the `AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG` configuration option.
155+
Any subnet names specified by this option will be ignored by this rule.
156+
157+
For example:
158+
159+
```yaml
160+
configuration:
161+
AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG:
162+
- subnet-1
163+
- subnet-2
164+
```
165+
149166
## LINKS
150167
151-
- [Implement network segmentation patterns on Azure](https://learn.microsoft.com/azure/well-architected/security/design-network-segmentation)
168+
- [SE:06 Network controls](https://learn.microsoft.com/azure/well-architected/security/networking)
152169
- [Network Security Best Practices](https://learn.microsoft.com/azure/security/fundamentals/network-best-practices#logically-segment-subnets)
153170
- [Azure Firewall FAQ](https://learn.microsoft.com/azure/firewall/firewall-faq#are-network-security-groups--nsgs--supported-on-the-azurefirewallsubnet)
154171
- [Forced tunneling configuration](https://learn.microsoft.com/azure/firewall/forced-tunneling#forced-tunneling-configuration)
155172
- [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)
156173
- [Azure Dedicated HSM networking](https://learn.microsoft.com/azure/dedicated-hsm/networking#subnets)
174+
- [NS-1: Establish network segmentation boundaries](https://learn.microsoft.com/security/benchmark/azure/baselines/virtual-network-security-baseline#ns-1-establish-network-segmentation-boundaries)
157175
- [Azure VNET deployment reference](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks?pivots=deployment-language-bicep)
158176
- [Azure NSG deployment reference](https://learn.microsoft.com/azure/templates/microsoft.network/networksecuritygroups)

docs/setup/configuring-rules.md

+35
Original file line numberDiff line numberDiff line change
@@ -665,3 +665,38 @@ Example:
665665
configuration:
666666
AZURE_VNET_DNS_WITH_IDENTITY: true
667667
```
668+
669+
### AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG
670+
671+
:octicons-milestone-24: v1.33.0
672+
673+
> Applies to [Azure.VNET.UseNSGs](../en/rules/Azure.VNET.UseNSGs.md).
674+
675+
This configuration option excludes subnets from requiring a Network Security Group (NSG).
676+
You can use this configuration option to exclude subnets that are specific to your environment.
677+
To configure this option, specify a list of subnet names to exclude.
678+
679+
Syntax:
680+
681+
```yaml
682+
configuration:
683+
AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: array
684+
```
685+
686+
Default:
687+
688+
```yaml
689+
# YAML: The default AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG configuration option
690+
configuration:
691+
AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: []
692+
```
693+
694+
Example:
695+
696+
```yaml
697+
# YAML: Set the AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG configuration option with two user defined subnets.
698+
configuration:
699+
AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG:
700+
- subnet-1
701+
- subnet-2
702+
```

src/PSRule.Rules.Azure/rules/Azure.VNET.Rule.ps1

+3-2
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,19 @@
1010
# Synopsis: Virtual network (VNET) subnets should have Network Security Groups (NSGs) assigned.
1111
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' } {
1212
$excludedSubnets = @('GatewaySubnet', 'AzureFirewallSubnet', 'AzureFirewallManagementSubnet', 'RouteServerSubnet');
13+
$customExcludedSubnets = $Configuration.GetStringValues('AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG');
1314
$subnet = @($TargetObject);
1415
if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') {
1516
# Get subnets
1617
$subnet = @($TargetObject.properties.subnets | Where-Object {
17-
$_.Name -notin $excludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0
18+
$_.Name -notin $excludedSubnets -and $_.Name -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0
1819
});
1920
if ($subnet.Length -eq 0 -or !$Assert.HasFieldValue($TargetObject, 'properties.subnets').Result) {
2021
return $Assert.Pass();
2122
}
2223
}
2324
elseif ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks/subnets' -and
24-
($PSRule.TargetName -in $excludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) {
25+
($PSRule.TargetName -in $excludedSubnets -or $PSRule.TargetName -in $customExcludedSubnets -or @($TargetObject.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -gt 0)) {
2526
return $Assert.Pass();
2627
}
2728
foreach ($sn in $subnet) {

src/PSRule.Rules.Azure/rules/Config.Rule.yaml

+13-3
Original file line numberDiff line numberDiff line change
@@ -25,14 +25,21 @@ spec:
2525
subscriptionId: [ 'subscriptionId' ]
2626
resourceGroupName: [ 'resourceGroupName' ]
2727
configuration:
28+
# Enable expansion from .json files.
2829
AZURE_PARAMETER_FILE_EXPANSION: false
2930
AZURE_PARAMETER_FILE_METADATA_LINK: false
31+
32+
# Enable expansion from .bicep files.
3033
AZURE_BICEP_FILE_EXPANSION: false
34+
35+
# Enable expansion from .bicepparam files.
3136
AZURE_BICEP_PARAMS_FILE_EXPANSION: false
37+
38+
# Check for a minimum version of the Bicep CLI.
3239
AZURE_BICEP_MINIMUM_VERSION: '0.4.451'
3340
AZURE_BICEP_CHECK_TOOL: false
3441

35-
# Configure minimum AKS cluster version
42+
# Configure minimum AKS cluster version.
3643
AZURE_AKS_CLUSTER_MINIMUM_VERSION: '1.27.7'
3744

3845
AZURE_DEPLOYMENT_SENSITIVE_PROPERTY_NAMES:
@@ -42,12 +49,15 @@ spec:
4249

4350
AZURE_DEPLOYMENT_NONSENSITIVE_PARAMETER_NAMES: []
4451

45-
# Configure Container Apps external ingress
52+
# Configure Container Apps external ingress.
4653
AZURE_CONTAINERAPPS_RESTRICT_INGRESS: false
4754

48-
# Configure DNS is within the identity subscription
55+
# Configure DNS is within the identity subscription.
4956
AZURE_VNET_DNS_WITH_IDENTITY: false
5057

58+
# Exclude subnets by name from requiring and NSG.
59+
AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG: []
60+
5161
convention:
5262
include:
5363
- Azure.Context

tests/PSRule.Rules.Azure.Tests/Azure.VNET.Tests.ps1

+19-16
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
3131
Module = 'PSRule.Rules.Azure'
3232
WarningAction = 'Ignore'
3333
ErrorAction = 'Stop'
34+
Option = @{
35+
'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ')
36+
}
3437
}
3538
$dataPath = Join-Path -Path $here -ChildPath 'Resources.VirtualNetwork.json';
3639
$result = Invoke-PSRule @invokeParams -InputPath $dataPath -Outcome All;
@@ -71,8 +74,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
7174
# Pass
7275
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
7376
$ruleResult | Should -Not -BeNullOrEmpty;
74-
$ruleResult.Length | Should -Be 3;
75-
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F';
77+
$ruleResult.Length | Should -Be 4;
78+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G';
7679
}
7780

7881
It 'Azure.VNET.SingleDNS' {
@@ -87,8 +90,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
8790
# Pass
8891
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
8992
$ruleResult | Should -Not -BeNullOrEmpty;
90-
$ruleResult.Length | Should -Be 5;
91-
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F';
93+
$ruleResult.Length | Should -Be 6;
94+
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G';
9295
}
9396

9497
It 'Azure.VNET.LocalDNS' {
@@ -97,8 +100,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
97100
# Fail
98101
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
99102
$ruleResult | Should -Not -BeNullOrEmpty;
100-
$ruleResult.Length | Should -Be 4;
101-
$ruleResult.TargetName | Should -Be 'vnet-B', 'vnet-D', 'vnet-E', 'vnet-F';
103+
$ruleResult.Length | Should -Be 5;
104+
$ruleResult.TargetName | Should -Be 'vnet-B', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G';
102105

103106
# Pass
104107
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
@@ -125,8 +128,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
125128
# None
126129
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' });
127130
$ruleResult | Should -Not -BeNullOrEmpty;
128-
$ruleResult.Length | Should -Be 3;
129-
$ruleResult.TargetName | Should -Be 'vnet-D', 'vnet-E', 'vnet-F';
131+
$ruleResult.Length | Should -Be 4;
132+
$ruleResult.TargetName | Should -Be 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G';
130133
}
131134

132135
It 'Azure.VNET.Name' {
@@ -139,8 +142,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
139142
# Pass
140143
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
141144
$ruleResult | Should -Not -BeNullOrEmpty;
142-
$ruleResult.Length | Should -Be 6;
143-
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F';
145+
$ruleResult.Length | Should -Be 7;
146+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G';
144147
}
145148

146149
It 'Azure.VNET.SubnetName' {
@@ -153,8 +156,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
153156
# Pass
154157
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
155158
$ruleResult | Should -Not -BeNullOrEmpty;
156-
$ruleResult.Length | Should -Be 6;
157-
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F';
159+
$ruleResult.Length | Should -Be 7;
160+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G';
158161
}
159162

160163
It 'Azure.VNET.BastionSubnet' {
@@ -178,8 +181,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
178181
# None
179182
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' });
180183
$ruleResult | Should -Not -BeNullOrEmpty;
181-
$ruleResult.Length | Should -Be 1;
182-
$ruleResult.TargetName | Should -BeIn 'vnet-F';
184+
$ruleResult.Length | Should -Be 2;
185+
$ruleResult.TargetName | Should -BeIn 'vnet-F', 'vnet-G';
183186
}
184187

185188
It 'Azure.VNET.FirewallSubnet' {
@@ -203,8 +206,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
203206
# None
204207
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' -and $_.TargetObject.ResourceType -eq 'Microsoft.Network/virtualNetworks' });
205208
$ruleResult | Should -Not -BeNullOrEmpty;
206-
$ruleResult.Length | Should -Be 1;
207-
$ruleResult.TargetName | Should -BeIn 'vnet-F';
209+
$ruleResult.Length | Should -Be 2;
210+
$ruleResult.TargetName | Should -BeIn 'vnet-F', 'vnet-G';
208211
}
209212
}
210213

tests/PSRule.Rules.Azure.Tests/Resources.VirtualNetwork.json

+40
Original file line numberDiff line numberDiff line change
@@ -760,6 +760,46 @@
760760
"Tags": null,
761761
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
762762
},
763+
{
764+
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G",
765+
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G",
766+
"Location": "region",
767+
"ResourceName": "vnet-G",
768+
"Name": "vnet-G",
769+
"Properties": {
770+
"addressSpace": {
771+
"addressPrefixes": [
772+
"10.6.0.0/24"
773+
]
774+
},
775+
"dhcpOptions": {
776+
"dnsServers": [
777+
"10.99.0.36",
778+
"10.99.0.37"
779+
]
780+
},
781+
"virtualNetworkPeerings": [],
782+
"enableDdosProtection": false,
783+
"enableVmProtection": false,
784+
"subnets": [
785+
{
786+
"name": "subnet-ZZ",
787+
"id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/virtualNetworks/vnet-G/subnets/subnet-ZZ",
788+
"properties": {
789+
"addressPrefix": "10.6.0.32/28",
790+
"serviceEndpoints": [],
791+
"delegations": []
792+
},
793+
"type": "Microsoft.Network/virtualNetworks/subnets"
794+
}
795+
]
796+
},
797+
"ResourceGroupName": "test-rg",
798+
"Type": "Microsoft.Network/virtualNetworks",
799+
"ResourceType": "Microsoft.Network/virtualNetworks",
800+
"Tags": null,
801+
"SubscriptionId": "00000000-0000-0000-0000-000000000000"
802+
},
763803
{
764804
"ResourceId": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/nsg-A",
765805
"Id": "/subscriptions/00000000-0000-0000-0000-000000000000/resourceGroups/test-rg/providers/Microsoft.Network/networkSecurityGroups/nsg-A",

0 commit comments

Comments
 (0)