Notes in Azure Storage Network Restriction

By default,  when creating a new Azure storage account it accepts connections from clients on any network. To limit that, Azure allows you to add a trusted list of virtual network subnets or IP ranges.

This article is not going to walk you through step-by-step guidance on how to add firewall rules to the Azure Storage account. Instead, it will mainly focus on deploy network restriction programmatically in a DevOps environment.

Scenario

Not only a security best practice, but you may also see compliance requirements in which you are asked to control connection to your storage account in the cloud environment. One of the most common scenarios is restricting network connection from developer workstation or building environment to storage accounts.

In the above illustration, you can see that network traffic from the virtual machine in the virtual network (10.0.0.0/16) is accepted. The network connection from the virtual machine from the Internet is denied.

Even with a file in a public container if your network isn’t allowed you will see an authorization failure error message.

Notes: the Azure Storage firewall would only help to control access to your storage account from a client (Azure Portal, PowerShell, CLI, SDK..) that sends requests to Storage Account-specific endpoint (blob.core.windows.net). It has no effect on requests to storage management endpoint such as List Key, List Container

ARM Template Deployment

In Azure ARM template, you can add a list of virtual network subnets you allow in virtualNetworkRules element in networkAcls property. Below is the sample:

...      
"networkAcls": {
    "virtualNetworkRules": [
        {
            "action": "Allow",
            "id": "/subscriptions/67d6179d-a99d-4ccd-8c56-4d3ff2e13349/resourceGroups/RG1/providers/Microsoft.Network/virtualNetworks/azsec01-vnet/subnets/azsec01-vm1-vm-subnet"
        }
    ],
    "defaultAction": "Deny"
}
...

Or with more than one subnet:

...      
"networkAcls": {
    "virtualNetworkRules": [
        {
            "action": "Allow",
            "id": "/subscriptions/67d6179d-a99d-4ccd-8c56-4d3ff2e13349/resourceGroups/RG1/providers/Microsoft.Network/virtualNetworks/azsec01-vnet/subnets/azsec01-vm1-vm-subnet"
        },
        {
            "action": "Allow",
            "id": "/subscriptions/67d6179d-a99d-4ccd-8c56-4d3ff2e13349/resourceGroups/RG1/providers/Microsoft.Network/virtualNetworks/azsec02-vnet/subnets/azsec01-vm1-vm-subnet"
        }
    ],
    "defaultAction": "Deny"
}
...

Advanced ARM Template Deployment

You may want to supply a list of virtual network subnets in a parameter and pass it to virtualNetworkRules. Let’s say you have the following parameter:

...
"trustedSubnets": {
    "value": [
        "/subscriptions/67d6179d-a99d-4ccd-8c56-4d3ff2e13349/resourceGroups/RG1/providers/Microsoft.Network/virtualNetworks/azsec01-vnet/subnets/azsec01-vm1-vm-subnet",
        "/subscriptions/67d6179d-a99d-4ccd-8c56-4d3ff2e13349/resourceGroups/RG1/providers/Microsoft.Network/virtualNetworks/azsec01-vnet/subnets/azsec2-vm1-vm-subnet"
    ]
}
...

In this case, you can use copy function in the Azure ARM template:

...
"variables": {
    "copy": [
        {
            "name": "virtualNetworkRules",
            "count": "[length(parameters('trustedSubnets'))]",
            "input": {
                "action": "Allow",
                "id": "[parameters('trustedSubnets')[copyIndex('virtualNetworkRules')]]"
            }
        }
    ]
}
...

and then pass it to storage account deployment:

...
"properties": {
    "networkAcls": {
        "virtualNetworkRules": "[variables('virtualNetworkRules')]",
        "defaultAction": "Deny"
    }
}
...

If you want to add ipRules you can still use the same approach:

...
"variables": {
    "copy": [
        {
            "name": "ipRules",
            "count": "[length(parameters('trustedIps'))]",
            "input": {
                "action": "Allow",
                "value": "[parameters('trustedIps')[copyIndex('ipRules')]]"
            }
        }
    ]
}
...
...
"properties": {
    "networkAcls": {
        "virtualNetworkRules": "[variables('virtualNetworkRules')]",
        "ipRules": "[variables('ipRules')]",
        "defaultAction": "Deny"
    }
}
...

Note that you need to set the defaultAction value as Deny to allow your storage account to only accept selected subnets and IP addresses/ranges.

DevSecOps Conceptual Idea

So now you know how to add trusted networks to restrict access to your storage account. The next question is how to coordinate with InfoSec or Network team. You surely don’t want to modify the list every time you want to add a new subnet or IP address to support.

There are some approaches:

  1. Create a trusted list of subnets in the format of Azure ARM template and use it as a nested template.
  2. Create a text file which contains trusted subnets and have a job in your CICD pipeline to retrieve the list and programmatically override the template parameter.

For the #1 approach you would just only need to create an ARM template. Below is the sample one:

{
    "$schema": "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#",
    "contentVersion": "1.0.0.0",
    "parameters": {},
    "variables": {
        "storageAccountTrustedSubnets": [
            "subnetResourceId1",
            "subnetResourceID2"
        ],
        "storageAccountTrustedIpRange": [
            "ipRange1",
            "ipRange2"
        ]
    }
}

With approach 2 it is more complicated because you’d need to write a script to retrieve latest list of trusted network and override/populate to Azure ARM Template.

 

Troubleshooting

You may encounter “The response for resource had empty or invalid content.” error message during your ARM template deployment when trying to create a storage account. There are two possible reasons:

  • Your specified subnet doesn’t exist.
  • Your specified subnet doesn’t have Service Endpoint for Microsoft.Storage enabled yet.

In that case, check your subnet again. If you deploy multiple resources (for testing purpose), here is where it needs to be done:

...
{
    "type": "subnets",
    "apiVersion": "2020-05-01",
    "location": "[parameters('location')]",
    "name": "[variables('subnetName')]",
    "dependsOn": ["[variables('vnetName')]"],
    "properties": {
      "addressPrefix": "[variables('subnetPrefix')]",
      "serviceEndpoints": [
          {
              "service": "Microsoft.Storage",
              "locations": "[parameters('location')]"
          }
      ]
    }
  }
...

Azure Policy Built-in Policy

Azure Policy has a built-in named “Storage accounts should restrict network access using virtual network rules” which audits storage account’s network rules

...
"policyRule": {
    "if": {
        "allOf": [
            {
                "field": "type",
                "equals": "Microsoft.Storage/storageAccounts"
            },
            {
                "anyOf": [
                    {
                        "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction",
                        "notEquals": "Deny"
                    },
                    {
                        "count": {
                            "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]"
                        },
                        "greaterOrEquals": 1
                    }
                ]
            }
        ]
...

Look at the policy it looks like that it would only recommend adding trusted subnets to your storage account. If you have any IP addresses/ranges added the storage would still be marked non-compliant.

There is another one named “Storage accounts should restrict network access” that only reads networkAcls.defaultAction.

Reference

There are some good references:

This entry was posted in Security Automation and tagged , . Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *