The blog of a cloud agnostic professional and craft beer connoisseur

Integrating Azure Front Door WAF with Azure Container Apps

Original Post Read More

Many customers require Web Applications & APIs to only be accessible via a private IP address with a Web Application Firewall on the internet edge, to protect from common exploits and vulnerabilities. Azure Front Door provides global routing and WAF capabilities to satisfy this requirement.

 

Azure Container Apps ingress can be exposed on either a Public or Private IP address. One option is to put Azure Front Door in front of an ACA public endpoint, but currently there is no way (other than in application code) to restrict access to the ACA public IP address from a single Azure Front Door instance. Azure App Service Access restrictions supports this scenario, but unfortunately, there is currently no equivalent access restriction for Azure Container Apps.

 

To work around this limitation, Azure Private Link Service can be provisioned in front of an internal ACA load balancer. A Private endpoint (NIC with private IP in a virtual network) is connected to the Private Link Service and an Azure Front Door Premium SKU instance can then be used to connect to the private endpoint (known as a Private Origin in AFD). This configuration removes the need to inspect the value of the “X-Azure-FDID” header sent from AFD since only a single AFD instance is connected to the private endpoint, guaranteeing traffic to the ACA environment occurs only from that specific AFD instance. The overall architecture is captured in the diagram below.

 

In order to create this architecture, we will cover the high-level steps outlined below.

 

1. Deploy an internal Azure Container App environment

2. Create an Azure Front Door Premium instance, origin group & route

3. Create an Azure Private Link Service (PLS) instance

4. Deploy an Azure Container App instance

5. Finally, approve the private endpoint connection to PLS

 

All steps above have been codified into an Azure Bicep deployment and shell script. To deploy the sample, you will need an Azure subscription and Bash or PowerShell console with the Az CLI installed. The Bicep templates and scripts referenced in this article are available on my GitHub, here.

 

First, let’s review the Bash shell script used to deploy the Bicep template. I also included a PowerShell script in my GitHub repo, which is almost identical, if that’s your preferred shell.

 

#!/bin/bash

LOCATION=’australiaeast’
PREFIX=’frontdoor’
RG_NAME=”${PREFIX}-aca-rg”

# create resource group
az group create –location $LOCATION –name $RG_NAME

# deploy infrastructure
az deployment group create
–resource-group $RG_NAME
–name ‘infra-deployment’
–template-file ./main.bicep
–parameters location=$LOCATION
–parameters prefix=$PREFIX

# get deployment template outputs
PLS_NAME=`az deployment group show –resource-group $RG_NAME –name ‘infra-deployment’ –query properties.outputs.privateLinkServiceName.value –output tsv`
AFD_FQDN=`az deployment group show –resource-group $RG_NAME –name ‘infra-deployment’ –query properties.outputs.afdFqdn.value –output tsv`
PEC_ID=`az network private-endpoint-connection list -g $RG_NAME -n $PLS_NAME –type Microsoft.Network/privateLinkServices –query [0].id –output tsv`

# approve private endpoint connection
echo “approving private endpoint connection ID: ‘$PEC_ID'”
az network private-endpoint-connection approve -g $RG_NAME -n $PLS_NAME –id $PEC_ID –description “Approved”

# test AFD endpoint
curl https://$AFD_FQDN

 

 

The script first defines 3 environment variables used throughout the script – LOCATION, PREFIX & RG_NAME. Modify these as you see fit for your environment. A resource group is created using a reference to the $RG_NAME variable, then on line 11, the Bicep template is deployed to the resource group.

 

Once the deployment has completed, 3 deployment template outputs are collected and used as input to the private endpoint approval command on line 25. 

 

Let’s break down the resources the deployed by the template. 

 

Input parameters are defined to control the deployment location, prefix and container image uri.

 

 

param location string = ‘australiaeast’
param prefix string = ‘contoso’
param imageName string = ‘mcr.microsoft.com/azuredocs/containerapps-helloworld:latest’

var suffix = uniqueString(resourceGroup().id)
var vnetName = ‘${prefix}-vnet-${suffix}’
var frontDoorName = ‘${prefix}-afd-${suffix}’
var wafPolicyName = ‘${prefix}wafpolicy’
var workspaceName = ‘${prefix}-wks-${suffix}’
var appName = ‘${prefix}-app-${suffix}’
var plsNicName = ‘${prefix}-pls-nic-${suffix}’
var plsName = ‘${prefix}-pls-${suffix}’
var appEnvironmentName = ‘${prefix}-env-${suffix}’
var originName = ‘${prefix}-origin-${suffix}’
var originGroupName = ‘${prefix}-origin-group-${suffix}’
var afdEndpointName = ‘${prefix}-afd-ep-${suffix}’
var loadBalancerName = ‘kubernetes-internal’
var defaultDomainArr = split(appEnvironment.properties.defaultDomain, ‘.’)
var appEnvironmentResourceGroupName = ‘mc_${defaultDomainArr[0]}-rg_${defaultDomainArr[0]}_${defaultDomainArr[1]}’

 

 

