How it works
Previously, I’ve blogged about two variants that we used at Prof-IT Services to block malicious IP addresses on Azure Frontdoor that were going over a certain threshold. We’ve now created a more simplified version, that only uses a C# function app, managed identity, and log analytics workspace.
Blocking abusive IP addresses that hit a certain threshold, prevents Treat Actors from finding vulnerabilities, as there is a maximum amount of request they can attempt before all traffic is blocked. The KQL query in this blog returns IP addresses that have more than 500 blocked requests in a time period of 24 hours.
The final result can be seen in the screenshot below, these bots are blocked entirely, even if they would find a vulnerability that would succeed, it will be blocked nevertheless.

These are the previous blogs with different methods. The variant #3 in this blog, is the most stable and simple.
WAF Policy
The Function App will export the existing WAF policies, and update it with the current abusive IP addresses if there are any changes. For this to work properly, you’ll need to create a custom rule “BlockedIPs” to the WAF Policy. Set it to Match type IP address & Does contain + a bogus IP address.

Azure Function App & IAM
We will leverage a Function App to query the Log Analytic Workspace, and update the WAF policies should abusive IP addresses be found.

Start with creating a .NET in-process model function app.
Enable system assigned managed identity, we’ll use this method to access resources.
Assign the newly created object in Entra ID the following rights:
- Log Analytics Reader on the LA WorkSpace
- Contributor on the WAF Policies

Visual Studio Project
We need to use VS to push the application to the function app, it’s a simple process, but it can look complicated if you’re not familiar with development processes. Feel free to reach out if you’re stuck at any point.

Install Visual Studio Community with Azure Development features installed.

Create a new .NET 8.0 in-process function app project, with a timer trigger. The default runs every 5 minutes, we’ll update this with the final code.
You should have an open project now, ready to paste the code into the main cs file in the function app project. You do need to manually install the nuget packages (with prefix using) to run the function. Your project will create a lot of errors and won’t run if you skip this step.
Right click the project name, and click ‘Manage NuGet Packages’. In the screen that opens, you can install the packages with the ‘using’ prefix, such as ‘Azure.Core’.

Don’t forget to replace the highlighted variables with the specifics of your environment, it will need the Azure Subscription, Log Analytics workspace ID, and resource group of the WAF policies. You’ll also need to define the WAF policy names around lines 140-161. If you don’t want to use a dev/prod environment, you can simply add the WAF names in the dev section at line 156.


When you’ve tested successfully on your local device, the function app can be deployed to Azure.



On the Azure Portal, we can see the executions and the logging the same way you’ve seen it execute on your local device. This also allows us to configure alerting should there be a hit, error, etc.


