diff --git a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs index f7dcb60b2b4..81ad596b9e8 100644 --- a/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs +++ b/playground/AzureVirtualNetworkEndToEnd/AzureVirtualNetworkEndToEnd.AppHost/Program.cs @@ -3,6 +3,7 @@ #pragma warning disable AZPROVISION001 // Azure.Provisioning.Network is experimental +using Aspire.Hosting.Azure; using Azure.Provisioning.Network; var builder = DistributedApplication.CreateBuilder(args); @@ -13,17 +14,17 @@ var vnet = builder.AddAzureVirtualNetwork("vnet"); var containerAppsSubnet = vnet.AddSubnet("container-apps", "10.0.0.0/23") - .AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp) - .DenyInbound(from: "VirtualNetwork") - .DenyInbound(from: "Internet"); + .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureServiceTags.VirtualNetwork) + .DenyInbound(from: AzureServiceTags.Internet); // Create a NAT Gateway for deterministic outbound IP on the ACA subnet var natGateway = builder.AddNatGateway("nat"); containerAppsSubnet.WithNatGateway(natGateway); var privateEndpointsSubnet = vnet.AddSubnet("private-endpoints", "10.0.2.0/27") - .AllowInbound(port: "443", from: "VirtualNetwork", protocol: SecurityRuleProtocol.Tcp) - .DenyInbound(from: "Internet"); + .AllowInbound(port: "443", from: AzureServiceTags.VirtualNetwork, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureServiceTags.Internet); // Configure the Container App Environment to use the VNet builder.AddAzureContainerAppEnvironment("env") diff --git a/src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs b/src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs new file mode 100644 index 00000000000..d09d8b61ec6 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Network/AzureServiceTags.cs @@ -0,0 +1,111 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Aspire.Hosting.Azure; + +/// +/// Provides well-known Azure service tags that can be used as source or destination address prefixes +/// in network security group rules. +/// +/// +/// +/// Service tags represent a group of IP address prefixes from a given Azure service. Microsoft manages the +/// address prefixes encompassed by each tag and automatically updates them as addresses change. +/// +/// +/// These tags can be used with the from and to parameters of methods such as +/// , , +/// , , +/// or with the and properties. +/// +/// +/// +/// Use service tags when configuring network security rules: +/// +/// var subnet = vnet.AddSubnet("web", "10.0.1.0/24") +/// .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) +/// .DenyInbound(from: AzureServiceTags.Internet); +/// +/// +public static class AzureServiceTags +{ + /// + /// Represents the Internet address space, including all publicly routable IP addresses. + /// + public const string Internet = nameof(Internet); + + /// + /// Represents the address space for the virtual network, including all connected address spaces, + /// all connected on-premises address spaces, and peered virtual networks. + /// + public const string VirtualNetwork = nameof(VirtualNetwork); + + /// + /// Represents the Azure infrastructure load balancer. This tag is commonly used to allow + /// health probe traffic from Azure. + /// + public const string AzureLoadBalancer = nameof(AzureLoadBalancer); + + /// + /// Represents Azure Traffic Manager probe IP addresses. + /// + public const string AzureTrafficManager = nameof(AzureTrafficManager); + + /// + /// Represents the Azure Storage service. This tag does not include specific Storage accounts; + /// it covers all Azure Storage IP addresses. + /// + public const string Storage = nameof(Storage); + + /// + /// Represents Azure SQL Database, Azure Database for MySQL, Azure Database for PostgreSQL, + /// Azure Database for MariaDB, and Azure Synapse Analytics. + /// + public const string Sql = nameof(Sql); + + /// + /// Represents Azure Cosmos DB service addresses. + /// + public const string AzureCosmosDB = nameof(AzureCosmosDB); + + /// + /// Represents Azure Key Vault service addresses. + /// + public const string AzureKeyVault = nameof(AzureKeyVault); + + /// + /// Represents Azure Event Hubs service addresses. + /// + public const string EventHub = nameof(EventHub); + + /// + /// Represents Azure Service Bus service addresses. + /// + public const string ServiceBus = nameof(ServiceBus); + + /// + /// Represents Azure Container Registry service addresses. + /// + public const string AzureContainerRegistry = nameof(AzureContainerRegistry); + + /// + /// Represents Azure App Service and Azure Functions service addresses. + /// + public const string AppService = nameof(AppService); + + /// + /// Represents Microsoft Entra ID (formerly Azure Active Directory) service addresses. + /// + public const string AzureActiveDirectory = nameof(AzureActiveDirectory); + + /// + /// Represents Azure Monitor service addresses, including Log Analytics, Application Insights, + /// and Azure Monitor metrics. + /// + public const string AzureMonitor = nameof(AzureMonitor); + + /// + /// Represents the Gateway Manager service, used for VPN Gateway and Application Gateway management traffic. + /// + public const string GatewayManager = nameof(GatewayManager); +} diff --git a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs index 4e15aa7ad3f..3d5d18475c0 100644 --- a/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs +++ b/src/Aspire.Hosting.Azure.Network/AzureVirtualNetworkExtensions.cs @@ -361,8 +361,8 @@ public static IResourceBuilder WithNetworkSecurityGroup( /// This example allows HTTPS traffic from the Azure Load Balancer: /// /// var subnet = vnet.AddSubnet("web", "10.0.1.0/24") - /// .AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp) - /// .DenyInbound(from: "Internet"); + /// .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + /// .DenyInbound(from: AzureServiceTags.Internet); /// /// public static IResourceBuilder AllowInbound( diff --git a/src/Aspire.Hosting.Azure.Network/README.md b/src/Aspire.Hosting.Azure.Network/README.md index 90975e8bbc6..3590fb1e102 100644 --- a/src/Aspire.Hosting.Azure.Network/README.md +++ b/src/Aspire.Hosting.Azure.Network/README.md @@ -90,8 +90,8 @@ Add security rules to control traffic flow on subnets using shorthand methods: ```csharp var vnet = builder.AddAzureVirtualNetwork("vnet"); var subnet = vnet.AddSubnet("web", "10.0.1.0/24") - .AllowInbound(port: "443", from: "AzureLoadBalancer", protocol: SecurityRuleProtocol.Tcp) - .DenyInbound(from: "Internet"); + .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureServiceTags.Internet); ``` An NSG is automatically created when shorthand methods are used. Priority auto-increments (100, 200, 300...) and rule names are auto-generated. diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityGroupExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityGroupExtensionsTests.cs index 964a030b6a3..79ba00243ac 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityGroupExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityGroupExtensionsTests.cs @@ -247,7 +247,7 @@ public async Task MultipleNSGs_WithSameRuleName_GeneratesDistinctBicepIdentifier Direction = SecurityRuleDirection.Inbound, Access = SecurityRuleAccess.Allow, Protocol = SecurityRuleProtocol.Tcp, - SourceAddressPrefix = "VirtualNetwork", + SourceAddressPrefix = AzureServiceTags.VirtualNetwork, SourcePortRange = "*", DestinationAddressPrefix = "*", DestinationPortRange = "443" @@ -271,7 +271,7 @@ public void WithNetworkSecurityGroup_AfterShorthand_Throws() var vnet = builder.AddAzureVirtualNetwork("myvnet"); var nsg = builder.AddNetworkSecurityGroup("web-nsg"); var subnet = vnet.AddSubnet("web-subnet", "10.0.1.0/24") - .AllowInbound(port: "443", from: "AzureLoadBalancer"); + .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer); var exception = Assert.Throws(() => subnet.WithNetworkSecurityGroup(nsg)); diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs index 08fafbde1a3..1747a3b1f96 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureVirtualNetworkExtensionsTests.cs @@ -361,6 +361,66 @@ await Verify(vnetManifest.BicepText, extension: "bicep") .AppendContentAsFile(nsgManifest.BicepText, "bicep", "nsg"); } + [Fact] + public void ServiceTags_CanBeUsedAsFromAndToParameters() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var vnet = builder.AddAzureVirtualNetwork("myvnet"); + var subnet = vnet.AddSubnet("web", "10.0.1.0/24") + .AllowInbound(port: "443", from: AzureServiceTags.AzureLoadBalancer, protocol: SecurityRuleProtocol.Tcp) + .DenyInbound(from: AzureServiceTags.Internet) + .AllowOutbound(port: "443", to: AzureServiceTags.Storage) + .DenyOutbound(to: AzureServiceTags.VirtualNetwork); + + var rules = subnet.Resource.NetworkSecurityGroup!.SecurityRules; + Assert.Equal(4, rules.Count); + + Assert.Equal("AzureLoadBalancer", rules[0].SourceAddressPrefix); + Assert.Equal("Internet", rules[1].SourceAddressPrefix); + Assert.Equal("Storage", rules[2].DestinationAddressPrefix); + Assert.Equal("VirtualNetwork", rules[3].DestinationAddressPrefix); + } + + [Fact] + public void ServiceTags_CanBeUsedInSecurityRuleProperties() + { + var rule = new AzureSecurityRule + { + Name = "allow-https-from-lb", + Priority = 100, + Direction = SecurityRuleDirection.Inbound, + Access = SecurityRuleAccess.Allow, + Protocol = SecurityRuleProtocol.Tcp, + SourceAddressPrefix = AzureServiceTags.AzureLoadBalancer, + DestinationAddressPrefix = AzureServiceTags.VirtualNetwork, + DestinationPortRange = "443" + }; + + Assert.Equal("AzureLoadBalancer", rule.SourceAddressPrefix); + Assert.Equal("VirtualNetwork", rule.DestinationAddressPrefix); + } + + [Fact] + public void ServiceTags_HaveExpectedValues() + { + Assert.Equal("Internet", AzureServiceTags.Internet); + Assert.Equal("VirtualNetwork", AzureServiceTags.VirtualNetwork); + Assert.Equal("AzureLoadBalancer", AzureServiceTags.AzureLoadBalancer); + Assert.Equal("AzureTrafficManager", AzureServiceTags.AzureTrafficManager); + Assert.Equal("Storage", AzureServiceTags.Storage); + Assert.Equal("Sql", AzureServiceTags.Sql); + Assert.Equal("AzureCosmosDB", AzureServiceTags.AzureCosmosDB); + Assert.Equal("AzureKeyVault", AzureServiceTags.AzureKeyVault); + Assert.Equal("EventHub", AzureServiceTags.EventHub); + Assert.Equal("ServiceBus", AzureServiceTags.ServiceBus); + Assert.Equal("AzureContainerRegistry", AzureServiceTags.AzureContainerRegistry); + Assert.Equal("AppService", AzureServiceTags.AppService); + Assert.Equal("AzureActiveDirectory", AzureServiceTags.AzureActiveDirectory); + Assert.Equal("AzureMonitor", AzureServiceTags.AzureMonitor); + Assert.Equal("GatewayManager", AzureServiceTags.GatewayManager); + } + [Fact] public void AllFourDirectionAccessCombos_SetCorrectly() {