Skip to content
Back to Blog
1 min read

Application Security Groups for Simplified Network Security

Why Application Security Groups?

Traditional NSG rules use IP addresses, which creates challenges:

  • IP addresses change when VMs are recreated
  • Rules become hard to read with many IP ranges
  • Scaling requires constant rule updates

ASGs solve these problems by:

  • Grouping VMs logically by application role
  • Rules remain valid even when IPs change
  • New VMs automatically inherit rules when added to ASG

Creating Application Security Groups

Set up ASGs for a multi-tier application:

# Create ASGs for each tier
az network asg create \
    --resource-group rg-networking \
    --name asg-web-servers

az network asg create \
    --resource-group rg-networking \
    --name asg-api-servers

az network asg create \
    --resource-group rg-networking \
    --name asg-database-servers

az network asg create \
    --resource-group rg-networking \
    --name asg-management

Terraform Configuration

Complete ASG implementation with Terraform:

# Application Security Groups
resource "azurerm_application_security_group" "web" {
  name                = "asg-web-servers"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  tags = {
    Tier        = "Web"
    Application = "Frontend"
  }
}

resource "azurerm_application_security_group" "api" {
  name                = "asg-api-servers"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  tags = {
    Tier        = "API"
    Application = "Backend"
  }
}

resource "azurerm_application_security_group" "database" {
  name                = "asg-database-servers"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  tags = {
    Tier        = "Database"
    Application = "Data"
  }
}

resource "azurerm_application_security_group" "management" {
  name                = "asg-management"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  tags = {
    Tier        = "Management"
    Application = "Operations"
  }
}

resource "azurerm_application_security_group" "monitoring" {
  name                = "asg-monitoring"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  tags = {
    Tier        = "Monitoring"
    Application = "Operations"
  }
}

# NSG with ASG-based rules
resource "azurerm_network_security_group" "application" {
  name                = "nsg-application"
  resource_group_name = azurerm_resource_group.networking.name
  location            = azurerm_resource_group.networking.location

  # Web tier rules
  security_rule {
    name                                       = "allow-https-to-web"
    priority                                   = 100
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "443"
    source_address_prefix                      = "Internet"
    destination_application_security_group_ids = [azurerm_application_security_group.web.id]
  }

  security_rule {
    name                                       = "allow-http-to-web"
    priority                                   = 110
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "80"
    source_address_prefix                      = "Internet"
    destination_application_security_group_ids = [azurerm_application_security_group.web.id]
  }

  # Web to API communication
  security_rule {
    name                                       = "allow-web-to-api"
    priority                                   = 200
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "8080"
    source_application_security_group_ids      = [azurerm_application_security_group.web.id]
    destination_application_security_group_ids = [azurerm_application_security_group.api.id]
  }

  # API to Database communication
  security_rule {
    name                                       = "allow-api-to-database"
    priority                                   = 300
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "1433"
    source_application_security_group_ids      = [azurerm_application_security_group.api.id]
    destination_application_security_group_ids = [azurerm_application_security_group.database.id]
  }

  # Management access
  security_rule {
    name                                       = "allow-ssh-from-management"
    priority                                   = 400
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "22"
    source_application_security_group_ids      = [azurerm_application_security_group.management.id]
    destination_application_security_group_ids = [
      azurerm_application_security_group.web.id,
      azurerm_application_security_group.api.id
    ]
  }

  security_rule {
    name                                       = "allow-rdp-from-management"
    priority                                   = 410
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_range                     = "3389"
    source_application_security_group_ids      = [azurerm_application_security_group.management.id]
    destination_application_security_group_ids = [azurerm_application_security_group.database.id]
  }

  # Monitoring access
  security_rule {
    name                                       = "allow-monitoring-agents"
    priority                                   = 500
    direction                                  = "Inbound"
    access                                     = "Allow"
    protocol                                   = "Tcp"
    source_port_range                          = "*"
    destination_port_ranges                    = ["9090", "9100"]  # Prometheus ports
    source_application_security_group_ids      = [azurerm_application_security_group.monitoring.id]
    destination_application_security_group_ids = [
      azurerm_application_security_group.web.id,
      azurerm_application_security_group.api.id,
      azurerm_application_security_group.database.id
    ]
  }

  # Deny all other inbound
  security_rule {
    name                       = "deny-all-inbound"
    priority                   = 4096
    direction                  = "Inbound"
    access                     = "Deny"
    protocol                   = "*"
    source_port_range          = "*"
    destination_port_range     = "*"
    source_address_prefix      = "*"
    destination_address_prefix = "*"
  }

  tags = {
    Environment = "Production"
  }
}

Associating NICs with ASGs

Add VM network interfaces to ASGs:

# Web server NIC with ASG association
resource "azurerm_network_interface" "web" {
  count               = 3
  name                = "nic-web-${count.index}"
  resource_group_name = azurerm_resource_group.compute.name
  location            = azurerm_resource_group.compute.location

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.web.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_network_interface_application_security_group_association" "web" {
  count                         = 3
  network_interface_id          = azurerm_network_interface.web[count.index].id
  application_security_group_id = azurerm_application_security_group.web.id
}

# API server NIC with ASG association
resource "azurerm_network_interface" "api" {
  count               = 2
  name                = "nic-api-${count.index}"
  resource_group_name = azurerm_resource_group.compute.name
  location            = azurerm_resource_group.compute.location

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.api.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_network_interface_application_security_group_association" "api" {
  count                         = 2
  network_interface_id          = azurerm_network_interface.api[count.index].id
  application_security_group_id = azurerm_application_security_group.api.id
}

