Windows Firewall

Another post with notes from my former to my future self.

What was I trying to do?

We had to wire up a firewall configuration policy for a group of computers so that they'd be prevented from accessing the internet. Ok, no problem, except that our email comes from Office 365 which happens to be on the internet.

Microsoft is nice enough to publish a list of IP addresses that are used by Office 365 services and bonus - it looks like its updated pretty frequently!

The path to get there.

Bad fireall assumption

It was a bit windy. It started with thinking that the Windows firewall was "allow by default", which meant that I would need to build a blocklist for the internet, with holes in ranges. So, if I wanted to allow the client access to 192.168.1.1 - 192.168.1.254, I would need two rules to block 0.0.0.0 - 192.168.0.255 and 192.168.2.0 - 255.255.255.255. This was going to create the worlds most complex set of rules, but I figured it's all just byte math - it could be programmed.

The poor assumption here: The firewall can be configured as "block by default", with rules defining allow actions.

A few self-imposed blocks later (it's a bad idea to NOT leave a hole out to your DC's to get new policy definitions), this was sorted.

Editing a .POL file manually

A .POL file is the binary file that effectively backs the registry. The Windows firewall rules are set as a bunch of registry keys that are really just a string of text. So, let's just edit the .POL file with something like PolicyFileEditor Powershell module, and we can fabricate the policy. All it needs to do is look like:

1ValueName : {588a55ba-4dff-48ba-adc8-fcd0406535c0}
2Key       : SOFTWARE\Policies\Microsoft\WindowsFirewall\FirewallRules
3Data      : v2.30|Action=Allow|Active=TRUE|Dir=Out|RA4=13.107.136.0/255.255.252.0|RA4=40.108.128.0/255.255.128.0|RA4=52.104.0.0/255.252.0.0|RA4=104.146.128.0/255.255.128.0|RA4=150.171.40.0/255.255.252.0|RA6=2603:1061:1300::/40|RA6=2620:1ec:8f8::/46|RA6=2620:1ec:908::/46|RA6=2a01:111:f402::/48|Name=SharePoint Online and OneDrive for Business|
4Type      : String

So.. the Data property has some version information, direction, active flag, a list of rule entries, a name and so on. Ez!

I don't really like the idea if tinkering directly with the registry imports in a GPO, but would work. What could go wrong?

Editing the Firewall policy in a Group Policy directly

The NetSecurity module that I had installed (not sure where it came from, to be honest?) gave me some much easier access to the firewall policies within the GPO.

Some extensive use of New-NetFirewallRule later, we have a working script to make this policy management so much easier. Breaking it down:

Define some custom rules that I will want to add

These will use the specific parameter names for New-NetFirewallRule so I can just splat it later.

1$customRules = @(
2    @{"DisplayName" = "Block access to internal applications"; Direction = "Outbound"; "Action" = "Block"; "RemoteAddress" = @("host1IP","host2IP","host3IP") }
3    @{"DisplayName" = "Allow connections to internal computers"; Direction = "Outbound"; "Action" = "Allow"; "RemoteAddress" = @("start1-end1","start2-end2") }
4    @{"DisplayName" = "Allow connections to website"; Direction = "Outbound"; "Action" = "Allow"; "RemoteAddress" = @("host4IP") }
5)

Meat and potatoes of the whole thing

The "main" function (my stupid naming convention, carried in from C/C++/Java, which REALLY breaks down in Powershell land where I really should be writing cmdlets)

  • sets a few variables
  • opens the GPO from the domain
  • deletes rules that have a group name that I've defined (lazy way of avoiding having to update a policy.. I'll just recreate it)
  • add the custom rules defined above, tagged with the group name
  • get the service name / IP addresses, using the function below main()
  • for each service name / IP address set, add a rule matching the service name and IPs found
  • save the policy.

I bumped into a small annoyance where the cmdlet would not take an ArrayList of items, instead was asking for String[]. Not too hard to work around, but annoying all the same.

 1function main() {
 2    $domain = "contoso.com"
 3    $GpoName = "Firewall: Enable firewall restrictions"
 4    $PolicyStore = "$Domain\$GpoName"
 5
 6    $GpoSession = open-netGPO -policystore $PolicyStore
 7
 8    # Flush existing  rules.
 9    try {
10        Remove-NetFirewallRule -GPOSession $GpoSession -Group "CustomRuleSet" | out-null
11    } catch {}
12
13    # Insert new custom rules
14    foreach ($rule in $customRules) {
15        # The @rule notation is the splat.
16        New-NetFirewallRule -GPOSession $gpoSession -Group "CustomRuleSet" @rule
17    }
18
19    # Get and insert Office 365 rules
20    $IPs = Get-Office365IPAddresses
21    foreach ($ip in $IPs) {
22        # Ugh. Converting this arraylist into a array of string is stupid.
23        $addresses = @()
24        foreach ($address in $ip.Ips) {
25            $addresses += [String]($address)
26        }
27        $StringOfIPs = $($ip.Ips -join [Environment]::NewLine)
28        New-NetFirewallRule -GPOSession $GpoSession -Group "CustomRuleSet" -DisplayName $ip.Name -Direction "Outbound" -Action "Allow" -RemoteAddress $addresses
29    }
30    Save-netGPO -GPOSession $GpoSession
31}

Get Office 365 endpoints

This returns an array of objects, each object having a name (of the broader service name) and IP addresses.

 1function Get-Office365IPAddresses() {
 2    $JSONEndpoint = "https://endpoints.office.com/endpoints/worldwide?clientrequestid=b10c5ed1-bad1-445f-b386-b919946339a7"
 3    $webData = invoke-webrequest -uri $JSONEndpoint
 4    $content = convertfrom-json $webData
 5
 6    $returnArray = [System.Collections.ArrayList]@()
 7
 8    $serviceAreaGroups = $content | group-object serviceAreaDisplayName
 9    foreach ($group in $serviceAreaGroups) {
10        $allIps = [System.Collections.ArrayList]@()
11        foreach ($groupIps in $group.Group) {
12            if ($null -ne $GroupIps.ips) {
13                $allIps.AddRange($GroupIps.ips) | out-null
14            }
15        }
16        $returnArray.Add(
17            [PSCustomObject]@{
18                Name = $Group.Name;
19                IPs = $allIps
20            }
21        ) | out-null
22    }
23
24    return $returnArray
25}

Final ..

In the end, the GPO works as desired. It arguably needs more tuning, since the affected computers don't need access to the whole internal IP range.