Securing Azure Application Networks: Best Practices with Terraform

  • Thread Author
When deploying modern applications to Azure, security isn’t just an afterthought—it’s baked into the architecture. In this deep dive, we’ll explore how to secure your application network using Azure’s built-in features, with a specific focus on Terraform code examples. We’ll examine how to design subnets, set up proper delegation, configure private endpoints, and leverage Azure Private Link to enable seamless, secure communication between your services—all while keeping external exposure to a minimum.

Securing Your Application Network in Azure: An Overview​

When you deploy an app to Azure, the last thing you want is an open door for unauthorized access. Microsoft recommends isolating your services into dedicated subnets, ensuring that each component—be it a function app, a database, or an API—is accessible only to the services that need to communicate. This approach adheres to the principle of least privilege and helps minimize the attack surface of your applications.
Azure’s network security blueprint includes several key components:
  • Subnets and Virtual Networks (VNets): Act as the foundational layer, setting up isolated environments.
  • Subnet Delegation: Automates configuration tasks like IP allocation and routing, but comes with important caveats.
  • Private Endpoints: Offer private IP connectivity for Azure services, ensuring that even if traffic is routed via the Azure backbone, it remains shielded from the public internet.
  • Private Link: Bridges the gap between services using private endpoints and the actual application resources.
  • DNS & Network Security Groups (NSGs): Manage internal name resolution and restrict traffic flows, respectively.
In our case study, we’ll examine a scenario where two function apps are deployed in a shared VNet but within their own dedicated subnets. Let’s break down the key components and the intricacies involved in such a setup.

The Architecture: Segregation and Service-Specific Subnets​

Subnets and Their Roles​

Azure’s best practice is to isolate services into separate subnets. In our example, two distinct function apps reside in their own subnets within the same VNet. This ensures that each service is only accessible to the designated consumers. However, deploying services in isolation isn’t enough on its own—each subnet must be carefully configured to support the intended workloads.

Subnet Delegation​

Delegation allows Azure to automatically manage key networking rules for specific services. For instance, by delegating a subnet to an Azure service like App Service or Azure Functions, you enable Azure to:
  • Automatically allocate IP addresses.
  • Enforce routing rules designed for that service.
  • Prevent conflicts by reserving the subnet exclusively for the intended service.
  • Apply service-specific policies without manual intervention.
Consider the following Terraform snippet that creates two subnets and adds delegation for Azure Function apps:
Code:
resource "azurerm_virtual_network" "vnet" {
  name                = "dns-vnet"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  address_space       = ["10.0.0.0/16"]
}

resource "azurerm_subnet" "subnet1" {
  name                 = "subnet1"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.1.0/24"]

  delegation {
    name = "delegation"
    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action",
        "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action",
      ]
    }
  }
}

resource "azurerm_subnet" "subnet2" {
  name                 = "subnet2"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.2.0/24"]

  delegation {
    name = "delegation"
    service_delegation {
      name    = "Microsoft.Web/serverFarms"
      actions = [
        "Microsoft.Network/virtualNetworks/subnets/join/action",
        "Microsoft.Network/virtualNetworks/subnets/prepareNetworkPolicies/action",
      ]
    }
  }
}
By configuring subnets in this manner, you’re essentially establishing “zones of trust” where only the right services are allowed. However, this comes with an important restriction: once a subnet is delegated to a specific service type, you cannot add resources like private endpoints that require unconstrained subnet access.

Private Endpoints and Their Requirements​

A private endpoint in Azure works by assigning a private IP to an Azure service, making it accessible only within your VNet. It’s similar to assigning a NIC (network interface card) with its own IP address to that service. Despite their utility, private endpoints cannot coexist with subnet delegation when that delegation targets services such as those under Microsoft.Web/serverFarms. Attempting to place a private endpoint in a delegated subnet leads to an error:
• Error Example:
"Private endpoint ... cannot be created as the subnet is delegated."
To address this conflict, the solution is to create a dedicated subnet exclusively for private endpoints. For example:
Code:
resource "azurerm_subnet" "subnet3" {
  name                 = "subnet3"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.3.0/24"]
}

resource "azurerm_private_endpoint" "app1_pe" {
  name                = "app1-pe"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  subnet_id           = azurerm_subnet.subnet3.id
}

resource "azurerm_private_endpoint" "app2_pe" {
  name                = "app2-pe"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  subnet_id           = azurerm_subnet.subnet3.id
}
By isolating private endpoints in a separate subnet (in this case, subnet3), we maintain the integrity of our delegated subnets while still allowing secure, private communication between services.

The Role of Azure Private Link​

Once you’ve set up private endpoints, you need a mechanism to connect these endpoints to the actual application resources securely. This is where Azure Private Link comes into play—it facilitates a private connection between the private endpoint and the corresponding Azure service (for example, a function app). Internally, the private endpoint uses Azure’s backbone network to forward requests securely to the target, keeping traffic off the public internet.
Azure Private Link can be used with any Azure PaaS service, including SQL Database, Storage Accounts, App Services, and even Azure Kubernetes Service (AKS). By leveraging Private Link, you can ensure that even if all services are hosted within the same VNet, the communication between them remains secure, isolated, and compliant with regulatory standards.

Diving into Terraform: Code Walkthrough​

In our example, both function apps are deployed using Terraform with carefully structured code to ensure network security. Let’s break down the critical components:

Application Components: Creating Storage and Service Plans​

Before deploying function apps, we first provision a storage account and an App Service Plan. These components provide the backbone for our function apps.
Code:
# Storage account for Function Apps
resource "azurerm_storage_account" "sa1" {
  name                     = "dnsexamplesa"
  resource_group_name      = azurerm_resource_group.rg.name
  location                 = azurerm_resource_group.rg.location
  account_tier             = "Standard"
  account_replication_type = "LRS"
}

# App Service Plan for Function Apps
resource "azurerm_service_plan" "asp" {
  name                = "dns-asp"
  resource_group_name = azurerm_resource_group.rg.name
  location            = azurerm_resource_group.rg.location
  os_type             = "Windows"
  sku_name            = "P1v2"
}
These resources are straightforward but are essential for the deployment and scaling of function apps.

Deploying Function Apps with VNet Integration​

Next, we deploy two function apps that reside in two separate subnets. Notice how each function app is tied back to its respective subnet, ensuring that the applications are isolated from one another. This isolation not only promotes security but also simplifies network management and troubleshooting.
Code:
# Function App 1
resource "azurerm_windows_function_app" "app1" {
  name                      = "dns-app1"
  resource_group_name       = azurerm_resource_group.rg.name
  location                  = azurerm_resource_group.rg.location
  storage_account_name      = azurerm_storage_account.sa1.name
  storage_account_access_key= azurerm_storage_account.sa1.primary_access_key
  service_plan_id           = azurerm_service_plan.asp.id
  virtual_network_subnet_id = azurerm_subnet.subnet1.id

  site_config {
    application_stack {
      dotnet_version = "v8.0"
    }
    cors {
      allowed_origins    = ["[Microsoft Azure](https://portal.azure.com)"]
      support_credentials = true
    }
  }

  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE"           = "1"
    "WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED" = "1"
  }
}

# Function App 2
resource "azurerm_windows_function_app" "app2" {
  name                      = "dns-app2"
  resource_group_name       = azurerm_resource_group.rg.name
  location                  = azurerm_resource_group.rg.location
  storage_account_name      = azurerm_storage_account.sa1.name
  storage_account_access_key= azurerm_storage_account.sa1.primary_access_key
  service_plan_id           = azurerm_service_plan.asp.id
  virtual_network_subnet_id = azurerm_subnet.subnet2.id

  site_config {
    application_stack {
      dotnet_version = "v8.0"
    }
    cors {
      allowed_origins    = ["[Microsoft Azure](https://portal.azure.com)"]
      support_credentials = true
    }
  }

  app_settings = {
    "WEBSITE_RUN_FROM_PACKAGE"           = "1"
    "WEBSITE_USE_PLACEHOLDER_DOTNETISOLATED" = "1"
  }
}
This Terraform code demonstrates best practices: not only are we segmenting our resources into defined subnets, but we’re also ensuring that the function apps can seamlessly communicate internally when necessary, without exposing them to unwanted external traffic.

