Skip to content

Commit 6fa18b6

Browse files
feat(new): Added Azure.VNET.PrivateSubnet (Azure#2999)
* feat(new): Added Azure.VNET.PrivateSubnet * feat(new): Added Azure.VNET.PrivateSubnet * feat: Referenced more aligned issue * Update docs/en/rules/Azure.VNET.PrivateSubnet.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.VNET.PrivateSubnet.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.VNET.PrivateSubnet.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.VNET.PrivateSubnet.md Co-authored-by: Bernie White <[email protected]> * Update docs/en/rules/Azure.VNET.PrivateSubnet.md Co-authored-by: Bernie White <[email protected]> * feat: Updated logic * Updated Azure.VNET.PrivateSubnet * Fixed tests * Fixed merge conflict and updated tests * Fix typos --------- Co-authored-by: Bernie White <[email protected]>
1 parent 46487c9 commit 6fa18b6

File tree

7 files changed

+366
-32
lines changed

7 files changed

+366
-32
lines changed

.vscode/settings.json

+1
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,7 @@
128128
"VMSS",
129129
"VNET",
130130
"VNETs",
131+
"VWAN",
131132
"webhook",
132133
"webhooks",
133134
"xunit"

docs/CHANGELOG-v1.md

+5
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
2929

3030
## Unreleased
3131

32+
- New rules:
33+
- Virtual Network:
34+
- Verify that subnets have disabled default outbound access for virtual machines by @BenjaminEngeset.
35+
[#3001](https://github.com/Azure/PSRule.Rules.Azure/issues/3001)
36+
3237
What's changed since pre-release v1.39.0-B0009:
3338

3439
- New rules:
+144
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
---
2+
severity: Critical
3+
pillar: Security
4+
category: SE:06 Network controls
5+
resource: Virtual Network
6+
online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.VNET.PrivateSubnet/
7+
---
8+
9+
# Disable default outbound access
10+
11+
## SYNOPSIS
12+
13+
Disable default outbound access for virtual machines.
14+
15+
## DESCRIPTION
16+
17+
Azure virtual network (VNET) subnets support disabling default outbound Internet access.
18+
By default, virtual machines (VMs) have outbound connectivity to the Internet.
19+
Default outbound Internet access also applies to virtual machine scale sets (VMSS) configured with the uniform orchestration mode.
20+
21+
When default outbound is enabled traffic to the Internet is automatically routing via a shared NAT pool.
22+
The IP address used by the shared NAT pool is used across multiple customers, and is subject to change.
23+
24+
Default outbound access for new VMs and VMSS is scheduled for retirement in September 2025.
25+
26+
Alternatively, outbound access to the Internet can be explicitly configured.
27+
Explicit outbound access has the following benefits:
28+
29+
- **Security**: Outbound Internet access can be used to transfer data or control out of your organization.
30+
Controlling outbound access allows you to make an explicit choice about which workloads require this vs those that don't.
31+
- **Ownership and stability**: The default outbound access IP is managed by Microsoft and used by multiple customers.
32+
As a result, you can't assume use the address for network security rules and the address might change unexpectedly in the future,
33+
potentially causing disruptions to any configuration relying on a stable or fixed IP address.
34+
35+
Explicit outbound access can be provided to a specific workload or application by:
36+
37+
- Deploying a NAT gateway into the subnet where the VM or VMSS is deployed.
38+
- Deploying a Standard load balancer and configuring an outbound SNAT rule.
39+
40+
A public IP address can also be attached to a specific VM instance to provide explicit outbound access.
41+
However, attaching a public IP address to a VM also allows inbound access from the Internet which further compromises the security of the VM.
42+
43+
To provide outbound access to multiple workloads or applications a VWAN or hub and spoke network topology can be deployed.
44+
See the Azure Cloud Adoption Framework (CAF) for guidance and a reference architecture for deploying network connectivity in Azure.
45+
46+
Note that there are limitations to this feature, so please refer to the documentation for detailed information.
47+
48+
## RECOMMENDATION
49+
50+
Consider using an explicit method of public connectivity for virtual machines.
51+
52+
## EXAMPLES
53+
54+
### Configure with Azure template
55+
56+
To configure virtual networks that pass this rule:
57+
58+
- For each subnet in defined the `properties.subnets` property:
59+
- Set the `properties.defaultOutboundAccess` property to `false`.
60+
61+
For example:
62+
63+
```json
64+
{
65+
"type": "Microsoft.Network/virtualNetworks",
66+
"apiVersion": "2023-11-01",
67+
"name": "[parameters('name')]",
68+
"location": "[parameters('location')]",
69+
"properties": {
70+
"addressSpace": {
71+
"addressPrefixes": [
72+
"10.0.0.0/16"
73+
]
74+
},
75+
"subnets": [
76+
{
77+
"name": "subnet-A",
78+
"properties": {
79+
"addressPrefix": "10.0.0.0/27",
80+
"defaultOutboundAccess": false
81+
}
82+
},
83+
{
84+
"name": "subnet-B",
85+
"properties": {
86+
"addressPrefix": "10.0.0.32/27",
87+
"defaultOutboundAccess": false
88+
}
89+
}
90+
]
91+
}
92+
}
93+
```
94+
95+
### Configure with Bicep
96+
97+
To configure virtual networks that pass this rule:
98+
99+
- For each subnet in defined the `properties.subnets` property:
100+
- Set the `properties.defaultOutboundAccess` property to `false`.
101+
102+
For example:
103+
104+
```bicep
105+
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' = {
106+
name: name
107+
location: location
108+
properties: {
109+
addressSpace: {
110+
addressPrefixes: [
111+
'10.0.0.0/16'
112+
]
113+
}
114+
subnets: [
115+
{
116+
name: 'subnet-A'
117+
properties: {
118+
addressPrefix: '10.0.0.0/27'
119+
defaultOutboundAccess: false
120+
}
121+
}
122+
{
123+
name: 'subnet-B'
124+
properties: {
125+
addressPrefix: '10.0.0.32/27'
126+
defaultOutboundAccess: false
127+
}
128+
}
129+
]
130+
}
131+
}
132+
```
133+
134+
## NOTES
135+
136+
This feature is currently in preview.
137+
138+
## LINKS
139+
140+
- [SE:06 Network controls](https://learn.microsoft.com/azure/well-architected/security/networking)
141+
- [Default outbound access](https://learn.microsoft.com/azure/virtual-network/ip-services/default-outbound-access)
142+
- [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)
143+
- [Azure deployment reference - Virtual Network](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks)
144+
- [Azure deployment reference - Subnet](https://learn.microsoft.com/azure/templates/microsoft.network/virtualnetworks/subnets)

src/PSRule.Rules.Azure/en/PSRule-rules.psd1

+2-1
Original file line numberDiff line numberDiff line change
@@ -117,4 +117,5 @@
117117
ASEAvailabilityZoneVersion = "The app service environment ({0}) is not deployed with a version that supports zone-redundancy."
118118
AppServiceAvailabilityZoneSKU = "The app service plan ({0}) is not deployed with a SKU that supports zone-redundancy."
119119
FirewallSubnetNAT = "The firewall should have a NAT gateway associated."
120-
}
120+
PrivateSubnet = "The subnet ({0}) should disable default outbound access."
121+
}

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

+24-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ Rule 'Azure.VNET.UseNSGs' -Ref 'AZR-000263' -Type 'Microsoft.Network/virtualNetw
1515
if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') {
1616
# Get subnets
1717
$subnet = @($TargetObject.properties.subnets | Where-Object {
18-
[PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $excludedSubnets -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0
18+
[PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $excludedSubnets -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.Name) -notin $customExcludedSubnets -and @($_.properties.delegations | Where-Object { $_.properties.serviceName -eq 'Microsoft.HardwareSecurityModules/dedicatedHSMs' }).Length -eq 0
1919
});
2020
if ($subnet.Length -eq 0 -or !$Assert.HasFieldValue($TargetObject, 'properties.subnets').Result) {
2121
return $Assert.Pass();
@@ -140,6 +140,29 @@ Rule 'Azure.VNET.FirewallSubnetNAT' -Ref 'AZR-000448' -Level 'Warning' -Type 'Mi
140140
}
141141
}
142142

143+
# Synopsis: Disable default outbound access for virtual machines.
144+
Rule 'Azure.VNET.PrivateSubnet' -Ref 'AZR-000447' -Type 'Microsoft.Network/virtualNetworks', 'Microsoft.Network/virtualNetworks/subnets' -Tag @{ release = 'preview'; ruleSet = '2024_09'; 'Azure.WAF/pillar' = 'Security'; } {
145+
$excludedSubnets = @('GatewaySubnet', 'AzureFirewallSubnet', 'AzureFirewallManagementSubnet', 'AzureBastionSubnet')
146+
if ($PSRule.TargetType -eq 'Microsoft.Network/virtualNetworks') {
147+
$subnets = @(
148+
$TargetObject.properties.subnets | Where-Object { $null -ne $_ -and -not $_.properties.delegations -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.name) -notin $excludedSubnets }
149+
GetSubResources -ResourceType 'Microsoft.Network/virtualNetworks/subnets' | Where-Object { $null -ne $_ -and -not $_.properties.delegations -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.name) -notin $excludedSubnets }
150+
)
151+
}
152+
153+
else {
154+
$subnets = @($TargetObject | Where-Object { -not $_.properties.delegations -and [PSRule.Rules.Azure.Runtime.Helper]::GetSubResourceName($_.name) -notin $excludedSubnets } )
155+
}
156+
157+
if ($subnets.Count -eq 0) {
158+
return $Assert.Pass()
159+
}
160+
161+
foreach ($subnet in $subnets) {
162+
$Assert.HasFieldValue($subnet, 'properties.defaultOutboundAccess', $false).Reason($LocalizedData.PrivateSubnet, $subnet.name)
163+
}
164+
}
165+
143166
#endregion Virtual Network
144167

145168
#region Helper functions

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

+67-30
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
7777
# Pass
7878
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
7979
$ruleResult | Should -Not -BeNullOrEmpty;
80-
$ruleResult.Length | Should -Be 6;
81-
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet';
80+
$ruleResult.Length | Should -Be 9;
81+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/subnet-A', 'vnet-H/subnet-B', 'vnet-H/subnet-C';
8282
}
8383

