Posted in:

Suppose you have an application that consists of a "front-end" website, and a back-end web API. You want the end users of your application to be able to access the front-end website, but you only want the front end to be accessible to your end users. The back-end should be locked down so it is only callable from the front-end.

Front-end

This is a fairly standard architecture, and is quite easy to achieve in Azure with traditional VMs in a Virtual Network, or with AKS. You simply only expose public endpoints for the front-end services you want to make available.

Unfortunately, if you're using App Service, with your front-end and back-end services hosted as Web Apps, there hasn't been an easy way to do this until recently. That's because there is no way for a Web App hosted on App Service to join a Virtual Network, unless you choose the "ASE" (App Service Environment) pricing tier, which is prohibitively expensive for many scenarios.

Restricting access to Web Apps

Now of course, you can (and should) protect your back-end API by requiring all callers to provide credentials, and encrypt the traffic with TLS. But we'd like to go further than that. Ideally, even if credentials to call the back-end API were leaked, they still shouldn't be usable by any attacker on the internet - we'd like to accept only traffic originating from trusted locations.

App Service does help us out a bit here. As well as securing our endpoints in the usual way, we can add Access Restrictions to our Web Apps. This way we can whitelist the IP addresses that are allowed to make incoming requests.

This is great for your front-end web app if you happen to know the exact IP addresses that your customer will use. That way you can prevent all and sundry from visiting your site and trying to brute force the login screen.

But if the back-end API is intended to to be called only by the front-end web app, then we have to whitelist all possible outbound IP addresses for the App Service plan that the front-end web app is hosted on. And those IP addresses are not exclusive to our front-end web app. It would be possible for other apps running on App Service in the same data center to attempt to access our back-end API.

Service Endpoints and new VNet integration to the rescue!

The good news is that by using a mixture of "Service Endpoints" alongside the new Virtual Network integration feature for App Service, you can completely lock down access to your back-end APIs, and only allow incoming traffic from the web apps that should be calling them.

Essentially we can force all outbound requests to the back-end service from the front-end service to flow through a Virtual Network. And then we can configure the back-end service to only accept traffic from that Virtual Network.

image

And how do we make the outbound requests go through the Virtual Network, and not take some other route? That's what the "Service Endpoints" do. Essentially they set up a custom route that all traffic for a particular Azure service (in our case App Service), will follow.

Demo overview

In the rest of this post, I'll show the steps to set this all up. Here's a quick overview of the key steps:

  1. Create a Virtual Network
  2. Create a delegated subnet, and enable a service endpoint for App Service
  3. Create an App Service Plan and front-end and back-end web apps
  4. Add an access restriction for the back-end web app to only allow traffic from the subnet
  5. Connect the front-end web app to the vnet
  6. (optional) connect the back-end web app to the vnet

Step 1 - create the virtual network

First we need to create a Virtual Network. I'll create a resource group to put everything in:

$resourceGroup = "VNetTest"
$location = "westeurope"
az group create -n $resourceGroup -l $location

And create a new VNet:

$vnetName = "vnettest"
$vnetAddressPrefix = "10.2.0.0/16"
az network vnet create -n $vnetName `
    -g $resourceGroup `
    --address-prefix $vnetAddressPrefix

Step 2 - Create a delegated subnet

We need to create a delegated subnet in our VNet that all traffic from our front-end web app will travel through. The key things here are to add a delegation of Microsoft.Web/serverFarms and enable the service endpoint of Microsoft.Web. A prefix of /27 is allows sufficient addresses for this subnet.

$subnetName = "delegatingsubnet"
az network vnet subnet create `
    -g $resourceGroup `
    --vnet-name $vnetName `
    -n $subnetName `
    --delegations "Microsoft.Web/serverFarms" `
    --address-prefixes "10.2.1.0/27" `
    --service-endpoints "Microsoft.Web"

Step 3 - Create the app service plan and web-apps

Now we'll create an App Service plan (which needs to be a "standard" plan or above) and two web apps - one for the front-end and one for the back-end.