Configuring Dedicated Subnets for Private Endpoints​

Remember the conflict between subnet delegation and private endpoints? To avoid this issue, a dedicated subnet is created specifically for private endpoints. This design decision is crucial, as it prevents configuration clashes and maintains clear boundaries between different network roles.
Code:
resource "azurerm_subnet" "subnet3" {
  name                 = "subnet3"
  resource_group_name  = azurerm_resource_group.rg.name
  virtual_network_name = azurerm_virtual_network.vnet.name
  address_prefixes     = ["10.0.3.0/24"]
}

resource "azurerm_private_endpoint" "app1_pe" {
  name                = "app1-pe"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  subnet_id           = azurerm_subnet.subnet3.id
}

resource "azurerm_private_endpoint" "app2_pe" {
  name                = "app2-pe"
  location            = azurerm_resource_group.rg.location
  resource_group_name = azurerm_resource_group.rg.name
  subnet_id           = azurerm_subnet.subnet3.id
}
By isolating private endpoints in their own subnet (subnet3), you ensure that these endpoints can securely connect to your function apps over Azure’s private network infrastructure via Azure Private Link. This segregation of duties within your network is a best practice that simplifies both management and troubleshooting.

Testing, Troubleshooting, and Best Practices​

Once you’ve deployed your environment, testing is critical. Ensure that:
  • Function apps in different subnets can communicate where required.
  • Private endpoints correctly resolve via the Azure Private Link setup.
  • NSGs (Network Security Groups) are applied as further safeguards to fine-tune access control.
A robust testing strategy on both the functionality and security fronts can include:
  • Deploying test requests via the Azure Portal and checking the flow of traffic.
  • Verifying DNS resolution aligns with your private endpoints.
  • Auditing NSG rules to ensure no unintended open paths exist.
Rhetorically speaking, can one ever be too careful when exposing critical services to potential threats? The answer remains a resounding no. Design your network with compartmentalization, automated policy enforcement, and clear, dedicated boundaries for each service.

Implications for Broader Cloud Security Strategies​

This approach to application network security in Azure isn’t just a one-off solution—it exemplifies a broader trend in cloud infrastructure management:
  • Security Through Isolation: The closer you adhere to service-specific subnetting and isolation, the fewer lateral movement opportunities exist for potential attackers.
  • Automation with Terraform: Leveraging Terraform for managing Azure resources ensures consistency. It reduces the incidence of human error during network configuration, which could otherwise lead to security vulnerabilities.
  • Evolving Compliance Requirements: Regulatory frameworks and compliance standards often mandate rigorous access controls and network segmentation. Adopting these best practices not only improves security but also simplifies compliance efforts.
Real-world applications of these principles can be seen in scenarios where financial institutions and healthcare providers need to maintain strict data separation and secure connectivity. The approach described here—segregating subnets with delegated policies, dedicated private endpoints, and the use of Private Link—can be tailored to a diverse range of sectors.

Conclusion and Next Steps​

Securing application networks in Azure requires a thoughtful, well-planned approach. In our detailed walkthrough, we explored how segregating services into dedicated subnets, correctly implementing subnet delegation, and isolating private endpoints into their own subnet all contribute to a robust security posture. Azure Private Link not only bridges services securely but also ensures that private connectivity is maintained across your infrastructure.
As you move forward with your own deployments:
  • Review your subnet architecture to ensure resources are appropriately isolated.
  • Use dedicated subnets for private endpoints to avoid conflicts with delegated subnets.
  • Employ testing practices vigorously to validate that your internal communications remain secure.
The path to improved network security is built on a foundation of clear planning, effective segregation, and an understanding of the intricacies of Azure’s networking capabilities. By following these steps, you’ll not only secure your applications but also set a robust framework for future growth and compliance.
Stay curious, keep experimenting, and let your network architecture be as secure as it is scalable—and never forget that sometimes the devil (or the hacker) is in the details.

Source: Medium
 

Back
Top