A virtual network with two subnets is created. The ‘infrastructure-subnet’ is used by the ACA environment to host an internal Azure load balancer and the ‘privatelinkservice-subnet’ is used to host the Azure Private Link Service.

 

 

resource vnet ‘Microsoft.Network/virtualNetworks@2022-07-01’ = {
name: vnetName
location: location
properties: {
addressSpace: {
addressPrefixes: [
‘10.0.0.0/16’
]
}
subnets: [
{
name: ‘infrastructure-subnet’
properties: {
addressPrefix: ‘10.0.0.0/23’
delegations: []
privateEndpointNetworkPolicies: ‘Disabled’
privateLinkServiceNetworkPolicies: ‘Enabled’
}
}
{
name: ‘privatelinkservice-subnet’
properties: {
addressPrefix: ‘10.0.2.0/28’
delegations: []
privateEndpointNetworkPolicies: ‘Disabled’
privateLinkServiceNetworkPolicies: ‘Disabled’
}
}
]
virtualNetworkPeerings: []
enableDdosProtection: false
}
}

 

 

A log analytics workspace is created to host the ACA application & System logs

 

 

resource wks ‘Microsoft.OperationalInsights/workspaces@2021-12-01-preview’ = {
name: workspaceName
location: location
properties: {
sku: {
name: ‘pergb2018’
}
retentionInDays: 30
features: {
enableLogAccessUsingOnlyResourcePermissions: true
}
workspaceCapping: {
dailyQuotaGb: -1
}
publicNetworkAccessForIngestion: ‘Enabled’
publicNetworkAccessForQuery: ‘Enabled’
}
}

 

 

Next, an Azure Container App environment is deployed. Notice the ‘properties.appLogsConfiguration’ and ‘properties.vnetConfiguration’ sections where the Log Analytics workspace and infrastructure subnet are specified, respectively.

 

 

resource appEnvironment ‘Microsoft.App/managedEnvironments@2022-06-01-preview’ = {
name: appEnvironmentName
location: location
sku: {
name: ‘Consumption’
}
properties: {
vnetConfiguration: {
internal: true
infrastructureSubnetId: vnet.properties.subnets[0].id
dockerBridgeCidr: ‘10.2.0.1/16’
platformReservedCidr: ‘10.1.0.0/16’
platformReservedDnsIP: ‘10.1.0.2’
outboundSettings: {
outBoundType: ‘LoadBalancer’
}
}
appLogsConfiguration: {
destination: ‘log-analytics’
logAnalyticsConfiguration: {
customerId: wks.properties.customerId
sharedKey: listKeys(wks.id, wks.apiVersion).primarySharedKey
}
}
zoneRedundant: false
}
}

 

 

An Azure Container App is then provisioned into the ACA environment. In this example we are deploying the ACA HelloWorld application from the Microsoft Container Registry (mcr.microsoft.com/azuredocs/containerapps-helloworld:latest).

 

 

resource containerApp ‘Microsoft.App/containerApps@2022-06-01-preview’ = {
name: appName
location: location
identity: {
type: ‘None’
}
properties: {
managedEnvironmentId: appEnvironment.id
configuration: {
activeRevisionsMode: ‘Single’
ingress: {
external: true
targetPort: 80
exposedPort: 0
transport: ‘Auto’
traffic: [
{
weight: 100
latestRevision: true
}
]
allowInsecure: false
}
}
template: {
containers: [
{
image: imageName
name: appName
resources: {
cpu: ‘0.25’
memory: ‘0.5Gi’
}
}
]
scale: {
maxReplicas: 10
}
}
}
}

 

 

An Azure Private Link Service is deployed using a separate Bicep module file (./modules/pls.bicep). Notice that the ‘appEnvironmentResourceGroupName’ parameter expects the ‘MC_’ prefixed resource group name that’s automatically created when a custom virtual network is specified at ACA environment deployment time.
The PLS deployment will also create a NIC in the ‘privatelinkservice-subnet’, to which the Azure Front Door backend will connect later in the deployment.

 

 

param name string
param location string
param appEnvironmentResourceGroupName string
param loadBalancerName string
param subnetId string

resource loadBalancer ‘Microsoft.Network/loadBalancers@2022-07-01’ existing = {
name: loadBalancerName
scope: resourceGroup(appEnvironmentResourceGroupName)
}

resource privateLinkService ‘Microsoft.Network/privateLinkServices@2022-07-01’ = {
name: name
location: location
properties: {
autoApproval: {
subscriptions: [
subscription().subscriptionId
]
}
visibility: {
subscriptions: [
subscription().subscriptionId
]
}
fqdns: []
enableProxyProtocol: false
loadBalancerFrontendIpConfigurations: [
{
id: loadBalancer.properties.frontendIPConfigurations[0].id
}
]
ipConfigurations: [
{
name: ‘ipconfig-0’
properties: {
privateIPAllocationMethod: ‘Dynamic’
subnet: {
id: subnetId
}
primary: true
privateIPAddressVersion: ‘IPv4’
}
}
]
}
}

