diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md index 67a30112d3..d97cdf82aa 100644 --- a/docs/CHANGELOG-v1.md +++ b/docs/CHANGELOG-v1.md @@ -32,6 +32,8 @@ What's changed since pre-release v1.30.0-B0080: - New rules: - Azure Container Registry: + - Check that Container Registries restricts network access by @BenjaminEngeset. + [#2423](https://github.com/Azure/PSRule.Rules.Azure/issues/2423) - Check that Container Registries disables anonymous pull access by @BenjaminEngeset. [#2422](https://github.com/Azure/PSRule.Rules.Azure/issues/2422) - Engineering: diff --git a/docs/en/rules/Azure.ACR.Firewall.md b/docs/en/rules/Azure.ACR.Firewall.md new file mode 100644 index 0000000000..2fe8d655db --- /dev/null +++ b/docs/en/rules/Azure.ACR.Firewall.md @@ -0,0 +1,112 @@ +--- +severity: Important +pillar: Security +category: Application endpoints +resource: Container Registry +online version: https://azure.github.io/PSRule.Rules.Azure/en/rules/Azure.ACR.Firewall/ +--- + +# Restrict network access to container registries + +## SYNOPSIS + +Limit network access of container registries to only trusted clients. + +## DESCRIPTION + +Azure Container Registry (ACR) allows you to restrict network access to trusted clients and networks instead of any client. + +Container registries using the Premium SKU can limit network access by setting firewall rules or using private endpoints. +Firewall and private endpoints are not supported when using the Basic or Standard SKU. + +In general, network access should be restricted to harden against unauthorized access or exfiltration attempts. +However may not be required when publishing and distributing public container images to external parties. + +## RECOMMENDATION + +Consider restricting network access to trusted clients to harden against unauthorized access or exfiltration attempts. + +## EXAMPLES + +### Configure with Azure template + +To deploy Azure Container Registries that pass this rule: + +- Set the `properties.publicNetworkAccess` property to `Disabled`. OR +- Set the `properties.networkRuleSet.defaultAction` property to `Deny`. + +For example: + +```json +{ + "type": "Microsoft.ContainerRegistry/registries", + "apiVersion": "2023-01-01-preview", + "name": "[parameters('registryName')]", + "location": "[parameters('location')]", + "sku": { + "name": "Premium" + }, + "properties": { + "publicNetworkAccess": "Enabled", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": { + "defaultAction": "Deny", + "ipRules": [ + { + "action": "Allow", + "value": "_PublicIPv4Address_" + } + ] + } + } +} +``` + +### Configure with Bicep + +To deploy Azure Container Registries that pass this rule: + +- Set the `properties.publicNetworkAccess` property to `Disabled`. OR +- Set the `properties.networkRuleSet.defaultAction` property to `Deny`. + +For example: + +```bicep +resource acr 'Microsoft.ContainerRegistry/registries@2023-01-01-preview' = { + name: registryName + location: location + sku: { + name: 'Premium' + } + properties: { + publicNetworkAccess: 'Enabled' + networkRuleBypassOptions: 'AzureServices' + networkRuleSet: { + defaultAction: 'Deny' + ipRules: [ + { + action: 'Allow' + value: '_PublicIPv4Address_' + } + ] + } + } +} +``` + +## NOTES + +Configuring firewall rules or using private endpoints is only available for the Premium SKU. + +When used with Microsoft Defender for Containers, you must enable trusted Microsoft services for the vulnerability assessment feature to be able to scan the registry. + +## LINKS + +- [Best practices for endpoint security on Azure](https://learn.microsoft.com/azure/well-architected/security/design-network-endpoints) +- [Restrict access using private endpoint](https://learn.microsoft.com/azure/container-registry/container-registry-private-link) +- [Restrict access using firewall rules](https://learn.microsoft.com/azure/container-registry/container-registry-access-selected-networks) +- [Allow trusted services to securely access a network-restricted container registry](https://learn.microsoft.com/azure/container-registry/allow-access-trusted-services) +- [Vulnerability assessments for Azure with Microsoft Defender Vulnerability Management](https://learn.microsoft.com/azure/defender-for-cloud/agentless-container-registry-vulnerability-assessment) +- [Azure security baseline for Container Registry](https://learn.microsoft.com/security/benchmark/azure/baselines/container-registry-security-baseline) +- [NS-2: Secure cloud services with network controls](https://learn.microsoft.com/security/benchmark/azure/baselines/container-registry-security-baseline#ns-2-secure-cloud-services-with-network-controls) +- [Azure deployment reference](https://learn.microsoft.com/azure/templates/microsoft.containerregistry/registries#registryproperties) diff --git a/src/PSRule.Rules.Azure/rules/Azure.ACR.Rule.yaml b/src/PSRule.Rules.Azure/rules/Azure.ACR.Rule.yaml index 6101847931..dad24e1753 100644 --- a/src/PSRule.Rules.Azure/rules/Azure.ACR.Rule.yaml +++ b/src/PSRule.Rules.Azure/rules/Azure.ACR.Rule.yaml @@ -152,16 +152,39 @@ spec: field: properties.anonymousPullEnabled hasDefault: false +--- +# Synopsis: Limit network access of container registries to only trusted clients. +apiVersion: github.com/microsoft/PSRule/v1 +kind: Rule +metadata: + name: Azure.ACR.Firewall + ref: AZR-000402 + tags: + release: GA + ruleSet: 2023_09 + Azure.WAF/pillar: Security + labels: + Azure.MCSB.v1/control: 'NS-2' +spec: + with: + - Azure.ACR.IsPremiumSKU + condition: + anyOf: + - field: properties.publicNetworkAccess + equals: Disabled + - field: properties.networkRuleSet.defaultAction + equals: Deny + #endregion Rules #region Selectors --- -# Synopsis: Azure Container Registries using a Premium SKU. +# Synopsis: Azure Container Registries using a Standard SKU. apiVersion: github.com/microsoft/PSRule/v1 kind: Selector metadata: - name: Azure.ACR.IsPremiumSKU + name: Azure.ACR.IsStandardSKU spec: if: allOf: @@ -169,16 +192,16 @@ spec: equals: Microsoft.ContainerRegistry/registries - anyOf: - field: sku.name - equals: Premium + equals: Standard - field: sku.tier - equals: Premium + equals: Standard --- -# Synopsis: Azure Container Registries using a Standard SKU. +# Synopsis: Azure Container Registries using a Premium SKU. apiVersion: github.com/microsoft/PSRule/v1 kind: Selector metadata: - name: Azure.ACR.IsStandardSKU + name: Azure.ACR.IsPremiumSKU spec: if: allOf: @@ -186,8 +209,8 @@ spec: equals: Microsoft.ContainerRegistry/registries - anyOf: - field: sku.name - equals: Standard + equals: Premium - field: sku.tier - equals: Standard + equals: Premium #endregion Selectors diff --git a/tests/PSRule.Rules.Azure.Tests/Azure.ACR.Tests.ps1 b/tests/PSRule.Rules.Azure.Tests/Azure.ACR.Tests.ps1 index 0537ca4d81..9576a80cd1 100644 --- a/tests/PSRule.Rules.Azure.Tests/Azure.ACR.Tests.ps1 +++ b/tests/PSRule.Rules.Azure.Tests/Azure.ACR.Tests.ps1 @@ -101,14 +101,14 @@ Describe 'Azure.ACR' -Tag 'ACR' { # Pass $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 1; - $ruleResult.TargetName | Should -BeIn 'registry-E'; + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'registry-E', 'registry-I'; # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 8; - $ruleResult.TargetName | Should -BeIn 'registry-A', 'registry-B', 'registry-C', 'registry-F', 'registry-G', 'registry-H', 'registry-I', 'registry-J'; + $ruleResult.Length | Should -Be 7; + $ruleResult.TargetName | Should -BeIn 'registry-A', 'registry-B', 'registry-C', 'registry-F', 'registry-G', 'registry-H', 'registry-J'; } It 'Azure.ACR.Retention' { @@ -117,8 +117,8 @@ Describe 'Azure.ACR' -Tag 'ACR' { # Fail $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 2; - $ruleResult.TargetName | Should -BeIn 'registry-D', 'registry-J'; + $ruleResult.Length | Should -Be 3; + $ruleResult.TargetName | Should -BeIn 'registry-D', 'registry-J', 'registry-I'; $ruleResult.Detail.Reason.Path | Should -BeIn 'Properties.policies.retentionPolicy.status'; # Pass @@ -130,8 +130,8 @@ Describe 'Azure.ACR' -Tag 'ACR' { # None $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' }); $ruleResult | Should -Not -BeNullOrEmpty; - $ruleResult.Length | Should -Be 7; - $ruleResult.TargetName | Should -BeIn 'registry-A', 'registry-B', 'registry-C', 'registry-F', 'registry-G', 'registry-H', 'registry-I'; + $ruleResult.Length | Should -Be 6; + $ruleResult.TargetName | Should -BeIn 'registry-A', 'registry-B', 'registry-C', 'registry-F', 'registry-G', 'registry-H'; } It 'Azure.ACR.Usage' { @@ -258,6 +258,26 @@ Describe 'Azure.ACR' -Tag 'ACR' { $ruleResult.Length | Should -Be 1; $ruleResult.TargetName | Should -BeIn 'registry-A'; } + + It 'Azure.ACR.Firewall' { + $filteredResult = $result | Where-Object { $_.RuleName -eq 'Azure.ACR.Firewall' }; + + # Fail + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Fail' }); + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'registry-D', 'registry-E'; + $ruleResult.Detail.Reason.Path | Should -BeIn 'properties.publicNetworkAccess', 'properties.networkRuleSet.defaultAction'; + + # Pass + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'Pass' }); + $ruleResult.Length | Should -Be 2; + $ruleResult.TargetName | Should -BeIn 'registry-I', 'registry-J'; + + # None + $ruleResult = @($filteredResult | Where-Object { $_.Outcome -eq 'None' }); + $ruleResult.Length | Should -Be 6; + $ruleResult.TargetName | Should -BeIn 'registry-A', 'registry-B', 'registry-C', 'registry-F', 'registry-G', 'registry-H'; + } } Context 'Resource name' { diff --git a/tests/PSRule.Rules.Azure.Tests/Resources.ACR.json b/tests/PSRule.Rules.Azure.Tests/Resources.ACR.json index 3adf872ac1..97082cb692 100644 --- a/tests/PSRule.Rules.Azure.Tests/Resources.ACR.json +++ b/tests/PSRule.Rules.Azure.Tests/Resources.ACR.json @@ -342,7 +342,8 @@ "Name": "registry-D", "Properties": { "loginServer": "registry-D.azurecr.io", - "adminUserEnabled": true + "adminUserEnabled": true, + "publicNetworkAccess": "Enabled" }, "ResourceGroupName": "test-rg", "Type": "Microsoft.ContainerRegistry/registries", @@ -379,6 +380,17 @@ "days": 180, "status": "enabled" } + }, + "publicNetworkAccess": "Enabled", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": { + "defaultAction": "Allow", + "ipRules": [ + { + "action": "Allow", + "value": "_IPv4Address_" + } + ] } }, "ResourceGroupName": "test-rg", @@ -655,14 +667,15 @@ "retentionDays": 90, "status": "enabled" } - } + }, + "publicNetworkAccess": "Disabled" }, "ResourceGroupName": "test-rg", "Type": "Microsoft.ContainerRegistry/registries", "ResourceType": "Microsoft.ContainerRegistry/registries", "Sku": { - "Name": "Standard", - "Tier": "Standard", + "Name": "Premium", + "Tier": "Premium", "Size": null, "Family": null, "Model": null, @@ -699,6 +712,17 @@ "retentionDays": 90, "status": "enabled" } + }, + "publicNetworkAccess": "Enabled", + "networkRuleBypassOptions": "AzureServices", + "networkRuleSet": { + "defaultAction": "Deny", + "ipRules": [ + { + "action": "Allow", + "value": "_IPv4Address_" + } + ] } }, "ResourceGroupName": "test-rg",