8484
It 'Azure.VNET.SingleDNS' {
@@ -159,8 +159,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
159159
# Pass
160160
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
161161
$ruleResult | Should -Not -BeNullOrEmpty;
162-
$ruleResult.Length | Should -Be 11;
163-
$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';
162+
$ruleResult.Length | Should -Be 14;
163+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-H/excludedSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/subnet-A', 'vnet-H/subnet-B', 'vnet-H/subnet-C';
164164
}
165165

166166
It 'Azure.VNET.BastionSubnet' {
@@ -224,6 +224,43 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
224224
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
225225
$ruleResult | Should -BeNullOrEmpty;
226226
}
227+
228+
It 'Azure.VNET.PrivateSubnet' {
229+
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.PrivateSubnet' };
230+
231+
# Fail
232+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
233+
$ruleResult.Length | Should -Be 7;
234+
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-B', 'vnet-C', 'vnet-G', 'vnet-H/excludedSubnet', 'vnet-H/subnet-A', 'vnet-H/subnet-B';
235+
236+
$ruleResult[0].Reason | Should -BeExactly @(
237+
"The subnet (subnet-A) should disable default outbound access."
238+
"The subnet (subnet-B) should disable default outbound access."
239+
"The subnet (subnet-C) should disable default outbound access."
240+
"The subnet (subnet-D) should disable default outbound access."
241+
);
242+
$ruleResult[1].Reason | Should -BeExactly @(
243+
"The subnet (subnet-A) should disable default outbound access."
244+
"The subnet (subnet-B) should disable default outbound access."
245+
"The subnet (subnet-C) should disable default outbound access."
246+
"The subnet (subnet-D) should disable default outbound access."
247+
);
248+
$ruleResult[2].Reason | Should -BeExactly @(
249+
"The subnet (subnet-A) should disable default outbound access."
250+
"The subnet (subnet-B) should disable default outbound access."
251+
"The subnet (subnet-C) should disable default outbound access."
252+
"The subnet (subnet-D) should disable default outbound access."
253+
);
254+
$ruleResult[3].Reason | Should -BeExactly "The subnet (subnet-ZZ) should disable default outbound access.";
255+
$ruleResult[4].Reason | Should -BeExactly "The subnet (vnet-H/excludedSubnet) should disable default outbound access.";
256+
$ruleResult[5].Reason | Should -BeExactly "The subnet (vnet-H/subnet-A) should disable default outbound access.";
257+
$ruleResult[6].Reason | Should -BeExactly "The subnet (vnet-H/subnet-B) should disable default outbound access.";
258+
259+
# Pass
260+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
261+
$ruleResult.Length | Should -Be 7;
262+
$ruleResult.TargetName | Should -BeIn 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/subnet-C';
263+
}
227264
}
228265

229266
Context 'Resource name - Azure.VNET.Name' {
@@ -400,9 +437,9 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
400437
Module = 'PSRule.Rules.Azure'
401438
WarningAction = 'Ignore'
402439
ErrorAction = 'Stop'
403-
Option = @{
404-
'Configuration.AZURE_VNET_DNS_WITH_IDENTITY' = $True
405-
'Configuration.AZURE_FIREWALL_IS_ZONAL' = $True
440+
Option = @{
441+
'Configuration.AZURE_VNET_DNS_WITH_IDENTITY' = $True
442+
'Configuration.AZURE_FIREWALL_IS_ZONAL' = $True
406443
'Configuration.AZURE_VNET_SUBNET_EXCLUDED_FROM_NSG' = @('subnet-ZZ', 'excludedSubnet')
407444
}
408445
}
@@ -421,6 +458,27 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
421458
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
422459
$ruleResult | Should -BeNullOrEmpty;
423460
}
461+
462+
It 'Azure.VNET.FirewallSubnetNAT' {
463+
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.FirewallSubnetNAT' };
464+
465+
# Fail
466+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
467+
$ruleResult.Length | Should -Be 3;
468+
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet';
469+
470+
$ruleResult[0].Reason | Should -BeExactly @(
471+
"The firewall should have a NAT gateway associated."
472+
"The firewall should have a NAT gateway associated."
473+
);
474+
$ruleResult[1].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";
475+
$ruleResult[2].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";
476+
477+
# Pass
478+
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
479+
$ruleResult.Length | Should -Be 11;
480+
$ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/excludedSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/subnet-A', 'vnet-H/subnet-B', 'vnet-H/subnet-C';
481+
}
424482

