This post will explore the new support in Azure Application Gateway for Containers (AGC) for Web Application Firewall (WAF) as documented in https://aka.ms/agc/waf. This blog is part of a series:
- 1. Introduction to AGC architecture and components
- 2. Troubleshooting
- 3. Resource model expansion: end-to-end TLS
- 4. Overlay networking and VNet Flow Logs (this post)
- 5. Web Application Firewall (this post)
- 6. Integration with Istio
Before we start, kudos need to go to the great Christof Claessens, author of this extremely useful Azure Monitor Workbook to triage WAF logs.
Is WAF a big thing?
Well, yes. There are many reverse proxies for Kubernetes out there, but not that many that include Web Application Firewall (WAF) functionality. WAF support has always been one of the main reasons why Azure customers used Azure Application Gateway in front of their AKS clusters, and with Application Gateway for Containers (AGC) you get a much more container-friendly service than AG ever was.
I will not go here over all of the differences between Application Gateway and Application Gateway for Containers, you can instead refer to the official documentation in https://aka.ms/agc or to the first blog post in this series. Suffice to say that they are completely different products, that share in common little more than their name.
Implementing the new support for WAF in AGC is quite straight-forward: you create an Application Gateway WAF policy in Azure (yes, the same WAF policy can be used for both AG and AGC), and in Kubernetes you define webApplicationFirewallPolicy resources to assign the policy. The WAF policy can be assigned to either an HTTPRoute (if you need different policies per application) or to a Listener, as the object diagram below describes:

