Code can be found here.
For the upcoming CCDC regionals, I'll need to manage multiple different windows machines and servers. In the state competition, I had made a simple script that created some basic firewall rules mostly targetting Active Directory services. I then applied these to both the Windows server 2012 DC and the Windows server 2016 Hyper-V/Docker machine. The rules worked fine for both, especially considering that I decided to 'reduce my threat surface' by removing the net adapters from the Docker box. However, for regionals, I'll be managing multiple Active Directories, Exchange servers, and Docker at the very least.
Having a rigid script that applies the same firewall rules to every box won't work as well as it did in state, and creating a new script for each machine is too time-consuming. So I decided to create something that could detect what services and roles were installed on a given machine and then install the necessary firewall rules based on what is detected.
It's a relatively simple script, one that still needs a lot of fine-tuning and simplification, but currently, it supports AD, Exchange, DHCP, DNS, and 'Basic' servers. Basic here is a group of rules applied to all devices to allow for basic functions such as ports 80, 443, 53, etc. The other supported services and roles have a full suite of TCP and UDP rules, as well as range block rules blocking communications on unnecessary ports. Creating range block rules isn't the best design, but serves as an extra backup layer, and allows me to free a specific range of ports at a moment's notice.
To scan the servers, I use a combination of service scanning and windows feature scanning. I also check to see if a given device is connected to a domain and if it needs to have AD communication ports open for authentication. To do this, I have the RoleCall function.
function RollCall(){ | |
$RoleCheck =@("DNS","AD","DHCP") | |
#correlates to $role switch | |
foreach($x in $RoleCheck){if(Get-WindowsFeature | where Installed | %{out-string -InputObject $_.Name} | ?{$_ -match $x}){FirewallRoles($x);if($x -eq 'AD'){$AD=$true}}} | |
#Pass installed roles to firewall creator | |
if((get-service | select-object Name, Status | %{$_.Name -match 'MSExchangeServiceHost'}) -eq 'True'){FirewallRoles('Exchange')} | |
#If true, create exchange rules | |
if(-Not $AD){if((gwmi win32_computersystem).partofdomain -eq $true){FirewallRoles('AD')}} | |
#If the device is part of a domain, create the AD rules to allow communication | |
} |
This is a simple function that utilizes the built-in get-service and get-windowsfeature commands to get a list of everything that is installed on the device. It then compares these installed services and roles to a pre-determined list of roles and services and passes the matches to the FirewallRoles function that actually creates the rules. To break it down:
foreach($x in $RoleCheck){if(Get-WindowsFeature | where Installed | %{out-string -InputObject $_.Name} | ?{$_ -match $x}){FirewallRoles($x);if($x -eq 'AD'){$AD=$true}}}
To break it down further, Get-WindowsFeature outputs a list of windows features, which include server roles installed through the server manager. This list is then filtered down with where Installed, which restricts the list to things that are actually installed on the device. However, this still outputs as an object rather than a string. So each entry on the list is passed as a windows object, which makes it difficult to compare it to a string. To make it easier to handle, we then pass the list of installed features to %{out-string -InputObject $_.Name}. This takes each Name value of the objects and turns it into a string that we can use. After converting the data to a readable format, we compare it to the list of roles we're looking for using ?{$_ -match $x}. This filters all of our string name list down to things that match what is in our RoleCheck array. After all of the filtering and comparing is done, we pass the successful values to the FirewallRoles function for rule creation. We also assign a true value to $AD if we found that the device is running an Active Directory. This allows us to skip some checks later, or to enable them depending on the value of $AD.
if((get-service | select-object Name, Status | %{$_.Name -match 'MSExchangeServiceHost'}) -eq 'True'){FirewallRoles('Exchange')} | |
This line is for service checks. It's rather simple at the moment, but will eventually be fleshed out more as more services are added to the script. At the moment, it uses get-service to generate a list of all services, stopped or running, and then uses select-object Name, Status to list only their names and status. Following this, it checks each name against MSExchangeServiceHost, which exists in Exchange servers, to see if it needs to install exchange server rules. When you run %{$_.Name -match 'MSExchangeServiceHost'} the console outputs either a 'True' or 'False' string. Thus it is necessary to then compare the output value of the command with -eq 'True' to ensure that the service exists. If it does, it sends the Exchange value to FirewallRoles. Eventually, this will check against an array like the role-based scan does, but as Exchange is the only supported service currently, it only checks for that. To change it later on, the line will be wrapped in a foreach($x in $ServiceCheck){} and will look similar to RoleCheck.
if(-Not $AD){if((gwmi win32_computersystem).partofdomain -eq $true){FirewallRoles('AD')}} | |
This line is a simple check to see if a device is connected to a domain, and thus needs to have AD authentication ports open. If(-Not $AD) uses the $AD variable that is assigned in the RoleCheck line to determine if the device is an AD server or not. If it isn't, then it will run (gwmi win32_computersystem).partofdomain -eq $true to determine if the device is joined to a domain. If it is, then it passes AD to FirewallRoles to create AD rules. This ensures that devices that do not have AD roles installed, will not be locked out of the domain by blocking necessary communication and authentication ports.
The actual firewall creation function is rather bland. It uses a switch statement to check the values passed to it, and then creates the TCP and UDP rules necessary for each service. As an example, this is the AD rule creation process.
$AD=@('88','135','138','139','389','445','464','636','3268','3269')
switch($Role){ | |
'AD'{foreach($x in $AD){New-NetFirewallrule -DisplayName "AD Port $x" -Direction Inbound -LocalPort $x -Protocol TCP -Action Allow | |
New-NetFirewallrule -DisplayName "AD Port $x (UDP)" -Direction Inbound -LocalPort $x -Protocol UDP -Action Allow | |
New-NetFirewallrule -DisplayName "AD Port $x" -Direction Outbound -LocalPort $x -Protocol TCP -Action Allow | |
New-NetFirewallrule -DisplayName "AD Port $x (UDP)" -Direction Outbound -LocalPort $x -Protocol UDP -Action Allow}} | |
For roles and services that don't need all ports open on both TCP and UDP, there are array names such as $DHCPUDP that are used for UDP rule creation. These have their own foreach() that creates the rules separately.
There is also an initialization function that turns the firewall on and creates the basic rules and range blocks. This is set up the same as the role rule creation, where $BasicRules is passed through a foreach loop, and rules are created for each given value. The range blocks are created the same way.
function FirewallInit(){ | |
Set-NetFirewallProfile -Enabled True | |
#Enable firewall | |
(New-Object -ComObject HNetCfg.FwPolicy2).RestoreLocalFirewallDefaults() | |
#Reset to defaults | |
$BasicRules =@('53','80','443') | |
$BasicRulesUDP =@('53','123') | |
foreach($x in $BasicRules){New-NetFirewallrule -DisplayName "Basic Port $x" -Direction Outbound -LocalPort $x -Protocol TCP -Action Allow} | |
foreach($x in $BasicRulesUDP){New-NetFirewallrule -DisplayName "Basic Port $x (UDP)" -Direction Outbound -LocalPort $x -Protocol UDP -Action Allow | |
New-NetFirewallrule -DisplayName "Basic Port $x (UDP)" -Direction Inbound -LocalPort $x -Protocol UDP -Action Allow} | |
We also restore the firewall to its defaults in case of tampering, something that happens frequently in the competition environment. The (New-Object -ComObject HNetCfg.FwPolicy2).RestoreLocalFirewallDefaults() line is what allows us to do a full reset. This can be tricky, however, on the off-chance that there are necessary services installed that are not caught by the script, which rely on certain ports remaining open. The reset will remove those rules and potentially interrupt those services. This can be mitigated by creating a backup of the firewall rules before running the script. A backup function was not included by default in the script because I don't believe it is necessary for most scenarios. For my typical use case, I don't need to worry about backing up rules. I typically use this script on fresh installs or devices where extraneous services are unnecessary and often unwanted. However, it is good practice to create a firewall back up with the original values if there is any concern about service uptime and connectivity. This can be done manually in the firewall manager, or through PowerShell with netsh advfirewall export "c:\advfirewallpolicy.wfw". Import is the same, netsh advfirewall import "c:advfirewallpolicy.wfw".
I think your ideas are excellent, and I am very grateful for your sharing. I have benefited enormously from them.https://www.finewaterprint.com/
ReplyDelete