Assume breach and minimize the impact with network security groups

8 minute read


In this blog post, I’ll explain how to restrict subnet traffic flow with network security groups and create fine-grained rules with service tags. Why it’s important to isolate workloads and resources inside your network to minimize the damage of a potential breach. As always, I’ll use the SmartMoney application as an example.


Assuming breach is one of the principles of Zero Trust. It basically means that we should assume that an attacker is in the network and minimize the damage of what an attacker can do. Every time you design a network, you should assume that a breach will happen at some point and that you try to minimize the impact of that breach. A best practice is to isolate workloads as much as possible. The Hub Spoke network topology allows us to do that by creating a spoke network for each workload. On top of that, we can segment the network into subnets and use network security groups to control the traffic. In this article, I will explain how network security groups work and how you can use them to follow the Zero Trust principles.

In March 2017, personally identifying data of hundreds of millions of people was stolen from Equifax

Equifax, a credit reporting agencies that assess nearly all American citizens, was hacked due to widely known vulnerability in a web portal. After the hackers where inside the network they were able to move freely through the network because no network segmentation was implemented. The hackers were able to stole millions of personal information on a time span of months. After the data breach, Equifax settled with the Federal Trade Commision for $425 million dollars to compensate the victims.

  • 143 million people were affected.
  • 76 days the hackers were active in the Equifax network without being discovered.
  • $1.4 billion was invested by Equifax to clean up and improve the infrastructure, application and security.
  • $425 million is the amount Equifax settled with the Federal Trade Commission to help people who were affected.
source: csoonline.com

The SmartMoney workload is deployed in a separate spoke network. Three subnets are created: snet-frontend for the frontend app service, snet-backend for the backend app service and snet-private-endpoints for the SQL and storage account private endpoints. Below the architecture diagram.

The following diagram illustrates the traffic flow between the frontend, backend application, SQL database and storage account.

The frontend app sends API calls to the backend app. The backend app retrieves and stores data in the SQL database and storage account. The SQL database and storage account are protected with private endpoints, as I explained in a previous article.

Map the transaction flow

In the second step of the five steps methodology, we should map the transaction flow. This means that we need to define the traffic between the different components in our network. Typical questions to answer are: what are the inbound and outbound connections and what port and protocol is used? Below a simple transaction map for the SmartMoney workload:

Source Destination Description Port
Frontend application Backend application API calls 443
Backend application SQL SQL queries 1433
Backend application Storage account Retrieve and store blobs 443

Now that we have the transaction flow mapped, let’s see how we can use network security groups to control the traffic flow.

What are network security groups?

Network security groups are a set of rules that allow or deny traffic to a subnet or a network interface. Rules are executed in a specific order to allow or deny traffic and the first rule that matches the traffic is applied. All other rules are ignored. Network security groups allows us to follow the principle of least privileged access because we explicitly allow the traffic that is needed. By default, all traffic is blocked and only the traffic that is defined in the transaction map is allowed. A network security group comes with a default set of inbound and outbound rules.

Default inbound rules

Order Name Description
65000 AllowVnetInBound Allow all inbound traffic from the virtual network
65001 AllowAzureLoadBalancerInBound Allow the reserved IP address (168.63.129.16) of the Azure load balancer. Used for health probes.
65500 DenyAllInBound Deny all inbound traffic

Default outbound rules

Order Name Description
65000 AllowVnetOutBound Allow all outbound traffic inside the virtual network
65001 AllowInternetOutBound Allow outbound traffic to the internet
65500 DenyAllOutBound Deny all inbound traffic

These rules already gives a good starting point. However, when following Zero Trust principles we want to ensure the least privileged access. This means that we don’t want to allow all traffic (AllowVnetInBound) from the virtual network. We want to explicitly allow the traffic that is needed as defined in the transaction map. For that reason, I added a deny all traffic rule. Let’s take a look at a network security group that is created for the snet-backend subnet. By default, all inbound traffic is blocked and only traffic from the snet-frontend subnet is allowed on port 433.