$appServicePlanName = "TestAppServicePlan"
az appservice plan create -n $appServicePlanName `
        -g $resourceGroup --sku S1 

$frontendAppName = "frontend-01a"
az webapp create -n $frontendAppName -g $resourceGroup -p $appServicePlanName
$backendAppName = "backend-01a"
az webapp create -n $backendAppName -g $resourceGroup -p $appServicePlanName

I also uploaded a couple of simple test apps to the web apps, so that I could test connectivity from the front-end to the back-end.

Step 4 - Lock down the back-end

Now, let's block any incoming traffic to the back-end web app that doesn't come from the VNet.

It's a little bit fiddly to automate with the Azure CLI from PowerShell as getting your JSON correctly escaped can be tricky, but essentially we're just adding a new "IP security restriction" that points at the resource ID of our delegated subnet.

$subscriptionId = az account show --query id -o tsv
$subnetId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.Network/virtualNetworks/$vnetName/subnets/$subnetName"
$restrictions =  "{ \""ipSecurityRestrictions\"": [ { \""action\"": \""Allow\"", \""vnetSubnetResourceId\"": " +
                    "\""$subnetId\"", \""name\"": \""LockdownToVNet\"", \""priority\"": 100, \""tag\"": \""Default\"" } ] }"

az webapp config set -g $resourceGroup -n $backendAppName --generic-configurations $restrictions

If all works correctly, it will be impossible to make a call to the back-end web app from the public internet, and the front-end web app will also be denied access.

Step 6 - Connect front-end to the VNET

Now we need to connect the front-end web app to the VNet. This is so that any outgoing traffic from the front-end web app will get routed through the delegated subnet and therefore be allowed to access the back-end.

This is unfortunately not a feature that is supported by the Azure CLI, and I found the documentation on how to call the REST API directly very difficult to follow. In the end I used the F12 tools in my browser to see what the Azure Portal does when you join a web app to the VNet. This revealed the endpoint I needed to call (/config/virtualNetwork) and the format of the payload (including the swiftSupported flag).

I then created the following PowerShell function to connect a web app to a specific subnet in a VNet:

function Join-Vnet ($resourceGroup, $webAppName, $vnetName, $subnetName)
{
    $subscriptionId = az account show --query id -o tsv
    $location = az group show -n $resourceGroup --query location -o tsv
    $subnetId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.Network/virtualNetworks/$vnetName/subnets/$subnetName"

    $resourceId = "/subscriptions/$subscriptionId/resourceGroups/$resourceGroup/providers/Microsoft.Web/sites/$webAppName/config/virtualNetwork"
    $url = "https://management.azure.com$resourceId" + "?api-version=2018-02-01"

    $payload = @{ id=$resourceId; location=$location;  properties=@{subnetResourceId=$subnetId; swiftSupported="true"} } | ConvertTo-Json
    $accessToken = az account get-access-token --query accessToken -o tsv
    $response = Invoke-RestMethod -Method Put -Uri $url -Headers @{ Authorization="Bearer $accessToken"; "Content-Type"="application/json" } -Body $payload
}

And we can use it to join the front-end app to the VNet:

Join-Vnet $resourceGroup $frontendAppName $vnetName $subnetName

With that done, our front-end should be able to communicate again with the back-end. I've noticed that the change isn't always immediate - sometimes it can take a minute or two before you are able to access.

Step 7 - (Optional) Connect Back-end to VNet

If you have multiple back-end services, they also may need to communicate with each other. That can be achieved by connecting the back end web-app to the VNet as well. We can reuse the function we just created:

Join-Vnet $resourceGroup $backendAppName $vnetName $subnetName

Summary

In this post we saw how to secure the back-end tier of a multi-tier web app, by making use of some new App Service features, and without having to use the expensive App Service Environment features. In fact, the costs for setting this up are minimal, and represents true defence in-depth for your back-end services.

Obviously, we've not talked about restricting access to the front-end. You might also want to lock that down to a VNet as well depending on your use case. That can be achieved by creating an Application Gateway and only allow incoming traffic to the front-end to come from that. I've also managed to set that up for an App Service, but I'll save that for another post, because it was quite a complex process.

Want to learn more about the Azure CLI? Be sure to check out my Pluralsight course Azure CLI: Getting Started.

Comments

Comment by Ruben

I followed this approach and executed all steps via the Azure portal. The necessary configuration looks OK, however, REST calls from the frontend app service to the backend app service still give a "Error 403 - This web app is stopped" response. Any idea what's going on?

Ruben
Comment by Mark Heath

hard to say without seeing in detail what you did, but how long did you give it to work? I found that I sometimes got a window of 403s for 5-10 minutes after setting it up. Remember front end service needs to be connected to the web app with App Service endpoints set up

Mark Heath
Comment by Ruben

Hi Mark, thank you for your quick reply! I've given it an hour, but still those 403's keep coming. If I remove the access restrictions on the backend app, it all works again.
Here are some screenshots of how things are configured via Azure portal:
1) VNET with subnet:
https://uploads.disquscdn.c...
2) VNET service endpoint:
https://uploads.disquscdn.c...
3) Backend API based app service restrictions:
https://uploads.disquscdn.c...
4) Frontend Angular app vnet integration (via "add vnet (preview)" button)
https://uploads.disquscdn.c...

Ruben
Comment by Ruben

If I add my public IP address as allowed on the backend API app service, everything works. So I'm guessing an Angular app doing REST calls as frontend app service doesn't fit in your scenario? It looks like those REST calls are not flowing through the vnet :/ If you say "frontend app", do you mean like a MVC app that does server side calls to the backend app?

Ruben
Comment by Mark Heath

No this setup only allows server to server calls from front end to backend. For example if you are using the backend for frontend (bff) pattern

Mark Heath
Comment by Ronald Noronha

Hi Mark,
I have multiple ASP (App servce Plans) with one WebApp in each. I want the WebApps to call each other via the VNET. I have configured as per your article (multiple subnets in the VNET as i have multiple ASPs) but when i try to access webapp2 from webapp1 (tried with KUDU console and curl command) but i get 403 error. Does your configuration work for this scenario?

Ronald Noronha
Comment by Mark Heath

hi Ronald, I have managed to get a version of this working with two app service plans. Although each app service plan can only connect out to a single VNET, you can set up multiple incoming vnet restrictions

Mark Heath
Comment by Mark Heath

Also, maybe the kudu console is not the best way to test this, as the networking for kudu is separate I think

Mark Heath
Comment by Ganesh A

Hi there,
i followed steps but still, front-end webapp cant able to communicate the backend webap
in our case front--webapp customer facing portal and backend--webapp process api call management .

Ganesh A
Comment by Mark Heath

I'm afraid i don't know why that would be - I've been able to get this running. First step would be to remove the access restriction for the backend and check that you can access it without the rule in place

Mark Heath
Comment by Ganesh A

Front-end web-app able communicate with back-end with-out any rules. when add rule as mentioned. it throws 403 error.

Ganesh A
Comment by Mark Heath

I guess traffic must not be flowing correctly through the delegated subnet then. Check that those settings have been configured correctly.

Mark Heath
Comment by Tibo B

Hi Mark, thanks for that useful post!
For information it is now possible to link a webapp to a subnet using the following command line:
az webapp vnet-integration add `
--name $callerAppName `
--resource-group $callerAppResourceGroup `
--vnet $vnetName `
--subnet $subnetName `
--slot $slotName

