May 14th, 2026
0 reactions

Securing Azure apps with Aspire enterprise networking

BERJAYA
Principal Software Engineer

Network security has a funny way of showing up late.

You start with a web app, an API, storage, and Key Vault. The app works. The demo works. Everyone is happy. Then the production checklist shows up:

  • Can storage and secrets be taken off the public internet?
  • Can this app run inside our virtual network?
  • What outbound IPs do we give to the partner allowlist?
  • Can we prove only the right subnets can talk to the right services?
  • What happens when someone accidentally opens a storage account to the world?

That is usually the point where the application model and the infrastructure model start drifting apart. The app says “I need storage and Key Vault.” The networking template says “I have seven subnets, two private DNS zones, a NAT gateway, a network security group, and a growing collection of comments explaining why nobody should touch any of it.”

Aspire makes this a lot nicer. The new Azure networking support lets you describe the network shape next to the resources that use it. Virtual networks (VNets), subnets, delegated subnets, NAT gateways, private endpoints, Network Security Groups (NSGs), and Network Security Perimeters (NSPs) can all be modeled from your AppHost.

This post is not going to pretend every enterprise network is simple. They are not. But the common building blocks are easier to reason about once you know what each one is for.

The short version

Here is a rough mental model for each Azure networking feature:

Azure feature Use it when Why it matters
Virtual network You need a private address space for the app It is the network boundary your subnets, private endpoints, and routing attach to
Subnet You need to separate workloads inside the VNet It gives each part of the system its own address range and policy surface
Delegated subnet A platform service needs to manage a subnet, like Azure Container Apps (ACA) It lets the service place its managed infrastructure in your VNet safely
NAT gateway You need predictable outbound public IPs It gives outbound traffic a stable address for allowlists and auditing
Private endpoint You want a platform as a service (PaaS) resource reachable privately It puts a private IP for that service inside your VNet and removes public exposure
NSG You need subnet-level traffic rules It is the basic allow/deny control for traffic entering or leaving a subnet
NSP You need PaaS-layer guardrails across services It lets you lock down Azure PaaS services to approved networks or subscriptions instead of the entire internet, even when those services still need public network access

The important part is that these are not competing features. A good production setup usually uses several of them together.

If you want the docs open while you read, start with Azure security best practices for Aspire deployments and the Aspire Azure Virtual Network integration reference.

Start with the network shape

Aspire 13.2 introduced the Aspire Azure Virtual Network integration. It is provided by the Aspire.Hosting.Azure.Network package, and it is the piece that lets your AppHost describe Azure networking primitives directly. Add it with:

aspire add azure-network

Then you can start modeling the network in code:

#:package Aspire.Hosting.Azure.AppContainers@13.3.0
#:package Aspire.Hosting.Azure.Network@13.3.0

var builder = DistributedApplication.CreateBuilder(args);

var network = builder.AddAzureVirtualNetwork(
    "app-network",
    addressPrefix: "10.20.0.0/16");

var appsSubnet = network.AddSubnet("apps", "10.20.0.0/23");
var privateEndpointSubnet = network.AddSubnet(
    "private-endpoints",
    "10.20.10.0/27");
var dataSubnet = network.AddSubnet("data", "10.20.20.0/24");

That is already useful. You have a single place where the application and its network layout live together.

The VNet is the private address space. The subnets are where you start drawing boundaries. It’s a good idea to keep separate subnets for:

  • compute, such as Azure Container Apps
  • private endpoints
  • data or service-specific infrastructure
  • shared egress, if the environment needs a dedicated outbound path

Could you put everything in one subnet? Sometimes. Should you? Usually no. The minute you want different routing, different NSG rules, or a clean way to explain the design to your security team, separate subnets pay for themselves.

Azure Container Apps wants a delegated subnet

ACA can run in your virtual network. When you do that, the Container Apps environment needs a subnet that is delegated to Microsoft.App/environments.

In Aspire, that becomes part of the same resource graph:

var containerApps = builder.AddAzureContainerAppEnvironment("apps")
    .WithDelegatedSubnet(appsSubnet);