The previous simplified diagram focuses on the WAF elements, for the full version of the AGC diagrams please refer to the previous posts in this series.
Hence, your first step would be creating a WAF policy in Azure whichever way you prefer: portal, REST API, PowerShell, etc. I used Azure CLI to create the WAF policy with a custom rule matching on the User-Agent HTTP header, with this code:
# Create WAF policy
az network application-gateway waf-policy create -g $rg -n $waf_policy_name --type Microsoft_DefaultRuleSet --version 2.1 -o none --only-show-errors
az network application-gateway waf-policy policy-setting update --policy-name $waf_policy_name -g $rg --state Enabled -o none
# Create custom rule based on custom header
match_conditions="[{'variables': [{'variableName': 'RequestHeaders', 'selector': 'User-Agent'}],'operator':'Contains','values': ['evilbot'], 'transforms': ['lowerCase']}]"
az network application-gateway waf-policy custom-rule create -g $rg --policy-name $waf_policy_name -n customrule01 --priority 20 --action Block -o none --only-show-errors \
--rule-type MatchRule --action Block --match-conditions "$match_conditions" \
If you are wondering about the custom rule I create there, I will use it later to test the WAF.
What is a Security Policy?
To be completely honest with you, I am not too sure. The concept of security policy exists in Azure Front Door as an intermediate resource between the Front Door and the WAF Policy resources (see here for more details), but I hadn’t used this in the context of Application Gateway. Whatever the case, with Application Gateway for Containers you will need a Security Policy in Azure, otherwise your webApplicationFirewallPolicy resource in Kubernetes will show the error message missing security policy for WAFPolicy "yada/agc-agc-waf-policy".
Hence, you can use this configuration to control which policies will be available for AGC instances to use: if there is not a security policy attached to a WAF policy, it will not be available for an AGC to use (at least in BYOD mode, see next paragraph).
If you deploy AGC in the “managed” mode, the ALB controller should create the security policy for you (didn’t happen in my test, feel free to share your experience). But if you use Bring-Your-Own-Deployment, you need it to create it yourself. I haven’t seen this resource in the portal, so I used the REST API documented here:
subscription_id=$(az account show --query id -o tsv)
waf_policy_id=$(az network application-gateway waf-policy show -g $rg -n $waf_policy_name --query id -o tsv)
url="https://management.azure.com/subscriptions/${subscription_id}/resourceGroups/${rg}/providers/Microsoft.ServiceNetworking/trafficControllers/${agc_name}/securityPolicies/${sec_policy_name}?api-version=2025-01-01"
body="{ \"location\": \"$location\", \"properties\": { \"wafPolicy\": { \"id\": \"$waf_policy_id\" } } }"
az rest --method put --uri "$url" --body "$body" -o none --only-show-errors
Two interesting things about the previous REST call:
- The security policy is a resource specific to Application Gateway for Containers, as you can tell from the resource provider in the URL (
trafficControllers, this was one of the names with which AGC was formerly known internally in Microsoft). - The security policy has a property to relate it to the WAF policy.
- Please note that this code is using variables. If you want to see the whole code you can find it in my GitHub repo.
WebApplicationFirewallPolicy
Now you need to attach the Azure WAF Policy to AGC. You can either attach it to a listener, if you want the policy to apply to all routes hanging from it, or to a specific HTTP route, if you want to use different WAF policies for different applications.
The YAML code to create the WebApplicationFirewallPolicy in Kubernetes attached to a listener should look like this:
apiVersion: alb.networking.azure.io/v1
kind: WebApplicationFirewallPolicy
metadata:
name: mypolicy-tls
namespace: default
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: $agc_name
namespace: default
sectionNames: [ https ]
webApplicationFirewall:
id: $waf_policy_id
Some remarks about that:
- You need to create the
WebApplicationFirewallPolicyresource in the same namespace as its target. Since I am targetting a listener configured as part of a gateway resource deployed in thedefaultnamespace, myWebApplicationFirewallPolicyresource must be in the default namespace too. - The listeners are not an independent resource in Kubernetes, but they are part of the gateway definition. Consequently, your target is the gateway, and you identify the listener(s) in the
sectionNamesattribute of thetargetRef(httpsis the name of my listener). - If you omit the
sectionNamesattribute in thetargetRefsection and you assign the WAFPolicy to a gateway, it will apply to all its listeners. - Lastly, you refer to the Azure WAF policy resource in the Kubernetes resource. You can get the Azure WAF policy ID with this command:
waf_policy_id=$(az network application-gateway waf-policy show -g $rg -n $waf_policy_name --query id -o tsv)
Alternatively, you can attach the WAF policy Kubernetes resource to a specific HTTP route. Here a YAML example to do that:
apiVersion: alb.networking.azure.io/v1
kind: WebApplicationFirewallPolicy
metadata:
name: $waf_policy_name
namespace: $app_ns
spec:
targetRef:
group: gateway.networking.k8s.io
kind: HTTPRoute
name: yadaapi
namespace: $app_ns
webApplicationFirewall:
id: $waf_policy_id
Here again you need to deploy the WebApplicationFirewallPolicy resource in the same namespace as the target. If your HTTP route was in the application’s namespace, that’s where your WebApplicationFirewallPolicy will go as well.
Note that there is no sectionNames attribute in the targetRef as it was the case with the attachment to the listener. When using a sectionName to include a specific URL you get the error message Spec.TargetRef.SectionNames must not be specified.
Testing
“Give me only one ping, Vasili!” Although in this case we will test with curl. We will use the -A flag, that allows to send requests to a custom User-Agent HTTP header, which will match on the WAF custom rule I defined earlier, and hence should be blocked:
❯ curl -k "http://${fqdn}/api/healthcheck"
{
"health": "OK",
"version": "1.0"
}
❯ curl -k "http://${fqdn}/api/healthcheck" -A evilbot
Access Forbidden
As you can see, using the “bad” user agent results in the request being blocked by the WAF, which proves it is working. If you are curious about what I use for testing, I am using the API part of the YADA app (Yet Another Demo App).
Logs
Of course, as with any other WAF, its logs will become your best friend (or enemy). This is where you will spend time identifying false positives to further refine your WAF policy with exclusions, enabling/disabling rules, creating new custom rules, etc. A new log category is enable in AGC for WAF logs: TrafficControllerFirewallLog (another place where AGC’s old name still shows).
Here you have a very basic query to explore these logs:

You might notice that per default these logs will be sent to a new table called AGCFirewallLogs.
The KQL query in the previous screenshot shows a subset of the available fields, there are more fields available. In the query results you can see that not only my custom rule customrule01 blocked the “evilbot” user agent, but also some attempts to get information about the web site were blocked.
Note that some of the field names in the AGC WAF logs are not exactly the same as for the standard Azure Application Gateway. Consequently, workbooks created for the WAF in Application Gateway will not work for the WAF in AGC. However, they are easily modifiable. For example, I quickly edited the fantastic workbook created by my former colleague Christof Claessens to get a version that would mostly (apparently the GitHub links for the rule definitions have changed, but that is not a problem of AG vs AGC):

You can get this workbook from my GitHub repo. Going forward, I will probably try to put this in https://github.com/Azure/Azure-Network-Security/, the official repo of the Azure Networking Security team for community-based artifacts.
Conclusion
As you can see, it is quite easy to deploy AGC with WAF functionality, so that your Kubernetes applications running on AKS will be protected.
I hope the WAF log workbook will be useful for you. If you have time to fix the GitHub links, Pull Requests to the GitHub repo are welcome!
Did I forget anything here? Let me know in the comments!

[…] 5. Web Application Firewall […]
LikeLike
[…] 5. Web Application Firewall […]
LikeLike
[…] 5. Web Application Firewall […]
LikeLike
[…] 5. Web Application Firewall […]
LikeLike
[…] 5. Web Application Firewall […]
LikeLike