Skip to content

Commit 3a3d08a

Browse files
Merge branch 'Azure:master' into master
2 parents 9754e14 + 666c14a commit 3a3d08a

17 files changed

Lines changed: 498 additions & 86 deletions

File tree

227 KB
Loading
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Synchronization of Azure Firewall DNAT rules to AKS services
2+
3+
This PowerShell script propagates any changes in Kubernetes services that expose applications over internal Azure Load Balancers to DNAT rules in an Azure Firewall policy, so that there is one DNAT rule for every exposed service.
4+
5+
This PowerShell script should be configured as an Azure Automation runbook that is called when Event Grid detects a change in the internal ALB associated to AKS. An Azure Logic App reacting to the event from Event Grid will give you more flexibility than directly configuring the Runbook's webhook in Event Grid:
6+
7+
![Architecture of automatic synchronization between AKS and Azure Firewall](./AzFW-AKS-sync.png)
8+
9+
For more details on how the script works, see a demo here:
10+
11+
[![Watch the video](https://img.youtube.com/vi/6A8AdfsGAXk/0.jpg)](https://www.youtube.com/watch?v=6A8AdfsGAXk)
12+
13+
The script has no parameters, optionally you can define the AKS cluster name and the Azure Firewall policy data (policy name, rule collection group and rule collection) as parameters.
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
##############################################
2+
# Script to sync Azure Firewall DNAT rules
3+
# to AKS services exposed via an Internal
4+
# Azure Load Balancer.
5+
#
6+
# Jose Moreno, 2025
7+
##############################################
8+
9+
10+
# Constants - consider moving some of these to parameters
11+
$resourceGroup = "akstest"
12+
$aksName = "aks"
13+
$azfwPolicyResourceGroup = "akstest"
14+
$azfwPolicyName = "myazfwpolicy"
15+
$azfwRCG = "DNAT_rcg"
16+
$azfwRC = "DNAT-rc"
17+
$azfwPublicIP = "1.2.3.4"
18+
$ruleSuffixLength = 5
19+
$randomCharacters = 'abcdefghijklmnopqrstuvwxyz0123456789'.ToCharArray()
20+
21+
# Ensures you do not inherit an AzContext in your runbook
22+
$null = Disable-AzContextAutosave -Scope Process
23+
# Connect using a Managed Service Identity
24+
try {
25+
Write-Output "Authenticating to Azure..."
26+
Connect-AzAccount -Identity
27+
}
28+
catch {
29+
Write-Output "There is no system-assigned user identity. Aborting."
30+
exit
31+
}
32+
33+
###########################
34+
# START #
35+
###########################
36+
37+
# Getting AKS information about the node RG and the load balancer
38+
try {
39+
$aks = Get-AzAksCluster -Name $aksName -ResourceGroupName $resourceGroup
40+
if ($null -ne $aks.Name) {
41+
Write-Output "AKS cluster $($aks.Name) found successfully in resource group $($resourceGroup)."
42+
}
43+
else {
44+
Write-Output "AKS cluster $($aksName) could not be found, aborting"
45+
exit
46+
}
47+
}
48+
catch {
49+
Write-Output "AKS cluster $($aksName) could not be found, aborting"
50+
exit
51+
}
52+
$aksALBs = Get-AzLoadBalancer -resourcegroup $aks.NodeResourceGroup
53+
Write-Output "$($aksALBs.Length) Load Balancers found in node resource group $($aks.NodeResourceGroup)."
54+
# Make sure the Azure Firewall policy and the RCG/RC exist
55+
try {
56+
$policy = Get-AzFirewallPolicy -Name $azfwPolicyName -ResourceGroupName $azfwPolicyResourceGroup
57+
if ($null -ne $policy.Name) {
58+
Write-Output "Azure Firewall Policy $($policy.Name) found successfully in resource group $($azfwPolicyResourceGroup)."
59+
}
60+
else {
61+
Write-Output "Azure Firewall Policy $($azfwPolicyName) could not be found in resource group $($azfwPolicyResourceGroup), aborting"
62+
exit
63+
}
64+
}
65+
catch {
66+
Write-Output "Azure Firewall Policy $($azfwPolicyName) could not be found in resource group $($azfwPolicyResourceGroup), aborting"
67+
exit
68+
}
69+
try {
70+
$rcg = Get-AzFirewallPolicyRuleCollectionGroup -Name $azfwRCG -AzureFirewallPolicyName $azfwPolicyName -ResourceGroupName $azfwPolicyResourceGroup
71+
if ($null -ne $rcg.Name) {
72+
Write-Output "Rule Collection Group $($rcg.Name) in Azure Firewall Policy $($policy.Name) found successfully."
73+
}
74+
else {
75+
Write-Output "Rule Collection Group $($azfwRCG) could not be found in Azure Policy $($azfwPolicyName), aborting"
76+
exit
77+
}
78+
}
79+
catch {
80+
Write-Output "Rule Collection Group $($azfwRCG) could not be found in Azure Policy $($azfwPolicyName), aborting"
81+
exit
82+
}
83+
try {
84+
$rc = $rcg.properties.GetRuleCollectionByName($azfwRC)
85+
Write-Output "Rule Collection $($rc.Name) in RCG $($rcg.Name) in Azure Firewall Policy $($policy.Name) found successfully with $($rc.Rules.Length) existing rules."
86+
}
87+
catch {
88+
Write-Output "Rule Collection $($azfwRC) could not be found in Rule Collection Group $($azfwRCG) in Azure Policy $($azfwPolicyName), aborting"
89+
exit
90+
}
91+
$azfwRules = $rc.Rules
92+
# Process the ALBs found in the node resource group
93+
$ALBrules = @()
94+
$ALBIPAddress = ""
95+
foreach ($ALB in $aksALBs) {
96+
if ($ALB.Name -eq "kube-apiserver") {
97+
Write-Output "System ALB $($ALB.Name) found in node resource group $($aks.NodeResourceGroup), skipping"
98+
}
99+
elseif ($null -ne $ALB.FrontendIpConfigurations[0].PrivateIpAddress) {
100+
$ALBIPAddress = $ALB.FrontendIpConfigurations[0].PrivateIpAddress
101+
Write-Output "Internal ALB $($ALB.Name) found in node resource group $($aks.NodeResourceGroup) with private IP address $($ALBIPAddress), processing rules..."
102+
$ALBrules = $ALB.LoadBalancingRules
103+
foreach ($rule in $ALBrules) {
104+
# Find the frontend IP for the rule
105+
$FrontendConfigFound = $false
106+
$FrontendIP = ""
107+
foreach ($FrontendIPConfig in $ALB.FrontendIpConfigurations) {
108+
if ($FrontendIPConfig.Id -eq $rule.FrontendIPConfiguration.Id) {
109+
$FrontendIP = $FrontendIPConfig.PrivateIpAddress
110+
$FrontendConfigFound = $true
111+
}
112+
}
113+
# Output
114+
if ($FrontendConfigFound) {
115+
Write-Output "Rule $($rule.Name) found, frontend IP is $($FrontendIP), frontend port is $($rule.FrontendPort)."
116+
# Look for an existing rule in the firewall's RC matching this ALB rule
117+
$ruleMatchFound = $false
118+
foreach ($azfwRule in $azfwRules) {
119+
if ($azfwRule.TranslatedPort -eq $rule.FrontendPort -And $azfwRule.TranslatedAddress -eq $FrontendIP) {
120+
$ruleMatchFound = $true
121+
Write-Output "Found matching rule $($azfwRule.Name) in the Azure Firewall rule collection."
122+
}
123+
}
124+
if (-Not $ruleMatchFound) {
125+
# DestinationPort = TranslatedPort ?
126+
Write-Output ("Adding rule for $($FrontendIP):$($rule.FrontendPort), since no existing rule found in the firewall")
127+
$randomSuffix = -join ($randomCharacters | Get-Random -Count $ruleSuffixLength)
128+
$newrule = New-AzFirewallPolicyNatRule -Name $($rule.Name + '-' + $randomSuffix) -Protocol "TCP" -SourceAddress "*" -DestinationAddress $azfwPublicIP -DestinationPort $rule.FrontendPort -TranslatedAddress $FrontendIP -TranslatedPort $rule.FrontendPort
129+
$rc.Rules.Add($newrule)
130+
Write-Output "Rule collection now has $($rc.Rules.Length.Length) rules." # For some reason the .Rules property is not a flat array
131+
}
132+
}
133+
else {
134+
Write-Output "Could not find frontend IP address for rule $($rule.Name), skipping."
135+
}
136+
}
137+
} else {
138+
Write-Output "Public ALB ${$ALB.Name} found in node resource group $($aks.NodeResourceGroup), skippping."
139+
}
140+
}
141+
# Go over the Firewall DNAT rules and remove anything that is not in the ALB rules
142+
$rulesToRemove = @()
143+
foreach ($azfwRule in $azfwRules) {
144+
$ruleMatchFound = $false
145+
Write-Output "Verifying whether Azure Firewall Rule $($azfwRule.Name) ($($azfwRule.TranslatedAddress):$($azfwRule.TranslatedPort)) has a corresponding rule in the AKS Load Balancer"
146+
foreach ($albRule in $ALBrules) {
147+
# Find the frontend IP for the rule
148+
$FrontendConfigFound = $false
149+
$FrontendIP = ""
150+
foreach ($FrontendIPConfig in $ALB.FrontendIpConfigurations) {
151+
if ($FrontendIPConfig.Id -eq $albRule.FrontendIPConfiguration.Id) {
152+
$FrontendIP = $FrontendIPConfig.PrivateIpAddress
153+
$FrontendConfigFound = $true
154+
}
155+
}
156+
if ($FrontendConfigFound) {
157+
if ($azfwRule.TranslatedPort -eq $albRule.FrontendPort -And $azfwRule.TranslatedAddress -eq $FrontendIP) {
158+
$ruleMatchFound = $true
159+
Write-Output "Found matching rule $($albRule.Name) ($($FrontendIP):$($albRule.FrontendPort)) in the Azure Load Balancer."
160+
} else {
161+
Write-Output "No match for ALB rule $($albRule.Name) ($($FrontendIP):$($albRule.FrontendPort))"
162+
}
163+
} else {
164+
Write-Output "Could not find frontend IP address for rule $($albRule.Name), skipping."
165+
}
166+
}
167+
if (-Not $ruleMatchFound) {
168+
Write-Output ("Removing rule $($azfwRule.Name) from rule collection, since no matching rule found in the AKS Load Balancer.")
169+
$rulesToRemove += $azfwRule.Name
170+
}
171+
else {
172+
Write-Output "Keeping rule $($azfwRule.Name), since a corresponding rule exists in the AKS Load Balancer."
173+
}
174+
}
175+
foreach ($rule in $rulesToRemove) {
176+
$rc.RemoveRuleByName($rule)
177+
}
178+
Write-Output "Rule collection now has $($rc.Rules.Length.Length) rules."
179+
# Apply changes
180+
try {
181+
Set-AzFirewallPolicyRuleCollectionGroup -Name $azfwRCG -FirewallPolicyObject $policy -Priority $rcg.Properties.Priority -RuleCollection $rc
182+
Write-Output "Firewall policy updated successfully."
183+
} catch {
184+
Write-Error "Failed to update firewall policy: $($_.Exception.Message)"
185+
}
186+

Azure Network Security - Workshop/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ These labs are a subset of the demos used by our internal teams to demonstrate t
2929
## Requirements
3030

3131
You must own or have access to an Azure subscription where you will deploy the resources used in this Workshop. While we strive to keep the materials updated, we cannot guarantee their accuracy at all times.
32+
You can create an [Azure Free Account](https://go.microsoft.com/fwlink/?linkid=2227353&clcid=0x409&l=en-us) which provides you $200 credit to get started with this lab.
3233

3334
**User, passwords and other useful resources**
3435

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
let Threshold = 3;
2+
AzureDiagnostics
3+
| where Category == "ApplicationGatewayFirewallLog"
4+
| where action_s == "Matched"
5+
| where Message has_any ("HTTP Header Injection", "HTTP Response Splitting", "HTTP Splitting", "LDAP Injection")
6+
| where ruleId_s startswith "921"
7+
| where ruleGroup_s startswith "PROTOCOL-ATTACK"
8+
| project transactionId_g, hostname_s, requestUri_s, TimeGenerated, clientIp_s, Message, details_message_s, details_data_s
9+
| join kind = inner(
10+
AzureDiagnostics
11+
| where Category =~ "ApplicationGatewayFirewallLog"
12+
| where action_s =~ "Blocked"
13+
) on transactionId_g
14+
| summarize
15+
StartTime = min(TimeGenerated),
16+
EndTime = max(TimeGenerated),
17+
TransactionID = make_set(transactionId_g, 100),
18+
Message = make_set(Message, 100),
19+
Detail_Message = make_set(details_message_s, 100),
20+
Detail_Data = make_set(details_data_s, 100),
21+
Total_TransactionId = dcount(transactionId_g)
22+
by clientIp_s, action_s
23+
| where Total_TransactionId >= Threshold
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
let Threshold = 3;
2+
AzureDiagnostics
3+
| where Category =~ "FrontDoorWebApplicationFirewallLog"
4+
| where action_s =~ "AnomalyScoring"
5+
| where details_msg_s has_any ("HTTP Header Injection", "HTTP Response Splitting", "HTTP Splitting", "LDAP Injection")
6+
| where ruleName_s has "Microsoft_DefaultRuleSet-2.1-PROTOCOL-ATTACK"
7+
| project trackingReference_s, host_s, requestUri_s, TimeGenerated, clientIP_s, details_matches_s, details_msg_s, details_data_s
8+
| join kind = inner(
9+
AzureDiagnostics
10+
| where Category =~ "FrontDoorWebApplicationFirewallLog"
11+
| where action_s =~ "Block"
12+
) on trackingReference_s
13+
| summarize
14+
StartTime = min(TimeGenerated),
15+
EndTime = max(TimeGenerated),
16+
TrackingReference = make_set(trackingReference_s, 100),
17+
Detail_Data = make_set(details_data_s, 100),
18+
Detail_Message = make_set(details_msg_s, 100),
19+
Total_TrackingReference = dcount(trackingReference_s)
20+
by clientIP_s, action_s
21+
| where Total_TrackingReference >= Threshold
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
let Threshold = 3;
2+
AGWFirewallLogs
3+
| where Action == "Matched"
4+
| where FileDetails contains "PROTOCOL-ATTACK"
5+
| where Message startswith "HTTP"
6+
| where RuleId startswith "921"
7+
| project TransactionId, Hostname, RequestUri, TimeGenerated, ClientIp, Message, DetailedMessage, DetailedData
8+
| join kind=inner (
9+
AGWFirewallLogs
10+
| where Action == "Blocked"
11+
) on TransactionId
12+
| extend Uri = strcat(Hostname, RequestUri)
13+
| summarize
14+
StartTime = min(TimeGenerated),
15+
EndTime = max(TimeGenerated),
16+
TransactionID = make_set(TransactionId, 100),
17+
Message = make_set(Message, 100),
18+
Detail_Message = make_set(DetailedMessage, 100),
19+
Detail_Data = make_set(DetailedData, 100),
20+
Total_TransactionId = dcount(TransactionId)
21+
by ClientIp, Uri, Action
22+
| where Total_TransactionId >= Threshold
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
{
2+
"properties": {
3+
"displayName": "AzureApplicationGateway-Should-be-Deployed-with-HTTPDDoSRuleset",
4+
"policyType": "Custom",
5+
"mode": "All",
6+
"description": "This policy ensures that the Application Gateway deployments are HTTP DDoS Ruleset enabled to protect against Application Layer DDoS attacks.",
7+
"metadata": {
8+
"category": ""
9+
},
10+
"version": "1.0.0",
11+
"parameters": {
12+
"effect": {
13+
"type": "String",
14+
"metadata": {
15+
"displayName": "Effect",
16+
"description": "Enable or disable the execution of the policy"
17+
},
18+
"allowedValues": [
19+
"Audit",
20+
"Deny",
21+
"Disabled"
22+
],
23+
"defaultValue": "Audit"
24+
}
25+
},
26+
"policyRule": {
27+
"if": {
28+
"allOf": [
29+
{
30+
"field": "type",
31+
"equals": "Microsoft.Network/ApplicationGatewayWebApplicationFirewallPolicies"
32+
},
33+
{
34+
"count": {
35+
"field": "Microsoft.Network/applicationGatewayWebApplicationFirewallPolicies/managedRules.managedRuleSets[*]",
36+
"where": {
37+
"field": "Microsoft.Network/applicationGatewayWebApplicationFirewallPolicies/managedRules.managedRuleSets[*].ruleSetType",
38+
"equals": "Microsoft_HTTPDDoSRuleSet"
39+
}
40+
},
41+
"less": 1
42+
}
43+
]
44+
},
45+
"then": {
46+
"effect": "[parameters('effect')]"
47+
}
48+
},
49+
"versions": [
50+
"1.0.0"
51+
]
52+
}
53+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## Application Gateway WAF should have HTTP DDoS Ruleset enabled
2+
3+
This policy mandates having HTTP DDoS Ruleset for Application Gateway WAF and has three options i.e., Audit, Deny & Disabled actions.

0 commit comments

Comments
 (0)