builder.AddProject<Projects.Api>("api");

builder.AddProject<Projects.Web>("web");

That WithDelegatedSubnet call is doing something important. It tells Azure that this subnet is for the managed Container Apps environment, not a random subnet where anything can be dropped later.

This is one of those network details that is easy to miss when you are writing infrastructure by hand. ACA owns infrastructure inside that subnet. It needs room to scale. It needs the delegation. And you generally should not mix private endpoints or unrelated resources into the same subnet.

My practical rule: give ACA its own delegated subnet and size it with growth in mind. The Azure Container Apps docs have recommendations for choosing the subnet size.

Use NAT gateways when someone asks, “What IP does this come from?”

Many apps eventually call something outside Azure:

  • a payment provider
  • a legacy API
  • a partner endpoint
  • a corporate firewall
  • a SaaS service with an IP allowlist

If that external system needs to allowlist your outbound traffic, “whatever IP Azure happens to use today” is not a satisfying answer.

A NAT gateway gives outbound traffic from a subnet a stable public IP.

var builder = DistributedApplication.CreateBuilder(args);

var network = builder.AddAzureVirtualNetwork(
    "app-network",
    addressPrefix: "10.20.0.0/16");
var appsSubnet = network.AddSubnet("apps", "10.20.0.0/23");

var egressIp = builder.AddPublicIPAddress("egress-ip");

var natGateway = builder.AddNatGateway("egress")
    .WithPublicIPAddress(egressIp);

appsSubnet.WithNatGateway(natGateway);

Now resources in appsSubnet have a deterministic outbound path.

This does not make the app private by itself. It is about egress, not ingress. Use it when outbound identity matters: partner allowlists, firewall rules, centralized auditing, or incident response.

One small warning: NAT gateway is not a policy engine. It does not decide which domains your app is allowed to call. It gives the traffic a predictable public IP. Pair it with routing, NSGs, Azure Firewall, or other controls when you need actual outbound filtering.

Private endpoints are for taking PaaS off the public internet

Private endpoints are one of the biggest wins for securing Azure resources. Instead of reaching a storage account or Key Vault through a public endpoint, Azure gives that service a private IP inside your VNet.

Aspire models this from the subnet that will hold the private endpoint:

var builder = DistributedApplication.CreateBuilder(args);

var network = builder.AddAzureVirtualNetwork(
    "app-network",
    addressPrefix: "10.20.0.0/16");
var privateEndpointSubnet = network.AddSubnet(
    "private-endpoints",
    "10.20.10.0/27");

var storage = builder.AddAzureStorage("storage");
var blobs = storage.AddBlobs("documents");
var keyVault = builder.AddAzureKeyVault("secrets");

privateEndpointSubnet.AddPrivateEndpoint(blobs);
privateEndpointSubnet.AddPrivateEndpoint(keyVault);

Aspire handles the annoying parts that usually come with private endpoints:

  • creating the private endpoint resource
  • creating and linking the private DNS zone
  • wiring DNS zone groups
  • configuring the target resource to deny public network access when deployed

That last point is the one that matters the most. A private endpoint that still leaves the public endpoint wide open is only half a security story.

Storage and Key Vault are great examples because they are so common. Almost every production app needs durable data and secrets, and neither one needs to be broadly reachable from the public internet.

Private endpoints are the right tool when the question is:

Can this service be reachable only through my private network?

If the answer should be yes, reach for a private endpoint.

NSGs are the subnet traffic rules

Network Security Groups (NSGs) are the familiar allow/deny rules for traffic entering or leaving a subnet. They are not glamorous, but they are still one of the most useful controls in Azure networking.

Aspire gives you shorthand helpers on subnets:

using Azure.Provisioning.Network;
...

appsSubnet
    .AllowInbound(
        port: "443",
        from: AzureServiceTags.AzureLoadBalancer,
        protocol: SecurityRuleProtocol.Tcp)
    .DenyInbound(from: AzureServiceTags.Internet);

For more explicit control, you can create an NSG resource and attach it:

