Skip to content

Commit 29f911f

Browse files
committed
Add Network Security Group (NSG) support for Azure Virtual Networks
Adds the ability to create NSGs with security rules and associate them with subnets, enabling fine-grained network traffic control for Azure resources deployed into VNets. New APIs: - AddNetworkSecurityGroup() on IResourceBuilder<AzureVirtualNetworkResource> - WithSecurityRule() on IResourceBuilder<AzureNetworkSecurityGroupResource> - WithNetworkSecurityGroup() on IResourceBuilder<AzureSubnetResource> New types: - AzureNetworkSecurityGroupResource (child of VNet) - AzureSecurityRule (public data class for rule configuration) Key behaviors: - NSGs are created before subnets in bicep for correct dependency ordering - Security rule bicep identifiers are prefixed with NSG name to avoid duplicate symbolic names across multiple NSGs - A single NSG can be shared across multiple subnets - Duplicate rule names within an NSG are rejected with ArgumentException - Subnets reference NSGs via id (not inline properties) in generated bicep
1 parent a30335e commit 29f911f

13 files changed

+866
-3
lines changed

playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
#pragma warning disable AZPROVISION001 // Azure.Provisioning.Network is experimental
5+
6+
using Aspire.Hosting.Azure;
7+
using Azure.Provisioning.Network;
8+
49
var builder = DistributedApplication.CreateBuilder(args);
510

611
// Create a virtual network with two subnets:
@@ -15,6 +20,74 @@
1520
var natGateway = builder.AddNatGateway("nat");
1621
containerAppsSubnet.WithNatGateway(natGateway);
1722

23+
// Create Network Security Groups for each subnet
24+
var acaNsg = vnet.AddNetworkSecurityGroup("aca-nsg")
25+
.WithSecurityRule(new AzureSecurityRule
26+
{
27+
Name = "allow-https-from-azure-lb",
28+
Priority = 100,
29+
Direction = SecurityRuleDirection.Inbound,
30+
Access = SecurityRuleAccess.Allow,
31+
Protocol = SecurityRuleProtocol.Tcp,
32+
SourceAddressPrefix = "AzureLoadBalancer",
33+
SourcePortRange = "*",
34+
DestinationAddressPrefix = "*",
35+
DestinationPortRange = "443"
36+
})
37+
.WithSecurityRule(new AzureSecurityRule
38+
{
39+
Name = "deny-vnet-inbound",
40+
Priority = 110,
41+
Direction = SecurityRuleDirection.Inbound,
42+
Access = SecurityRuleAccess.Deny,
43+
Protocol = SecurityRuleProtocol.Asterisk,
44+
SourceAddressPrefix = "VirtualNetwork",
45+
SourcePortRange = "*",
46+
DestinationAddressPrefix = "*",
47+
DestinationPortRange = "*"
48+
})
49+
.WithSecurityRule(new AzureSecurityRule
50+
{
51+
Name = "deny-internet-inbound",
52+
Priority = 4096,
53+
Direction = SecurityRuleDirection.Inbound,
54+
Access = SecurityRuleAccess.Deny,
55+
Protocol = SecurityRuleProtocol.Asterisk,
56+
SourceAddressPrefix = "Internet",
57+
SourcePortRange = "*",
58+
DestinationAddressPrefix = "*",
59+
DestinationPortRange = "*"
60+
});
61+
62+
var peNsg = vnet.AddNetworkSecurityGroup("pe-nsg")
63+
.WithSecurityRule(new AzureSecurityRule
64+
{
65+
Name = "allow-https-from-vnet",
66+
Priority = 100,
67+
Direction = SecurityRuleDirection.Inbound,
68+
Access = SecurityRuleAccess.Allow,
69+
Protocol = SecurityRuleProtocol.Tcp,
70+
SourceAddressPrefix = "VirtualNetwork",
71+
SourcePortRange = "*",
72+
DestinationAddressPrefix = "*",
73+
DestinationPortRange = "443"
74+
})
75+
.WithSecurityRule(new AzureSecurityRule
76+
{
77+
Name = "deny-all-internet-inbound",
78+
Priority = 4096,
79+
Direction = SecurityRuleDirection.Inbound,
80+
Access = SecurityRuleAccess.Deny,
81+
Protocol = SecurityRuleProtocol.Asterisk,
82+
SourceAddressPrefix = "Internet",
83+
SourcePortRange = "*",
84+
DestinationAddressPrefix = "*",
85+
DestinationPortRange = "*"
86+
});
87+
88+
containerAppsSubnet.WithNetworkSecurityGroup(acaNsg);
89+
privateEndpointsSubnet.WithNetworkSecurityGroup(peNsg);
90+
1891
// Configure the Container App Environment to use the VNet
1992
builder.AddAzureContainerAppEnvironment("env")
2093
.WithDelegatedSubnet(containerAppsSubnet);

playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/vnet.module.bicep

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,91 @@ resource vnet 'Microsoft.Network/virtualNetworks@2025-05-01' = {
1818
}
1919
}
2020

21+
resource aca_nsg 'Microsoft.Network/networkSecurityGroups@2025-05-01' = {
22+
name: take('aca_nsg-${uniqueString(resourceGroup().id)}', 80)
23+
location: location
24+
}
25+
26+
resource aca_nsg_allow_https_from_azure_lb 'Microsoft.Network/networkSecurityGroups/securityRules@2025-05-01' = {
27+
name: 'allow-https-from-azure-lb'
28+
properties: {
29+
access: 'Allow'
30+
destinationAddressPrefix: '*'
31+
destinationPortRange: '443'
32+
direction: 'Inbound'
33+
priority: 100
34+
protocol: 'Tcp'
35+
sourceAddressPrefix: 'AzureLoadBalancer'
36+
sourcePortRange: '*'
37+
}
38+
parent: aca_nsg
39+
}
40+
41+
resource aca_nsg_deny_vnet_inbound 'Microsoft.Network/networkSecurityGroups/securityRules@2025-05-01' = {
42+
name: 'deny-vnet-inbound'
43+
properties: {
44+
access: 'Deny'
45+
destinationAddressPrefix: '*'
46+
destinationPortRange: '*'
47+
direction: 'Inbound'
48+
priority: 110
49+
protocol: '*'
50+
sourceAddressPrefix: 'VirtualNetwork'
51+
sourcePortRange: '*'
52+
}
53+
parent: aca_nsg
54+
}
55+
56+
resource aca_nsg_deny_internet_inbound 'Microsoft.Network/networkSecurityGroups/securityRules@2025-05-01' = {
57+
name: 'deny-internet-inbound'
58+
properties: {
59+
access: 'Deny'
60+
destinationAddressPrefix: '*'
61+
destinationPortRange: '*'
62+
direction: 'Inbound'
63+
priority: 4096
64+
protocol: '*'
65+
sourceAddressPrefix: 'Internet'
66+
sourcePortRange: '*'
67+
}
68+
parent: aca_nsg
69+
}
70+
71+
resource pe_nsg 'Microsoft.Network/networkSecurityGroups@2025-05-01' = {
72+
name: take('pe_nsg-${uniqueString(resourceGroup().id)}', 80)
73+
location: location
74+
}
75+
76+
resource pe_nsg_allow_https_from_vnet 'Microsoft.Network/networkSecurityGroups/securityRules@2025-05-01' = {
77+
name: 'allow-https-from-vnet'
78+
properties: {
79+
access: 'Allow'
80+
destinationAddressPrefix: '*'
81+
destinationPortRange: '443'
82+
direction: 'Inbound'
83+
priority: 100
84+
protocol: 'Tcp'
85+
sourceAddressPrefix: 'VirtualNetwork'
86+
sourcePortRange: '*'
87+
}
88+
parent: pe_nsg
89+
}
90+
91+
resource pe_nsg_deny_all_internet_inbound 'Microsoft.Network/networkSecurityGroups/securityRules@2025-05-01' = {
92+
name: 'deny-all-internet-inbound'
93+
properties: {
94+
access: 'Deny'
95+
destinationAddressPrefix: '*'
96+
destinationPortRange: '*'
97+
direction: 'Inbound'
98+
priority: 4096
99+
protocol: '*'
100+
sourceAddressPrefix: 'Internet'
101+
sourcePortRange: '*'
102+
}
103+
parent: pe_nsg
104+
}
105+
21106
resource container_apps 'Microsoft.Network/virtualNetworks/subnets@2025-05-01' = {
22107
name: 'container-apps'
23108
properties: {
@@ -33,6 +118,9 @@ resource container_apps 'Microsoft.Network/virtualNetworks/subnets@2025-05-01' =
33118
natGateway: {
34119
id: nat_outputs_id
35120
}
121+
networkSecurityGroup: {
122+
id: aca_nsg.id
123+
}
36124
}
37125
parent: vnet
38126
}
@@ -41,6 +129,9 @@ resource private_endpoints 'Microsoft.Network/virtualNetworks/subnets@2025-05-01
41129
name: 'private-endpoints'
42130
properties: {
43131
addressPrefix: '10.0.2.0/27'
132+
networkSecurityGroup: {
133+
id: pe_nsg.id
134+
}
44135
}
45136
parent: vnet
46137
dependsOn: [
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Aspire.Hosting.ApplicationModel;
5+
using Azure.Provisioning;
6+
using Azure.Provisioning.Network;
7+
8+
namespace Aspire.Hosting.Azure;
9+
10+
/// <summary>
11+
/// Represents an Azure Network Security Group resource.
12+
/// </summary>
13+
/// <remarks>
14+
/// Use <see cref="AzureVirtualNetworkExtensions.AddNetworkSecurityGroup"/> to create an instance
15+
/// and <see cref="AzureVirtualNetworkExtensions.WithSecurityRule"/> to add security rules.
16+
/// Associate the NSG with a subnet using <see cref="AzureVirtualNetworkExtensions.WithNetworkSecurityGroup"/>.
17+
/// </remarks>
18+
public class AzureNetworkSecurityGroupResource : Resource, IResourceWithParent<AzureVirtualNetworkResource>
19+
{
20+
/// <summary>
21+
/// Initializes a new instance of the <see cref="AzureNetworkSecurityGroupResource"/> class.
22+
/// </summary>
23+
/// <param name="name">The name of the resource.</param>
24+
/// <param name="parent">The parent Virtual Network resource.</param>
25+
public AzureNetworkSecurityGroupResource(string name, AzureVirtualNetworkResource parent)
26+
: base(name)
27+
{
28+
Parent = parent ?? throw new ArgumentNullException(nameof(parent));
29+
}
30+
31+
/// <summary>
32+
/// Gets the parent Azure Virtual Network resource.
33+
/// </summary>
34+
public AzureVirtualNetworkResource Parent { get; }
35+
36+
internal List<AzureSecurityRule> SecurityRules { get; } = [];
37+
38+
/// <summary>
39+
/// Converts the current instance to provisioning entities: the NSG and its security rules.
40+
/// </summary>
41+
internal (NetworkSecurityGroup Nsg, List<SecurityRule> Rules) ToProvisioningEntity()
42+
{
43+
var nsg = new NetworkSecurityGroup(Infrastructure.NormalizeBicepIdentifier(Name));
44+
45+
var rules = new List<SecurityRule>();
46+
foreach (var rule in SecurityRules)
47+
{
48+
var ruleIdentifier = Infrastructure.NormalizeBicepIdentifier($"{nsg.BicepIdentifier}_{rule.Name}");
49+
var securityRule = new SecurityRule(ruleIdentifier)
50+
{
51+
Name = rule.Name,
52+
Priority = rule.Priority,
53+
Direction = rule.Direction,
54+
Access = rule.Access,
55+
Protocol = rule.Protocol,
56+
SourceAddressPrefix = rule.SourceAddressPrefix,
57+
SourcePortRange = rule.SourcePortRange,
58+
DestinationAddressPrefix = rule.DestinationAddressPrefix,
59+
DestinationPortRange = rule.DestinationPortRange,
60+
Parent = nsg,
61+
};
62+
rules.Add(securityRule);
63+
}
64+
65+
return (nsg, rules);
66+
}
67+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Azure.Provisioning.Network;
5+
6+
namespace Aspire.Hosting.Azure;
7+
8+
/// <summary>
9+
/// Represents a security rule configuration for an Azure Network Security Group.
10+
/// </summary>
11+
/// <remarks>
12+
/// Security rules control inbound and outbound network traffic for subnets associated with the Network Security Group.
13+
/// Rules are evaluated in priority order, with lower numbers having higher priority.
14+
/// </remarks>
15+
public sealed class AzureSecurityRule
16+
{
17+
/// <summary>
18+
/// Gets or sets the name of the security rule. This name must be unique within the Network Security Group.
19+
/// </summary>
20+
public required string Name { get; set; }
21+
22+
/// <summary>
23+
/// Gets or sets the priority of the rule. Valid values are between 100 and 4096. Lower numbers have higher priority.
24+
/// </summary>
25+
public required int Priority { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the direction of the rule.
29+
/// </summary>
30+
public required SecurityRuleDirection Direction { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets whether network traffic is allowed or denied.
34+
/// </summary>
35+
public required SecurityRuleAccess Access { get; set; }
36+
37+
/// <summary>
38+
/// Gets or sets the network protocol this rule applies to.
39+
/// </summary>
40+
public required SecurityRuleProtocol Protocol { get; set; }
41+
42+
/// <summary>
43+
/// Gets or sets the source address prefix. Use "*" for any.
44+
/// </summary>
45+
public required string SourceAddressPrefix { get; set; }
46+
47+
/// <summary>
48+
/// Gets or sets the source port range. Use "*" for any.
49+
/// </summary>
50+
public required string SourcePortRange { get; set; }
51+
52+
/// <summary>
53+
/// Gets or sets the destination address prefix. Use "*" for any.
54+
/// </summary>
55+
public required string DestinationAddressPrefix { get; set; }
56+
57+
/// <summary>
58+
/// Gets or sets the destination port range. Use "*" for any, or a range like "80-443".
59+
/// </summary>
60+
public required string DestinationPortRange { get; set; }
61+
}

src/Aspire.Hosting.Azure.Network/AzureSubnetResource.cs

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,13 +82,18 @@ public AzureSubnetResource(string name, string subnetName, ParameterResource add
8282
/// </summary>
8383
internal AzureNatGatewayResource? NatGateway { get; set; }
8484

85+
/// <summary>
86+
/// Gets or sets the Network Security Group associated with the subnet.
87+
/// </summary>
88+
internal AzureNetworkSecurityGroupResource? NetworkSecurityGroup { get; set; }
89+
8590
private static string ThrowIfNullOrEmpty([NotNull] string? argument, [CallerArgumentExpression(nameof(argument))] string? paramName = null)
8691
=> !string.IsNullOrEmpty(argument) ? argument : throw new ArgumentNullException(paramName);
8792

8893
/// <summary>
8994
/// Converts the current instance to a provisioning entity.
9095
/// </summary>
91-
internal SubnetResource ToProvisioningEntity(AzureResourceInfrastructure infra, ProvisionableResource? dependsOn)
96+
internal SubnetResource ToProvisioningEntity(AzureResourceInfrastructure infra, ProvisionableResource? dependsOn, Dictionary<AzureNetworkSecurityGroupResource, NetworkSecurityGroup> nsgMap)
9297
{
9398
var subnet = new SubnetResource(Infrastructure.NormalizeBicepIdentifier(Name))
9499
{
@@ -129,6 +134,14 @@ internal SubnetResource ToProvisioningEntity(AzureResourceInfrastructure infra,
129134
subnet.NatGatewayId = NatGateway.Id.AsProvisioningParameter(infra);
130135
}
131136

137+
if (NetworkSecurityGroup is not null &&
138+
nsgMap.TryGetValue(NetworkSecurityGroup, out var provisioningNsg))
139+
{
140+
// Set the NSG reference on the subnet by setting the model's Id property.
141+
// This produces the correct bicep: networkSecurityGroup: { id: nsg.id }
142+
subnet.NetworkSecurityGroup.Id = provisioningNsg.Id;
143+
}
144+
132145
// add a provisioning output for the subnet ID so it can be referenced by other resources
133146
infra.Add(new ProvisioningOutput(Id.Name, typeof(string))
134147
{

0 commit comments

Comments
 (0)