output id string = privateLinkService.id
output name string = privateLinkService.name

 

 

The Azure Front Door Premium instance and it’s dependent Endpoint, Origin, Origin Group and Route resources are now created. 
Endpoint – defines a new publicly accessible Global AFD endpoint
Origin Group – defines the AFD load balancing and health probe settings
Origin – associates the ACA container app ingress hostname & header with the Azure Private Link Service
Route – binds the Endpoint to the Origin Group

 

 

resource frontDoor ‘Microsoft.Cdn/profiles@2022-11-01-preview’ = {
name: frontDoorName
location: ‘Global’
sku: {
name: ‘Premium_AzureFrontDoor’
}
properties: {
originResponseTimeoutSeconds: 30
extendedProperties: {
}
}
}

resource afdOriginGroup ‘Microsoft.Cdn/profiles/origingroups@2022-11-01-preview’ = {
parent: frontDoor
name: originGroupName
properties: {
loadBalancingSettings: {
sampleSize: 4
successfulSamplesRequired: 3
additionalLatencyInMilliseconds: 50
}
healthProbeSettings: {
probePath: ‘/’
probeRequestType: ‘GET’
probeProtocol: ‘Https’
probeIntervalInSeconds: 60
}
sessionAffinityState: ‘Disabled’
}
}

resource afdEndpoint ‘Microsoft.Cdn/profiles/afdendpoints@2022-11-01-preview’ = {
parent: frontDoor
name: afdEndpointName
location: ‘Global’
properties: {
autoGeneratedDomainNameLabelScope: ‘TenantReuse’
enabledState: ‘Enabled’
}
}

resource afdRoute ‘Microsoft.Cdn/profiles/afdendpoints/routes@2022-11-01-preview’ = {
parent: afdEndpoint
name: ‘route’
properties: {
customDomains: []
originGroup: {
id: afdOriginGroup.id
}
originPath: ‘/’
ruleSets: []
supportedProtocols: [
‘Http’
‘Https’
]
patternsToMatch: [
‘/*’
]
forwardingProtocol: ‘MatchRequest’
linkToDefaultDomain: ‘Enabled’
httpsRedirect: ‘Enabled’
enabledState: ‘Enabled’
}
dependsOn: [
afdOrigin
]
}

resource afdOrigin ‘Microsoft.Cdn/profiles/origingroups/origins@2022-11-01-preview’ = {
parent: afdOriginGroup
name: originName
properties: {
hostName: containerApp.properties.configuration.ingress.fqdn
httpPort: 80
httpsPort: 443
originHostHeader: containerApp.properties.configuration.ingress.fqdn
priority: 1
weight: 1000
enabledState: ‘Enabled’
sharedPrivateLinkResource: {
privateLink: {
id: privateLinkService.outputs.id
}
privateLinkLocation: location
status: ‘Approved’
requestMessage: ‘Please approve this request to allow Front Door to access the container app’
}
enforceCertificateNameCheck: true
}
}

 

 

Finally, we define a WAF policy and associate it with the AFD Endpoint.

 

 

resource wafPolicy ‘Microsoft.Network/FrontDoorWebApplicationFirewallPolicies@2022-05-01’ = {
name: wafPolicyName
location: ‘Global’
sku: {
name: ‘Premium_AzureFrontDoor’
}
properties: {
policySettings: {
enabledState: ‘Enabled’
mode: ‘Prevention’
requestBodyCheck: ‘Enabled’
}
managedRules: {
managedRuleSets: [
{
ruleSetType: ‘Microsoft_DefaultRuleSet’
ruleSetVersion: ‘1.1’
ruleGroupOverrides: []
exclusions: []
}
{
ruleSetType: ‘Microsoft_BotManagerRuleSet’
ruleSetVersion: ‘1.0’
ruleGroupOverrides: []
exclusions: []
}
]
}
}
}

resource afdSecurityPolicy ‘Microsoft.Cdn/profiles/securitypolicies@2022-11-01-preview’ = {
parent: frontDoor
name: ‘${prefix}-default-security-policy’
properties: {
parameters: {
wafPolicy: {
id: wafPolicy.id
}
associations: [
{
domains: [
{
id: afdEndpoint.id
}
]
patternsToMatch: [
‘/*’
]
}
]
type: ‘WebApplicationFirewall’
}
}
}

 

 

One the template has deployed successfully, the 3 template output parameters are collected and used as input to the ”az network private-endpoint-connection approve” Az CLI command to approve the Private Endpoint connection to the Private Link Service, on line 25.

 

Once approved, you will be able to access the Azure Container app via a browser using the AFD endpoint URL saved in the AFD_FQDN environment variable.

 

 

$ echo https://$AFD_FQDN
https://frontdoor-afd-ep-rczv4qasdrrms-akcehrf2dncngxfa.z01.azurefd.net