using Azure.Provisioning.Network;
...

var appNsg = builder.AddNetworkSecurityGroup("apps-nsg")
    .WithSecurityRule(new AzureSecurityRule
    {
        Name = "allow-https-from-load-balancer",
        Priority = 100,
        Direction = SecurityRuleDirection.Inbound,
        Access = SecurityRuleAccess.Allow,
        Protocol = SecurityRuleProtocol.Tcp,
        SourceAddressPrefix = AzureServiceTags.AzureLoadBalancer,
        SourcePortRange = "*",
        DestinationAddressPrefix = "*",
        DestinationPortRange = "443"
    });

appsSubnet.WithNetworkSecurityGroup(appNsg);

Use NSGs for broad traffic rules at the subnet boundary. For example, you can:

  • allow HTTPS traffic to the app subnet only from Azure’s load-balancing infrastructure
  • deny direct inbound traffic from the internet to subnets that should not accept it
  • keep SSH, RDP, and other admin ports closed unless you have an explicit management path
  • limit traffic between subnets so one compromised workload cannot freely reach everything else

NSGs are stateful L3/L4 rules. They are not a replacement for authentication, authorization, WAF policies, or application-level security. Think of them as the network’s first “nope” before traffic gets anywhere near your code.

NSPs protect PaaS resources as a group

Private endpoints are fantastic, but they are not the only way to think about PaaS security. NSPs add a logical boundary around PaaS resources. Instead of only asking “what subnet is this traffic from?”, an NSP lets you group resources like Storage, Key Vault, Cosmos DB, and SQL and define access rules for the perimeter.

Aspire 13.3 adds first-class NSP support:

var builder = DistributedApplication.CreateBuilder(args);

var perimeter = builder.AddNetworkSecurityPerimeter("data-boundary")
    .WithAccessRule(new AzureNspAccessRule
    {
        Name = "allow-corp-network",
        Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound,
        AddressPrefixes = { "203.0.113.0/24" }
    });

var secrets = builder.AddAzureKeyVault("secrets")
    .WithNetworkSecurityPerimeter(
        perimeter,
        NetworkSecurityPerimeterAssociationAccessMode.Learning);

var database = builder.AddAzureCosmosDB("catalog")
    .WithNetworkSecurityPerimeter(perimeter);

var storage = builder.AddAzureStorage("storage")
    .WithNetworkSecurityPerimeter(perimeter);

Learning mode is powerful because you can attach a resource to the perimeter and observe what would be blocked before flipping to Enforced. That gives you a safer rollout path: measure first, then tighten.

NSPs are not a replacement for VNets or private endpoints. They complement them. Use NSPs when you want a PaaS-layer guardrail that follows the resource grouping, not just the network path.

Good places to use NSPs:

  • A storage account or Key Vault that should be reachable by approved apps, build agents, or corporate networks, but not the whole internet.
  • Shared data services used by apps in different VNets or subscriptions, where private endpoints alone do not describe every allowed caller.
  • Production hardening work where Learning mode lets you find legitimate callers before you start blocking traffic.

Putting it together

The smaller snippets above are focused on one idea at a time. If you want a copy/paste starting point, use this complete AppHost shape. It includes the package directives and using statements needed for the networking APIs.

#:sdk Aspire.AppHost.Sdk@13.3.0
#:package Aspire.Hosting.Azure.AppContainers@13.3.0
#:package Aspire.Hosting.Azure.KeyVault@13.3.0
#:package Aspire.Hosting.Azure.Network@13.3.0
#:package Aspire.Hosting.Azure.Storage@13.3.0

#pragma warning disable ASPIREAZURE003

using Aspire.Hosting.Azure;
using Azure.Provisioning.Network;

var builder = DistributedApplication.CreateBuilder(args);

var network = builder.AddAzureVirtualNetwork(
    "app-network",
    addressPrefix: "10.20.0.0/16");

var appsSubnet = network.AddSubnet("apps", "10.20.0.0/23");
var privateEndpointSubnet = network.AddSubnet(
    "private-endpoints",
    "10.20.10.0/27");