# Database server NIC with ASG association
resource "azurerm_network_interface" "database" {
  name                = "nic-database-primary"
  resource_group_name = azurerm_resource_group.compute.name
  location            = azurerm_resource_group.compute.location

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.data.id
    private_ip_address_allocation = "Dynamic"
  }
}

resource "azurerm_network_interface_application_security_group_association" "database" {
  network_interface_id          = azurerm_network_interface.database.id
  application_security_group_id = azurerm_application_security_group.database.id
}

Managing ASG Memberships Programmatically

Add and remove VMs from ASGs dynamically:

from azure.mgmt.network import NetworkManagementClient
from azure.identity import DefaultAzureCredential

credential = DefaultAzureCredential()
network_client = NetworkManagementClient(credential, subscription_id)

def add_nic_to_asg(nic_resource_group, nic_name, asg_id):
    """Add a network interface to an Application Security Group."""

    nic = network_client.network_interfaces.get(nic_resource_group, nic_name)

    # Get current ASG associations
    current_asgs = nic.ip_configurations[0].application_security_groups or []
    current_asg_ids = [asg.id for asg in current_asgs]

    # Add new ASG if not already present
    if asg_id not in current_asg_ids:
        current_asgs.append({"id": asg_id})
        nic.ip_configurations[0].application_security_groups = current_asgs

        result = network_client.network_interfaces.begin_create_or_update(
            nic_resource_group,
            nic_name,
            nic
        ).result()

        print(f"Added {nic_name} to ASG")
        return result

    print(f"{nic_name} already in ASG")
    return nic

def remove_nic_from_asg(nic_resource_group, nic_name, asg_id):
    """Remove a network interface from an Application Security Group."""

    nic = network_client.network_interfaces.get(nic_resource_group, nic_name)

    current_asgs = nic.ip_configurations[0].application_security_groups or []
    new_asgs = [asg for asg in current_asgs if asg.id != asg_id]

    if len(new_asgs) < len(current_asgs):
        nic.ip_configurations[0].application_security_groups = new_asgs

        result = network_client.network_interfaces.begin_create_or_update(
            nic_resource_group,
            nic_name,
            nic
        ).result()

        print(f"Removed {nic_name} from ASG")
        return result

    print(f"{nic_name} was not in ASG")
    return nic

def list_asg_members(asg_resource_group, asg_name):
    """List all NICs that are members of an ASG."""

    asg = network_client.application_security_groups.get(asg_resource_group, asg_name)
    asg_id = asg.id

    members = []

    # List all NICs and check membership
    nics = network_client.network_interfaces.list_all()
    for nic in nics:
        if nic.ip_configurations:
            asgs = nic.ip_configurations[0].application_security_groups or []
            for asg in asgs:
                if asg.id == asg_id:
                    members.append({
                        "nic_name": nic.name,
                        "resource_group": nic.id.split("/")[4],
                        "private_ip": nic.ip_configurations[0].private_ip_address
                    })

    return members

# Example usage
asg_id = f"/subscriptions/{subscription_id}/resourceGroups/rg-networking/providers/Microsoft.Network/applicationSecurityGroups/asg-web-servers"

# Add new web server to ASG
add_nic_to_asg("rg-compute", "nic-web-new", asg_id)

# List all web servers
members = list_asg_members("rg-networking", "asg-web-servers")
for member in members:
    print(f"  {member['nic_name']}: {member['private_ip']}")

Multiple ASG Memberships

VMs can belong to multiple ASGs:

# Server that needs both API and monitoring access
resource "azurerm_network_interface" "api_with_monitoring" {
  name                = "nic-api-monitored"
  resource_group_name = azurerm_resource_group.compute.name
  location            = azurerm_resource_group.compute.location

  ip_configuration {
    name                          = "internal"
    subnet_id                     = azurerm_subnet.api.id
    private_ip_address_allocation = "Dynamic"
  }
}

# Associate with API ASG
resource "azurerm_network_interface_application_security_group_association" "api_role" {
  network_interface_id          = azurerm_network_interface.api_with_monitoring.id
  application_security_group_id = azurerm_application_security_group.api.id
}

# Also associate with monitoring agent ASG
resource "azurerm_network_interface_application_security_group_association" "monitoring_role" {
  network_interface_id          = azurerm_network_interface.api_with_monitoring.id
  application_security_group_id = azurerm_application_security_group.monitoring.id
}

ASG Best Practices

# Best practices for ASG design
best_practices = {
    "naming_conventions": [
        "Use consistent naming: asg-{role}-{environment}",
        "Include application/service name in tags",
        "Document purpose in description"
    ],
    "design_principles": [
        "Create ASGs based on application roles, not subnets",
        "Keep ASGs within same region as VMs",
        "Use ASGs instead of IP addresses whenever possible"
    ],
    "security": [
        "Combine ASGs with service tags for comprehensive rules",
        "Use multiple ASGs for VMs with multiple roles",
        "Review ASG membership regularly"
    ],
    "limitations": [
        "Max 3,000 ASGs per subscription per region",
        "ASG and resources must be in same region",
        "Cannot use ASGs across different VNets in NSG rules"
    ]
}

for category, items in best_practices.items():
    print(f"\n{category.upper().replace('_', ' ')}:")
    for item in items:
        print(f"  - {item}")

Conclusion

Application Security Groups transform network security management from IP-centric to application-centric. By grouping VMs logically based on their workload, you create more maintainable, readable security rules that automatically apply to new resources.

The combination of ASGs with NSGs provides a powerful framework for implementing zero-trust networking and microsegmentation in Azure. As your infrastructure scales, ASGs ensure security policies remain consistent without constant manual updates.

Michael John Peña

Michael John Peña

Data & AI Director and Microsoft MVP based in Sydney. Writing about Microsoft Fabric, Azure, data engineering, and AI.