Introduction
In the dynamic realm of today’s digital environment, safeguarding your applications and data holds utmost importance. This blog post is designed to lead you through the steps of establishing comprehensive security measures for an Azure Function App, Azure SQL database, and API Management. Our approach involves leveraging Azure Virtual Network (VNet) for robust network protection, employing Private Link for secure communication, and utilizing Azure Bicep for infrastructure as code. Additionally, we’ll implement Private DNS zones for both the SQL and Function app, facilitating easy access through Private Endpoints.
Quick start
You need following to get started.
- Azure Subscription
- Azure CLI configured
Here is the GitHub repository that contains all the Bicep templates and accompanied bash scripts that helps provisioning the entire stack.
Into the Platform folder we have a main.bicep that stiches all the granular Bicep modules.
You need to perform the following:
- Clone the repo. (e.g.
git clone https://github.com/MoimHossain/secure-api-functions.git
) - Navigate to Platform folder (e.g.
cd /src/Infrastructure/Platform
) - Run the bash script create-core-infra.sh. (e.g.
./create-core-infra.sh
)
That is all you need. It will take a while and provision the entire stack protected with network restrictions.
If you want to change the names of the resource group, and/or resources please change them in the script, or bicep params – as applicable.
Breakdowns
Creating Azure Virtual Network (VNet)
We will create a VNet with 3 subnets within it, one for data, one for function and one for API management. Here’s the crucial part of it in below snippet:
resource virtualNetwork 'Microsoft.Network/virtualNetworks@2021-05-01' = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
addressPrefix
]
}
subnets: [
{
name: dataSubnetName
properties: {
addressPrefixes: [
dataSubnetAddressPrefix
]
}
}
{
name: gatewaySubnetName
properties: {
addressPrefixes: [
gatewaySubnetAddressPrefix
]
networkSecurityGroup: {
id: nsgApim.outputs.nsgId
}
}
}
{
name: backendSubnetName
properties: {
addressPrefixes: [
backendAddressPrefix
]
delegations: [
{
name: 'delegation'
properties: {
serviceName: 'Microsoft.Web/serverfarms'
}
type: 'Microsoft.Network/virtualNetworks/subnets/delegations'
}
]
}
}
]
}
}
Notice, the backend subnet is delegated to Microsoft.Web/serverfarms
so that we can later use Virtual network integration for the Function.
Azure SQL Database with Private Endpoint
Now we can provision the SQL server and database with a private endpoint.
module sqlServer 'modules/data/sql-server.bicep' = {
name: sqlServerName
params: {
serverName: sqlServerName
location: location
sqlAdminUserAssignedIdentityName: sqlAdminUAMI.name
azureADOnlyAuthentication: false
publicNetworkAccess: 'Disabled'
}
}
module sqlDatabase 'modules/data/sql-database.bicep' = {
name: sqlDatabaseName
params: {
databaseName: sqlDatabaseName
location: location
serverName: sqlServer.outputs.sqlServerName
}
}
module sqlPrivateEndpoint 'modules/network/private-endpoints/private-endpoint.bicep' = {
name: sqlPrivateEndpointName
params: {
location: location
vnetId: virtualNetwork.outputs.vnetId
createPrivateDns: true
zoneFqdn: sqlZoneName
zoneGroupName: sqlZoneGroupName
privateEndpointName: sqlPrivateEndpointName
privateLinkServiceId: sqlServer.outputs.sqlServerResourceId
subnetId: virtualNetwork.outputs.dataSubnetId
targetSubResource: 'sqlServer'
}
}
Here, the SQL server public network access is completely disabled and only allowed access via the private endpoint. Here we only showed the modules but into the repo you will notice we also create the private DNS for the private endpoint.
Azure Function App with Private Endpoint
Like SQL, we will do the similar for the function. It will Disable all public internet access and access will be only granted with a private endpoint, also a Private DNS zone will be created for the function domain name.
module serverFarm 'modules/web/server-farm.bicep' = {
name: serverFarmName
params: {
location: location
name: serverFarmName
}
}
module functionApp 'modules/web/function-app.bicep' = {
name: functionAppName
params: {
location: location
name: functionAppName
delegatedSubnetResourceId: virtualNetwork.outputs.backendSubnetId
functionUAMIName: functionIdentity.name
serverFarmId: serverFarm.outputs.serverfarmId
}
}
module functionPrivateEndpoint 'modules/network/private-endpoints/private-endpoint.bicep' = {
name: funcPrivateEndpointName
params: {
location: location
vnetId: virtualNetwork.outputs.vnetId
createPrivateDns: true
zoneFqdn: funcZoneName
zoneGroupName: funcZoneGroupName
privateEndpointName: funcPrivateEndpointName
privateLinkServiceId: functionApp.outputs.functionAppId
subnetId: virtualNetwork.outputs.apimSubnetId
targetSubResource: 'sites'
}
}
API Management Configuration
We will create a External API management which has a public endpoint but the backends are connected via private endpoint.
module apimService 'modules/api-management/apim.bicep' = {
name: apimServiceName
params: {
apimServiceName: apimServiceName
location: location
sku: sku
skuCount: skuCount
publisherEmail: publisherEmail
publisherName: publisherName
publicIpAddressName: publicIpAddressName
publicIpDnsLabel: publicIpDnsLabel
subnetName: virtualNetwork.outputs.apimSubnetName
virtualNetworkName: virtualNetwork.name
}
}
Code deployment via Pipeline
One caveat with network locked Azure Functions or web apps is, once they are network protected, you can’t deploy code via Kudo site. You need to have an agent or VM that is connected to that VNet to perform code deployment.
I will propose a rather simplified way to achieve the same without VM or Self-Hosted pipeline agents.
Here is the trick, in our pipeline before we do the code publish to Azure Function, we will change the network rules for the Functions Kudo site, Just in time, creating a network rule that allows traffic from the pipeline agents host IP (exclusively to the Kudo site). Once the deployment/publish is done, we will remove that exemption to keep it protected as it was before.
Here is the bash script that we can run before the code publish:
## To enable the public network access and add the IP security restrictions to the function app
# Get local machine IP address
localMachineIP=$(curl -s ifconfig.me)
echo "Local Machine IP: $localMachineIP"
bodyJson=$(cat <<EOF
{
"properties": {
"publicNetworkAccess": "Enabled",
"siteConfig": {
"ipSecurityRestrictionsDefaultAction": "Deny",
"scmIpSecurityRestrictionsUseMain": false,
"scmIpSecurityRestrictionsDefaultAction": "Deny",
"scmIpSecurityRestrictions": [
{
"ipAddress": "$localMachineIP/32",
"action": "Allow",
"tag": "Default",
"priority": 100,
"name": "LOCALMACHINE",
"description": "LocalmachineIP",
"vnetSubnetResourceId": null,
"headers": {},
"serviceEndpointDisabled": false,
"id": "5"
}
],
"ipSecurityRestrictions": []
}
}
}
EOF
)
# Apply the changes to the function app
az resource patch \
--name <FUNC-NAME>\
--resource-group <RESOURCE-GROUP> \
--resource-type Microsoft.web/sites \
--is-full-object \
--properties "$bodyJson"
And once the publish is completed we will remove the exemption:
## To Disable the public network access to the function app
bodyJson=$(cat <<EOF
{
"properties": {
"publicNetworkAccess": "Disabled"
}
}
EOF
)
# Apply the changes to the function app
az resource patch \
--name <FUNC-NAME>\
--resource-group <RESOURCE-GROUP> \
--resource-type Microsoft.web/sites \
--is-full-object \
--properties "$bodyJson"
That is it!
Conclusion:
By implementing these Azure Bicep snippets, you have established a secure end-to-end architecture for your Azure resources, ensuring the confidentiality and integrity of your data and applications. Following these best practices helps protect your assets in the cloud environment.
Full code can be found here: https://github.com/MoimHossain/secure-api-functions