var egressIp = builder.AddPublicIPAddress("egress-ip");
var natGateway = builder.AddNatGateway("egress")
    .WithPublicIPAddress(egressIp);

appsSubnet
    .WithNatGateway(natGateway)
    .AllowInbound(
        port: "443",
        from: AzureServiceTags.AzureLoadBalancer,
        protocol: SecurityRuleProtocol.Tcp)
    .DenyInbound(from: AzureServiceTags.Internet);

var containerApps = builder.AddAzureContainerAppEnvironment("apps")
    .WithDelegatedSubnet(appsSubnet);

var perimeter = builder.AddNetworkSecurityPerimeter("data-boundary")
    .WithAccessRule(new AzureNspAccessRule
    {
        Name = "allow-corp-network",
        Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound,
        AddressPrefixes = { "203.0.113.0/24" }
    });

var storage = builder.AddAzureStorage("storage")
    .WithNetworkSecurityPerimeter(perimeter);
var blobs = storage.AddBlobs("documents");

var keyVault = builder.AddAzureKeyVault("secrets");

privateEndpointSubnet.AddPrivateEndpoint(keyVault);

builder.AddProject("api", "api/api.csproj")
    .WithReference(blobs)
    .WithReference(keyVault);

builder.Build().Run();

A simplified diagram for that AppHost looks like this:

network architecture image

Putting the network in the AppHost does not remove the need for good network design. It makes the design visible at the same level as the app itself. In the example above, the API references storage and Key Vault, while the network model says how those resources are exposed, where the app runs, what subnet ACA uses, what outbound path it takes, and which resource is protected by the perimeter.

How to roll this out

If you already have an Aspire app and want to harden the Azure side, don’t try to do everything at once. Instead, try breaking each piece into its own step:

  1. Add a VNet and give your app a real subnet plan.
  2. Move Azure Container Apps into a delegated subnet.
  3. Add a private endpoint subnet.
  4. Put Storage and Key Vault behind private endpoints.
  5. Add a NAT gateway if anything outside Azure needs stable outbound IPs.
  6. Add NSG rules that document and enforce the subnet boundaries.
  7. Add NSPs for PaaS resources, start in Learning mode, then move to Enforced when the logs look right.

That sequence keeps each step understandable. It also gives you a clean rollback story if you discover some dependency you did not know existed.

A few mistakes to avoid

Do not put private endpoints in the ACA delegated subnet. Give private endpoints their own subnet. It keeps the design cleaner and avoids mixing platform-managed compute infrastructure with service endpoints.

Do not make your subnets too small. ACA scales. Private endpoints accumulate. Future you will not be impressed by a perfectly packed IP plan that has no room for the next service.

Do not use NAT gateway as if it were a firewall. It gives you stable outbound IPs. It does not decide whether calling example.com is okay.

Do not assume private endpoints automatically solve every access path. Check the target resource’s public network access settings and DNS behavior. Aspire helps here, but the architecture still needs to be intentional.

Do not jump straight to enforced NSP rules on a busy production environment. Learning mode exists for a reason.

Try it

Upgrade to Aspire 13.3 and start small:

aspire update --self
aspire update

Then start with the network shape: add a VNet, decide which subnets you need, and give Azure Container Apps its own delegated subnet. From there, add private endpoints, NAT gateway, NSG rules, and NSPs as needed.

Networking can still be complex. But with Aspire, it does not have to be a separate story from the app. That is the part that changes the day-to-day experience: the secure architecture is right there in the code you already use to describe the system.

Author

Eric Erhardt
Principal Software Engineer

Working on Aspire

2 comments

Sort by :
  • BERJAYA
    David Gardiner

    Is this purely for publishing /deploying, or does it have any value if you’re just using Aspire for local development?

    David

    • BERJAYA
      Eric ErhardtMicrosoft employee Author

      It only affects deployed apps – just like compute environments like Azure Container Apps or App Service. The network resources are only deployed at publish/deploy time.

      When running your apps locally, they don’t run as part of the virtual network, nor have access to the private endpoints.