Code
The variables in the code need modifications, and the highlighted code as well depending on if you’re using a dev/prod environment.
using System;
using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Linq;
using System.Text;
using Azure.Identity;
using Azure.Monitor.Query;
using Azure.Core;
namespace BlockWAFIP
{
public static class BlockWAFIP
{
public static string azureid = ""; // your azure subscription
public static string resgrp = ""; // the resource group the WAF policies are in
public static string workspaceId = ""; //workspace id of the log analytics workspace that ingests the frontdoor data
[FunctionName("TimerTriggerFunction")]
public static async Task RunTimer([TimerTrigger("0 */15 * * * *")] TimerInfo myTimer, ILogger log)
{
log.LogInformation($"C# Timer trigger function executed at: {DateTime.Now}");
try
{
var credential = new DefaultAzureCredential();
var client = new LogsQueryClient(credential);
string query = @"AzureDiagnostics
| where TimeGenerated >= ago(24h) // Filter last 24 hours
| where ResourceProvider == 'MICROSOFT.CDN'
and Category == 'FrontDoorWebApplicationFirewallLog'
and action_s == 'Block'
and ruleName_s != 'RateLimiter'
and details_msg_s !contains 'Outlook'
and details_matches_s !contains 'Outlook'
| summarize RequestCount = count() by ClientIP = clientIP_s, UserAgent = userAgent_s, Resource, requestUri_s
| where RequestCount > 500
| order by RequestCount desc";
log.LogInformation("Executing KQL query...");
var response = await client.QueryWorkspaceAsync(workspaceId, query, QueryTimeRange.All);
var results = response.Value.Table.Rows.Select(row => row[0].ToString()).ToList();
// Now use `results` (List of IPs) to update WAF Policy
log.LogInformation($"Blocked IPs: {string.Join(", ", results)}");
if (results.Count() > 0)
{
await InitiateBlockWAFIP(results, log);
}
}
catch (Exception ex)
{
log.LogError($"An error occurred: {ex.Message}");
throw;
}
}
public static async Task InitiateBlockWAFIP(List<string> ips, ILogger log)
{
try
{
log.LogInformation("Processing WAF");
await http.GetToken();
var client = new HttpClient();
foreach (var wafpolicyname in GetWAFPolicies())
{
log.LogInformation("Processing " + wafpolicyname);
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", http.accessToken);
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));
HttpResponseMessage response = await client.GetAsync("https://management.azure.com/subscriptions/" + azureid + "/resourceGroups/" + resgrp + "/providers/Microsoft.Network/frontdoorWebApplicationFirewallPolicies/" + wafpolicyname + "?api-version=2020-11-01");
string result = await response.Content.ReadAsStringAsync();
var currentwaf = JsonConvert.DeserializeObject<WAF.WAFRoot>(result);
var newwaf = new WAF.WAFRoot();
newwaf.location = currentwaf.location;
newwaf.properties = new WAF.Properties();
newwaf.properties.policySettings = currentwaf.properties.policySettings;
newwaf.properties.managedRules = currentwaf.properties.managedRules;
newwaf.properties.customRules = currentwaf.properties.customRules;
newwaf.properties.policySettings = currentwaf.properties.policySettings;
newwaf.sku = currentwaf.sku;
var otherrules = currentwaf.properties.customRules.rules.Where(p => p.name != "BlockedIPs");
var blockedIPs = currentwaf.properties.customRules.rules.FirstOrDefault(p => p.name == "BlockedIPs");
var matchConditions = new List<WAF.MatchCondition>();
var blockedIPsSet = new HashSet<string>(blockedIPs.matchConditions.FirstOrDefault().matchValue);
var ipsSet = new HashSet<string>(ips);
if (blockedIPsSet.SetEquals(ipsSet))
{
continue;
}
blockedIPs.matchConditions.FirstOrDefault().matchValue = ips;
var rules = (otherrules).ToList();
rules.Add(blockedIPs);
newwaf.properties.customRules.rules = rules;
var post = JsonConvert.SerializeObject(newwaf, Newtonsoft.Json.Formatting.None, new JsonSerializerSettings
{
NullValueHandling = NullValueHandling.Ignore
});
var poststring = post.ToString();
var postresponse = await client.PutAsync("https://management.azure.com/subscriptions/" + azureid + "/resourceGroups/" + resgrp + "/providers/Microsoft.Network/frontdoorWebApplicationFirewallPolicies/" + wafpolicyname + "?api-version=2020-11-01", new StringContent(post, Encoding.UTF8, "application/json"));
postresponse.EnsureSuccessStatusCode();
}
client.Dispose();
}
catch (Exception ex)
{
log.LogError($"An error occurred: {ex.Message}");
throw;
}
return;
}
public static string GetWafEnv()
{
string siteName = Environment.GetEnvironmentVariable("WEBSITE_SITE_NAME") ?? "LocalFunctionApp";
string wafName = siteName.ToUpper() switch
{
var name when name.EndsWith("DEV") => "DEV",
var name when name.EndsWith("PROD") => "PROD",
_ => "DEV" // Default case
};
return wafName;
}
public static List<String> GetWAFPolicies()
{
var wafenv = GetWafEnv();
var wafpolicynames = new List<string>();
if (wafenv.Contains("DEV"))
{
wafpolicynames = new List<string> { "wafdev" };
}
if (wafenv.Contains("PROD"))
{
wafpolicynames = new List<string> { "wafstage", "wafprod" };
}
return wafpolicynames;
}
}
public class http
{
public static string token { get; set; }
public static string accessToken { get; set; }
public static async Task GetToken()
{
var credential = new DefaultAzureCredential();
var tokenRequestContext = new TokenRequestContext(new[] { "https://management.azure.com/.default" });
var token = await credential.GetTokenAsync(tokenRequestContext);
// Use the access token to authenticate to Azure resources
accessToken = token.Token;
}
}
}
namespace Entitie
{
public class EntitieRoot
{
[JsonProperty("odata.metadata")]
public string odatametadata { get; set; }
public List<Value> value { get; set; }
}
public class Value
{
[JsonProperty("odata.etag")]
public string odataetag { get; set; }
public string PartitionKey { get; set; }
public object RowKey { get; set; }
public DateTime Timestamp { get; set; }
}
}
namespace WAF
{
public class CustomRules
{
public List<Rule> rules { get; set; }
}
public class Exclusion
{
public string matchVariable { get; set; }
public string selectorMatchOperator { get; set; }
public string selector { get; set; }
}
public class ManagedRules
{
public List<ManagedRuleSet> managedRuleSets { get; set; }
}
public class ManagedRuleSet
{
public string ruleSetType { get; set; }
public string ruleSetVersion { get; set; }
public string ruleSetAction { get; set; }
public List<RuleGroupOverride> ruleGroupOverrides { get; set; }
public List<Exclusion> exclusions { get; set; }
}
public class MatchCondition
{
public string matchVariable { get; set; }
public string selector { get; set; }
public string @operator { get; set; }
public bool negateCondition { get; set; }
public List<string> matchValue { get; set; }
public List<string> transforms { get; set; }
}
public class PolicySettings
{
public string enabledState { get; set; }
public string mode { get; set; }
public object redirectUrl { get; set; }
public object customBlockResponseStatusCode { get; set; }
public object customBlockResponseBody { get; set; }
public string requestBodyCheck { get; set; }
}
public class Properties
{
public PolicySettings policySettings { get; set; }
public CustomRules customRules { get; set; }
public ManagedRules managedRules { get; set; }
public List<object> frontendEndpointLinks { get; set; }
public List<SecurityPolicyLink> securityPolicyLinks { get; set; }
public List<object> routingRuleLinks { get; set; }
public string resourceState { get; set; }
public string provisioningState { get; set; }
}
public class WAFRoot
{
public string id { get; set; }
public string type { get; set; }
public string name { get; set; }
public string location { get; set; }
public Tags tags { get; set; }
public Sku sku { get; set; }
public Properties properties { get; set; }
}
public class Rule
{
public string name { get; set; }
public string enabledState { get; set; }
public int? priority { get; set; }
public string ruleType { get; set; }
public int? rateLimitDurationInMinutes { get; set; }
public int? rateLimitThreshold { get; set; }
public List<MatchCondition> matchConditions { get; set; }
public string action { get; set; }
public string ruleId { get; set; }
public List<Exclusion> exclusions { get; set; }
}
public class RuleGroupOverride
{
public string ruleGroupName { get; set; }
public List<Rule> rules { get; set; }
public List<Exclusion> exclusions { get; set; }
}
public class SecurityPolicyLink
{
public string id { get; set; }
}
public class Sku
{
public string name { get; set; }
}
public class Tags
{
}
}
Code language: C# (cs)