425483
It 'Azure.VNET.UseNSGs' {
426484
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.UseNSGs' };
@@ -457,29 +515,8 @@ Describe 'Azure.VNET' -Tag 'Network', 'VNET' {
457515
# Pass
458516
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
459517
$ruleResult | Should -Not -BeNullOrEmpty;
460-
$ruleResult.Length | Should -Be 8;
461-
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/excludedSubnet';
462-
}
463-
464-
It 'Azure.VNET.FirewallSubnetNAT' {
465-
$filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.VNET.FirewallSubnetNAT' };
466-
467-
# Fail
468-
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' });
469-
$ruleResult.Length | Should -Be 3;
470-
$ruleResult.TargetName | Should -Be 'vnet-A', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet';
471-
472-
$ruleResult[0].Reason | Should -BeExactly @(
473-
"The firewall should have a NAT gateway associated."
474-
"The firewall should have a NAT gateway associated."
475-
);
476-
$ruleResult[1].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";
477-
$ruleResult[2].Reason | Should -BeExactly "The firewall should have a NAT gateway associated.";
478-
479-
# Pass
480-
$ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' });
481-
$ruleResult.Length | Should -Be 8;
482-
$ruleResult.TargetName | Should -BeIn 'vnet-B', 'vnet-C', 'vnet-D', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/excludedSubnet', 'vnet-J/AzureFirewallSubnet';
518+
$ruleResult.Length | Should -Be 11;
519+
$ruleResult.TargetName | Should -BeIn 'vnet-A', 'vnet-E', 'vnet-F', 'vnet-G', 'vnet-H/AzureFirewallSubnet', 'vnet-I/AzureFirewallSubnet', 'vnet-H/excludedSubnet', 'vnet-J/AzureFirewallSubnet', 'vnet-H/subnet-A', 'vnet-H/subnet-B', 'vnet-H/subnet-C';
483520
}
484521
}
485522
}

0 commit comments

Comments
 (0)