resource "azurerm_network_security_group" "snet_backend_nsg" {
  name                = "nsg-backend"
  location            = azurerm_resource_group.rg_smartmoney.location
  resource_group_name = azurerm_resource_group.rg_smartmoney.name

  security_rule {
    name                       = "allow-frontend-433"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "433"
    source_address_prefixes    = azurerm_subnet.subnet_frontend.address_prefixes
    destination_address_prefix = "VirtualNetwork"
  }

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

The inbound-deny-all rule ensures that all inbound traffic is blocked. Because of the priority (1000) it takes precedence over the default inbound rules. The allow-frontend-433 rule allows traffic from the snet-frontend subnet on port 433. The source_address_prefixes field holds the IP address range of the snet-frontend subnet. A service tag is used in the destination_address_prefix field to only allow traffic to the virtual network’s address space.

What are service tags?

Service tags are predefined groups that represent IP address ranges of specific Azure services/ regions. This allows us to keep our network security groups clean and eliminate the need of manually entering IP addresses. This means that every time Azure updates an IP address range you don’t need to update rules in the network security group. An example, is the AppService.WestEurope service tag which is available for outbound rules. Let’s say that you’ve a virtual machine within a virtual network. The virtual machine needs to access an app service in the West Europe region. You can use the AppService.WestEurope service tag to only allow traffic to app services in the West Europe region. In my example, I enabled private endpoint for the app service which mean it’s accessible over a private IP address. For SmartMoney, I don’t need the AppService service tags because private endpoint is enabled for the app service so the traffic is routed over the virtual network. In the network security group rules I can use the virtual network’s address space or the VirtualNetwork service tag to control the traffic. The VirtualNetwork service tag represents the entire address space of the virtual network that the network security group is connected to.

Below the network security group for the private endpoint subnet. This allows traffic from the backend subnet to the SQL database and storage account on the respective ports. All other traffic is blocked.

resource "azurerm_network_security_group" "private_endpoint_nsg" {
  name                = "nsg-private-endpoint"
  location            = azurerm_resource_group.rg_smartmoney.location
  resource_group_name = azurerm_resource_group.rg_smartmoney.name

  security_rule {
    name                       = "allow-backend-1433"
    priority                   = 100
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "1433"
    source_address_prefixes    = azurerm_subnet.subnet_backend.address_prefixes
    destination_address_prefix = "VirtualNetwork"
  }

  security_rule {
    name                       = "allow-backend-433"
    priority                   = 200
    direction                  = "Inbound"
    access                     = "Allow"
    protocol                   = "Tcp"
    source_port_range          = "*"
    destination_port_range     = "433"
    source_address_prefixes    = azurerm_subnet.subnet_backend.address_prefixes
    destination_address_prefix = "VirtualNetwork"
  }

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

Testing network security groups

Implementing network security group is a best practice to restrict the traffic flow however, it can be a bit tricky to test if the rules are working as expected. Unfortunately, there isn’t a good way of testing network security groups without deploying virtual machines.

Test with IP flow verify

The IP flow verify allows us to test the network connectivity for a specific virtual machine. It simulates traffic based on configurations and reports back if traffic is allowed or denied by NSG rules. For this test, I deployed a Linux virtual machine (vm-vm2-test) in the snet-backend subnet. For my test, I selected the virtual machine as the target resource and the private endpoint for the frontend app service (deployed in snet-frontend subnet) as the source (remote IP address). In the screenshot below you see that traffic is blocked for port 80 which is expected because we only allow traffic on port 433.

Test network security groups with NSG diagnostics

For diagnosing a network security group the NSG diagnostics tool gives a better overview. It reports back a more detailed overview of all the NSG rules. I executed a test with the same configurations as the IP flow verify test. In the screenshot below, you can see that one rule (inbound-deny-all) is applied and that the traffic is denied.

And a test over port 433 which is allowed.

As always the source code can be found on GitHub.

Zero Trust principles

Verify explicitly

With network security groups we can explicitly define and verify which traffic is allowed or denied based on rules. Access is only permitted when there is a rule that allows the traffic.

Least privileged access

The least privileged access principle tells us that we should grant the minimum level of access or permissions to perform a specific task. In case of network traffic that means that we should only allow the traffic that is needed. For SmartMoney, that means we only allow traffic from the frontend subnet (snet-frontend) to the backend subnet (snet-backend) on port 433. All other traffic is blocked.

Assume breach

If network security groups are implemented correctly we prevent that an attacker can move freely through the network.

What is next?

In the next article, I’ll continue with protecting our application and infrastructure with Entra ID conditional access.

Updated: