This week, I had an interesting issue to resolve--one of my customers previously hosting their architecture on-premises was utilizing geo-filtering services provided by their ISP. These geofiltering services were provided at the network layer, so filtered requests never reached the environment.
In order to respond to regionally sourced threats, my customer wanted to carry over that type of configuration into Azure. Our public guidance is for this type of security, you should seek to deploy a Next-Generation Firewall from the Azure Marketplace (https://docs.microsoft.com/en-us/azure/security-center/security-center-add-next-generation-firewall).
However, we needed a stopgap solution that wouldn't require re-architecting the Azure networking. Digging back into my IIS knowledge, I remembered being able to do IP site filtering. But could I do it at a massive scale? We're about to find out.
I started off my adventure at IPDeny.com--they compile and host IP blocks assigned at the country level. My customer's requirement was to block anything outside of the US, so I planned on a generic "Deny all" rule in IIS and then would add a the US blocks to an allow list. I downloaded just the US block (http://ipdeny.com/ipblocks/data/aggregated/us-aggregated.zone), which has just under 17,000 networks listed.
You'll need to have access to the Add-WebConfiguration and Set-WebConfigurationProperty cmdlets in order to run this code.
Then, download the appropriate country file, save it, and execute this script:
<# .SYNOPSIS Add bulk IP filtering to IIS Website #> param ( $Logfile = (Get-Date -Format yyyy-mm-dd-hh-mm) + "_" + $($MyInvocation.MyCommand) + "_.txt", $Site = "Default Web Site", $Source) #### Begin Function declaration function Write-Log([string[]]$Message, [string]$LogFile = $Script:LogFile, [switch]$ConsoleOutput, [ValidateSet("SUCCESS", "INFO", "WARN", "ERROR", "DEBUG")][string]$LogLevel) { $Message = $Message + $Input If (!$LogLevel) { $LogLevel = "INFO" } switch ($LogLevel) { SUCCESS { $Color = "Green" } INFO { $Color = "White" } WARN { $Color = "Yellow" } ERROR { $Color = "Red" } DEBUG { $Color = "Gray" } } if ($Message -ne $null -and $Message.Length -gt 0) { $TimeStamp = [System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss") if ($LogFile -ne $null -and $LogFile -ne [System.String]::Empty) { Out-File -Append -FilePath $LogFile -InputObject "[$TimeStamp] $Message" } if ($ConsoleOutput -eq $true) { Write-Host "[$TimeStamp] [$LogLevel] :: $Message" -ForegroundColor $Color } } } Function cidr { [CmdLetBinding()] Param ( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [Alias("Length")] [ValidateRange(0, 32)] $MaskLength ) Process { Return LongToDotted ([Convert]::ToUInt32($(("1" * $MaskLength).PadRight(32, "0")), 2)) } } Function LongToDotted { [CmdLetBinding()] Param ( [Parameter(Mandatory = $True, Position = 0, ValueFromPipeline = $True)] [String]$IPAddress ) Process { Switch -RegEx ($IPAddress) { "([01]{8}.){3}[01]{8}" { Return [String]::Join('.', $($IPAddress.Split('.') | ForEach-Object { [Convert]::ToUInt32($_, 2) })) } "d" { $IPAddress = [UInt32]$IPAddress $DottedIP = $(For ($i = 3; $i -gt -1; $i--) { $Remainder = $IPAddress % [Math]::Pow(256, $i) ($IPAddress - $Remainder) / [Math]::Pow(256, $i) $IPAddress = $Remainder }) Return [String]::Join('.', $DottedIP) } default { } } } } ### End Function Declaration # New-Alias appcmd.exe -Value $env:windirSystem32inetsrvappcmd.exe # Check if Elevated $wid = [system.security.principal.windowsidentity]::GetCurrent() $prp = New-Object System.Security.Principal.WindowsPrincipal($wid) $adm = [System.Security.Principal.WindowsBuiltInRole]::Administrator if ($prp.IsInRole($adm)) { Write-Log -LogFile $Logfile -LogLevel SUCCESS -ConsoleOutput -Message "Elevated PowerShell session detected. Continuing." } else { Write-Log -LogFile $Logfile -LogLevel ERROR -ConsoleOutput -Message "This application/script must be run in an elevated PowerShell window. Please launch an elevated session and try again." Break } $IPs = Get-Content $Source $count = $IPs.Count $i = 1 # Allow localhost add-webconfiguration /system.webServer/security/ipSecurity -location $Site -value @{ ipAddress = "localhost"; allowed = "true" } -pspath IIS: # Add Source IPs foreach ($IP in $IPs) { Write-Host "Processing [$i / $count] :: $IP" try { If ($IP -match "/") { $CIDR = $IP.Split("/")[1] $IPAddr = $IP.Split("/")[0] $Mask = cidr $cidr #appcmd.exe set config "$Site" -section:system.webServer/security/ipSecurity /+"[ipAddress='$($IPAddr)',subnetmask='$($Mask)',allowed='True']" /commit:apphost add-webconfiguration /system.webServer/security/ipSecurity -location $Site -value @{ ipAddress = $IPAddr; subnetMask = $Mask; allowed = "true" } -pspath IIS: -ErrorAction stop Write-Log -LogFile $Logfile -LogLevel INFO -Message "Network $IP processed." } Else { #appcmd.exe set config $Site -section:system.webServer/security/ipSecurity /+"[ipAddress='$($IP)',allowed='True']" /commit:apphost add-webconfiguration /system.webServer/security/ipSecurity -location $Site -value @{ ipAddress = $IP; allowed = "true" } -pspath IIS: -erroraction stop Write-Log -LogFile $Logfile -LogLevel INFO -Message "Address $IP processed." } } catch { $ErrorMessage = $_.Exception.Message Write-Log -Message "Error processing $IP" -LogFile $Logfile -LogLevel ERROR -ConsoleOutput Write-Log -Message $ErrorMessage -LogFile $Logfile -LogLevel ERROR } finally { $i++ } } Try { Set-WebConfigurationProperty -Filter /system.webserver.ipsecurity -name allowUnlisted -value $false -Location $Site Write-Log -LogFile $Logfile -Message "Updated web configuration property to deny unlisted IP addresses to site $($Site)." } Catch { $ErrorMessage = $_.Exception.Message $FailedItem = $_.Exception.ItemName Write-Log -Message $ErrorMessage -LogFile $Logfile -LogLevel ERROR -ConsoleOutput }
Save the script as something like IPGeofencing.ps1, and then run with the -Source parameter to point it to your downloaded list of IP addresses. I would recommend adding your local hosts and internal/external organizational networks, just to be on the safe side.