Introduction
The Windows Firewall provides an important layer of protection and a rich interface to configure it. Unfortunately, at the time of writing, the Active Directory Group Policy section for Windows Firewall with Advanced Security does not allow for the concept of item level filter, resulting in a GPO for each policy with different filtering requirements.
For example, if a handful of servers require FTP ports to be open and another group requires RDP to be allowed, you will have to create two separate GPOs.
Over time these GPOs really add up.
The solution to this is to do “Firewall as Code” using PowerShell.
Investigation
Well-behaved applications automatically register firewall rules which means that applications will happily continue to work when the firewall is enabled.
Unfortunately, some applications do not include this as part of their install process. These applications require a manual rule to be configured.
Involve your environment application owners, especially for providing information of running processes and supplying network diagrams. I recommend, however, that you do not directly ask application owners which ports an application needs. I’ve found that if you do that, you will usually get a massive list of irrelevant ports back, mainly because application owners will just give you a list of open ports found on the application servers.
Always check that ports which are required are limited to a specific process. This way you can whitelist the application, allowing all the required ports without actually worrying which protocols or ports are required.
This PowerShell function can be helpful to identify ports used by applications (adapted from http://blogs.microsoft.co.il/scriptfanatic/2011/02/10/how-to-find-running-processes-and-their-port-number/)
function Get-NetworkStatistics
{
$properties = 'Protocol','LocalAddress','LocalPort'
$properties += 'RemoteAddress','RemotePort','State','ProcessName','ProcessPath','ProcessCompany','PID'
netstat -ano | Select-String -Pattern '\s+(TCP|UDP)' | ForEach-Object {
$item = $_.line.split(" ",[System.StringSplitOptions]::RemoveEmptyEntries)
if($item[1] -notmatch '^\[::')
{
if (($la = $item[1] -as [ipaddress]).AddressFamily -eq 'InterNetworkV6')
{
$localAddress = $la.IPAddressToString
$localPort = $item[1].split('\]:')[-1]
}
else
{
$localAddress = $item[1].split(':')[0]
$localPort = $item[1].split(':')[-1]
}
if (($ra = $item[2] -as [ipaddress]).AddressFamily -eq 'InterNetworkV6')
{
$remoteAddress = $ra.IPAddressToString
$remotePort = $item[2].split('\]:')[-1]
}
else
{
$remoteAddress = $item[2].split(':')[0]
$remotePort = $item[2].split(':')[-1]
}
New-Object PSObject -Property @{
PID = $item[-1]
ProcessName = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Name
ProcessPath = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Path
ProcessCompany = (Get-Process -Id $item[-1] -ErrorAction SilentlyContinue).Company
Protocol = $item[0]
LocalAddress = $localAddress
LocalPort = $localPort
RemoteAddress =$remoteAddress
RemotePort = $remotePort
State = if($item[0] -eq 'tcp') {$item[3]} else {$null}
} | Select-Object -Property $properties
}
}
}
To get a report, run the following after importing the function
Get-NetworkStatistics | Export-Csv -NoTypeInformation .\Ports.csv
Sample export from PowerShell function
Prestage rules
The key to enabling Windows Firewall involves pre-staging firewall rules well in advance of actually flipping the switch. This is achieved with a PowerShell script, executed at an interval on all devices. Conditions within the script selectively create the appropriate rule.
For example, you only want MSSQL firewall rule on servers running MSSQL,
Create PowerShell Script
Although many options exist that would be able to achieve this, I picked PowerShell because of its popularity amongst Windows Administrators.
The PowerShell script to achieve this consists of the following concepts;
- Environmental Variables
- Global Variables
- Local Variables
- Conditions
- Rules
Environmental Variables
Specifically, to creating path conditions and rules, it is always better to use Environmental variables if the application is installed into one of these common locations.
To get these variable names, use the following PowerShell command
# List PowerShell's Environmental Variables
Get-Childitem -Path Env:* |Sort-Object Name
Global Variables
Global variables are variables that are used multiple times within the script thus, it makes sense to move these to the top of the file so that they are only evaluated once. The only downside to having multiple instances of these throughout the file is that the script will take longer to execute.
Some examples:
Computer account membership
$computerGroupMembers = ([adsisearcher]"(&(objectCategory=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.memberof -replace '^CN=([^,]+).+$','$1';
Application registry key
$SqlKey = Get-ChildItem -ErrorAction SilentlyContinue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";
Computer system information
$Computer = Get-WmiObject -Class Win32_ComputerSystem
Local Variables
When a variable is changed from one condition to another and/or needs to be re-evaluated from one condition to the next, use a local variable.
Conditions
Conditions determine if a particular rule will be applied. That said, a rule without a condition is considered a global rule because it applies to all devices
Some examples:
Domain Roles
switch -regex ($computer.DomainRole)
{
"0|1"
{ #Rules for workstations go here }
"2|3"
{ #Rules for servers go here }
"4|5"
{ #Rules for domain controllers go here }
}
Group membership
If ($computerGroupMembers -contains "Group01") { #Rules go here }
Multiple group membership
If ($computerGroupMembers -contains "Group01" -or $computerGroupMembers -contains "Group01") { #Rules go here }
Application binary exists at path using environmental variables
If (Test-Path -Path "$($Env:programfiles)\SomePath\SomeApp.exe"){ #Rules go here }
If (Test-Path -Path "$($Env:SystemDrive)\SomePath\SomeApp.exe "){ #Rules go here }
If (Test-Path -Path "$(${Env:ProgramFiles(x86)})\SomePath\SomeApp.exe") { #Rules go here }
Application binary running (notice the local variable)
$processPath = (Get-Process -Name SomeProcessName).path;
If (Test-Path -Path "$($processPath)") { #Rules go here }
Rules
Each rule name must be unique and descriptive and if applicable to a specific protocol, add the protocol to the rule name within parentheses.
Please note: PowerShell has a powerful CMDLet called New-NetFirewallRule that vastly simplifies the addition of firewall rule. Unfortunately, older versions of PowerShell do not contain it and as such, NETSH is used because it is compatible with more devices. The downside of NETSH is that we first need to delete the rule to ensure we do not create duplicate rules on subsequent executions.
Some examples:
Management range that bypass firewall completely
netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" protocol=any action=allow dir=IN remoteip=10.1.1.0/24,10.2.2.0/24,10.3.3.0/24
TCP single port rule
netsh advfirewall firewall delete rule name="FTP (TCP)"
netsh advfirewall firewall add rule name="FTP (TCP)" protocol=6 action=allow dir=IN localport="21"
TCP multiple port ranges rule
netsh advfirewall firewall delete rule name="Sample Rule Name (TCP)"
netsh advfirewall firewall add rule name="Sample Rule Name (TCP)" protocol=6 action=allow dir=IN localport="900,1000-2000,3000-4000"
TCP and UDP port range rule
netsh advfirewall firewall delete rule name="Sample Rule Name (TCP)"
netsh advfirewall firewall delete rule name="Sample Rule Name (UDP)"
netsh advfirewall firewall add rule name="Sample Rule Name (TCP)" protocol=6 action=allow dir=IN localport="1000-2000"
netsh advfirewall firewall add rule name="Sample Rule Name (UDP)" protocol=17 action=allow dir=IN localport="1000-2000"
Absolute path application rule
netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" dir=in action=allow program="C:\SomePath\SomeApp.exe"
Multiple combined environmental variable application rules
netsh advfirewall firewall delete rule name="Sample Rule Name 01"
netsh advfirewall firewall delete rule name="Sample Rule Name 02"
netsh advfirewall firewall delete rule name="Sample Rule Name 03"
netsh advfirewall firewall add rule name="Sample Rule Name 01" dir=in action=allow program="$($Env:programfiles)\SomePath\SomeApp.exe"
netsh advfirewall firewall add rule name="Sample Rule Name 02" dir=in action=allow program="$($Env:ProgramFiles(x86))\SomePath\SomeApp.exe"
netsh advfirewall firewall add rule name="Sample Rule Name 02" dir=in action=allow program="$($Env:SystemDrive)\SomePath\SomeApp.exe"
Using local variable
netsh advfirewall firewall delete rule name="Sample Rule Name"
netsh advfirewall firewall add rule name="Sample Rule Name" dir=in action=allow program="$($processPath)"
Predefined rules
netsh advfirewall firewall set rule group="remote desktop" new enable=yes
Firewall.ps1 example
$computerGroupMembers = ([adsisearcher]"(&(objectCategory=computer)(cn=$env:COMPUTERNAME))").FindOne().Properties.memberof -replace '^CN=([^,]+).+$','$1';
$SqlKey = Get-ChildItem -ErrorAction SilentlyContinue "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";
If ($computerGroupMembers -contains "FirewallAllowFTP")
{
netsh advfirewall firewall delete rule name="FTP (TCP)"
netsh advfirewall firewall add rule name="FTP (TCP)" protocol=6 action=allow dir=IN localport="21"
}
If ($computerGroupMembers -contains "FirewallAllowRDP")
{
netsh advfirewall firewall set rule group="remote desktop" new enable=yes
}
If (Test-Path -Path "$($Env:SystemDrive)\App\bin\Engine.exe")
{
netsh advfirewall firewall delete rule name="App01"
netsh advfirewall firewall add rule name="App02" dir=in action=allow program="$($Env:SystemDrive)\App\bin\App.exe"
}
If ($SqlKey -ne $null)
{
netsh advfirewall firewall delete rule name="MSSQL - SQL Server"
netsh advfirewall firewall delete rule name="MSSQL - SQL Admin Connection"
netsh advfirewall firewall delete rule name="MSSQL - SQL Database Management"
netsh advfirewall firewall delete rule name="MSSQL - SQL Service Broker"
netsh advfirewall firewall delete rule name="MSSQL - SQL Debugger/RPC"
netsh advfirewall firewall delete rule name="MSSQL - SQL Server Browse Button Service"
netsh advfirewall firewall delete rule name="MSAS - SQL Analysis Services"
netsh advfirewall firewall delete rule name="MSAS - SQL Browser"
netsh advfirewall firewall delete rule name="MSRS - HTTP"
netsh advfirewall firewall delete rule name="MSRS - SSL"
netsh advfirewall firewall add rule name="MSSQL - SQL Server" dir=in action=allow protocol=TCP localport=1433
netsh advfirewall firewall add rule name="MSSQL - SQL Admin Connection" dir=in action=allow protocol=TCP localport=1434
netsh advfirewall firewall add rule name="MSSQL - SQL Database Management" dir=in action=allow protocol=UDP localport=1434
netsh advfirewall firewall add rule name="MSSQL - SQL Service Broker" dir=in action=allow protocol=TCP localport=4022
netsh advfirewall firewall add rule name="MSSQL - SQL Debugger/RPC" dir=in action=allow protocol=TCP localport=135
netsh advfirewall firewall add rule name="MSSQL - SQL Server Browse Button Service" dir=in action=allow protocol=UDP localport=1433
netsh advfirewall firewall add rule name="MSAS - SQL Analysis Services" dir=in action=allow protocol=TCP localport=2383
netsh advfirewall firewall add rule name="MSAS - SQL Browser" dir=in action=allow protocol=TCP localport=2382
netsh advfirewall firewall add rule name="MSRS - HTTP" dir=in action=allow protocol=TCP localport=80
netsh advfirewall firewall add rule name="MSRS - SSL" dir=in action=allow protocol=TCP localport=443
$SubKeys = Get-ChildItem "HKLM:\SOFTWARE\Microsoft\Microsoft SQL Server";
ForEach ($SubKey in $SubKeys)
{
If ($SubKey.Name.Contains("\MSSQL") -And !$SubKey.Name.Contains("\MSSQLServer"))
{
$InstanceName = $SubKey.Name.Split("\")[4]
$RegistryKey = Get-ItemProperty "HKLM:$($SubKey.Name)\Setup" -name SQLBinRoot;
$EXEPath = "$($RegistryKey.SQLBinRoot)\sqlservr.exe";
netsh advfirewall firewall delete rule name="MSSQL - $($InstanceName)"
netsh advfirewall firewall add rule name="MSSQL - $($InstanceName)" dir=in action=allow program="$($EXEPath)"
}
If ($SubKey.Name.Contains("\MSAS"))
{
$InstanceName = $SubKey.Name.Split("\")[4]
$RegistryKey = Get-ItemProperty "HKLM:$($SubKey.Name)\Setup" -name SQLBinRoot;
$EXEPath = "$($RegistryKey.SQLBinRoot)\msmdsrv.exe";
netsh advfirewall firewall delete rule name="MSAS - $($InstanceName)"
netsh advfirewall firewall add rule name="MSAS - $($InstanceName)" dir=in action=allow program="$($EXEPath)"
}
}
}
Implementation
1) Copy Firewall.ps1 to shared location (\\ittelligence.com\NETLOGON\Software\Scripts in this case)
2) Deploy file via GPO Files Preferences
3) Create a scheduled task with GPO Scheduled Tasks running as NT AUTHORITY\SYSTEM
4) Specify the following as options;
Program/scripts: PowerShell.exe
Add arguments (optional): -ExecutionPolicy Bypass -File “%CommonAppdataDir%\ITtelligence\Scripts\Firewall.ps1”
5) After running this GPO for a few days do some spot checks. If rules are creating successfully, enable the firewall.