And for those who wants to apply that process to Azure Functions, you can manage to do it using az functionapp instead of az webapp commands

Tibo B
Comment by Mark Heath

that's great, thanks for letting me know. I'll hopefully get a chance to try this out soon and update the post.

Mark Heath
Comment by Tibo B

I was reading that it should be also possible to connect in the same way two WebApps from a different VNet using Peering (VNets are on the same Azure region). But I can't make it work. Have you ever tried do to that?
edit: I also tried to communicate between two subnets (as my services are on different Service Plan) without success

Tibo B
Comment by Mark Heath

Afraid that's not something I've tried yet

Mark Heath
Comment by Seth

Great article, thanks. What do you use in the front-end in order to access the back-end? Do you use a specific internal IP address from the service enabled subnet or use the public domain name still and that somehow gets redirected to come over the private ip address instead of the public ip?

Seth
Comment by Mark Heath

Yes, you just use the public domain name pointing at the public IP address. It's where the incoming traffic is coming from that determines whether the request is accepted or not.

Mark Heath
Comment by Pow Wow Artist Mgmt

Thanks for the wonderful article. Could you also please describe, how to test connectivity between Front end and back end web app? I guess ping is not allowed. How can I confirm that the Front end is able to talk to the back end?

Pow Wow Artist Mgmt
Comment by Mark Heath

I just made a very simple test website that made an ongoing call through to the back end

Mark Heath
Comment by Pow Wow Artist Mgmt

Hi Mark, could you explain a little bit more on how to test the connectivity between frontend and backend? I am not an expert in web apps, I would appreciate your help.

Pow Wow Artist Mgmt
Comment by Sammy

Is there an alternative for those kinds of frontend apps? I would love to protect my backend in the same way, but the front end is a React app

Sammy
Comment by Alexander Johansson

I had the same idea, so I made this configuration as well. When it didn't work, I started to google and found this post, where you're doing the exact same configuration as I did.
I did this test with two web apps, with a Linux app service plan. This didn't work.
So I tried the exact same configuration, but with a windows app service plan. Then it worked.
Have you had any luck with a Linux app service plan?
Thank you!

Alexander Johansson
Comment by Alexander Johansson

Ok, so now I've waited for like 19 hours, and now the Linux web apps works as expected as well. I guess it had to do with the following:
"Communicating with resources behind a service endpoint for this subnet may take a number of hours to take effect".
19 hours, is quite a number of hours though.

Alexander Johansson
Comment by Mark Heath

yeah that is slow. thanks for sharing your experiences though

Mark Heath
Comment by Ketan Shah

Hi Mark,
This was very helpful blog (and your pluralsight courses as well). I tried to set the Rule on my App Service (the rule gets set) but the Virtual Network and Subnet is showing blank (I have used the powershell you mentioned above) Rule is added but the Virtual network and subnet are showing blank (When I select the rule and the pane opens up).. Is this because this is a LINUX App Service plan? As I read in the comments it took someone 19hours on Linux App. Or I am having totally a different issue (the Virtual Network and subnet are already created and names used are correct).

Ketan Shah
Comment by Ketan Shah

Hello @disqus_kGSxeU5mHg:disqus , I think I am facing a similar issue, in my case I have created the VNet and Subnet and now after applying the Rule to the web-app I do see in the Networking-->Access Restrictions the Rule that is created but upon selecting the rule I see Name of the Rule but the Vnet and Subnet is blank. Now this is my Linux app service plan and Web-App hosted on Linux. And for now its been 8 hours but still not showing up (Although from portal I can manually select the VNet and Subnet on that rule created when I go to the Web-App-->>Networking-->>Access Restrictions and select the Rule. Thought of checking with you if you had a similar issue (and may be my best bet is to wait for 20hours to see if it changes). Appreciate your response. Thank you.

Ketan Shah
Comment by Ketan Shah

Nevermind my issue was resolved it was a SILLY mistake on my part .. the Resource Group Name for the Vnet was all given in UPPER CASE where as it is defined in Camel case :(. This was super silly mistake but learnt a lesson it is case-sensitive.

Ketan Shah
Comment by Mark Heath

glad you got it working and thanks for sharing your solution

Mark Heath
Comment by Amine

I tried the arm template just as it is. Everything built correctly, no errors. It's been two days and still the web can't access the api. Something wrong here for sure.

Amine
Comment by Qing Wan

Hi Mark, I was having a similar problem for many months and found your article today, it solved my problem immidiately.
I have a web app running on App Service, and the app need to connect to a Azure SQL db to run query. The gaol is to protect the database, block all connections to the database except the ones from the app.
After reading your article, I finally understand what "Service Endpoint" is. The solution is simple and can be done all via the Azure Portal GUI.
1. Create VNet and subnet with Microsoft.Sql service
2. Integrate VNet with the App
3. In SQL server filewall, block public access and Azure access. Add the subnet created in step 1 to allowed virtual network.
and Done!
Thank you very much for writing a great blog!

Qing Wan
Comment by Killua

@markheath1010:disqus
Thank you so much. I need this explanation :)
For other readers this working fine with self hosted App Gateway like Ocelot.

Killua
Comment by Ferenc Szabó

I implemented the same thing, except with private endpoints instead of service endpoints, and an APIM. I haven't read through the whole article, but I can't seem to see any references to the configuration needed to route traffic(https://docs.microsoft.com/....
Anyway, what I'm not sure about is whether the connection between the two apps are secure or not, meaning do I need https between them or not? Does the traffic flow through network resources not in the Vnet? Can I terminate https with an app gateway?
Thank you!

Ferenc Szabó
Comment by José Salazar

Hi Mark! This is a great post, I was able to follow all the steps using Azure's portal, and it works great.
It is worthwhile clarifying though, that this approach will likely work when your front end uses sort of SSR approach and/or the fetching process occurs in the backend of your frontend. For SPAs let's say, this won't work as the outbound traffic goes through your client's (browser) ip addres :)
Cheers!

José Salazar
Comment by Tirlochan

Hi Mark, it is a great insightful post on securing backend system. I used the same CLI commands to create the required services in azure. When the access restriction were not added the frontend was able to call the backend and there was no issue. When I added the access restriction and integrated the frontend with the VNET created, and test the changes by making call to backend from that frontend it is giving CORS error(which I can see in network tab).
Although in azure portal the backend has CORS policy added for that particular frontend app but still it gives CORS error on adding the access restrictions.
Can you please help me out with this and how to make that backend only accessible to frontend?

Tirlochan