Skip to content
XOP.SE
System architecture and cybersecurity. Love it when they meet.

Policy-based Micro-segmentation on pfSense

Sometimes we want to play with things without risking our entire setup. Micro-segmentation reduces the blast radius. But I struggled to find a spolution that made policy-first segmentation straightforward across wired devices, Wi-Fi, and VMs. I went with pfSense. It has the building blocks. The challenge is putting them together while keeping the policy manageable. Here is how I made it work.

So I unboxed my new router and got to work. How hard could it be?

Turns out pfSense had another point of view. And that we must think very carefully about the trinity of Identity, Policy, and Topology. Because pfSense will happily gravitate to a topology first approach.

How pfSense micro-segmentation feels

My setup

  • Router: Netgate 6100 (pfSense+)
  • WiFi: Ubiquiti wireless access point
  • NAS: Synology (running a handful of Virtual Machines)
  • Half a dozen physical computers (many over Wi-Fi)
  • Local AI server
  • A legacy music system using UPnP

Note that I do not use a switch. This is what makes micro-segmentation possible without having to configure VLANs for everything. More on this later.

The Design

Identity

For the purpose of this article, identity means an IPv4 address used in policy, associated with a device through a static DHCP mapping.

On each client-facing interface, the DHCP server of the router maps the device’s MAC address to its intended IP address. The same approach is used for physical as well as virtual interfaces (VLANs). Note that this provides consistent addressing, but not authentication.

To protect against impersonation, we must use topology (i.e. which devices connect through which interfaces) and firewall rules.

Policy

Policy membership is implemented with nested aliases on three levels:

  1. Device
  2. Group
  3. Policy

Where the policy level is used in the firewall rules. This makes the rules readable and easy to understand.

Alias nameValuesDescription
DEV_NAS192.168.1.10
DEV_MUSIC_PLAYER192.168.1.20
DEV_IPHONE_1192.168.1.30
DEV_IPHONE_2192.168.10.40
DEV_PC_NAME1192.168.2.50
DEV_VM_SYSLOG192.168.20.60
DEV_VM_SIEM192.168.20.70
GRP_PHONESDEV_IPHONE_1, DEV_IPHONE_2
POL_INTERNETGRP_PHONES, DEV_PC_NAME1Allow unlimited access to the internet
POL_MUSIC_SYSTEMDEV_NAS, DEV_MUSIC_PLAYER, DEV_IPHONE_1Allowed to engage in music playback
POL_SYSLOGDEV_NAS, DEV_PC_NAME1 …Allowed to send syslog
POL_UPDATEDEV_NAS, DEV_VM_SYSLOG, DEV_VM_SIEMAllow limited access to internet for update only. Limited ports and IP addresses

A few notes:

  • A policy alias can contain both groups and individual devices
  • All device networks use /24 prefixes, so the first three octets identify the subnet
  • If the third octet of the IP address is single digit (192.168.2.x) it indicates a physical interface while a double digit (192.168.20.x) indicates a VLAN
  • Using distinct host ID across different subnets helps if topology needs refactoring

In the firewall, policy is implemented as a set of allow (pass) rules between two sets of security rules (block).

Inter- facesProtocolSourceDestinationPortDescription
Block……………Rules for security (discussed later)
PassAnyIPv4 TCP / UDPHOME_SUBNETSThis firewall (self)53Allow DNS
PassAnyIPv4 UDPHOME_SUBNETSThis firewall (self)123Allowed NTP
PassAnyIPv4 UDPPOL_SYSLOGDEV_VM_SYSLOG514Devices allowed to send syslog
PassAnyIPv4 TCP / UDPPOL_MUSIC_ SYSTEMPOL_MUSIC_ SYSTEMMUSIC_CTRLAllowed to engage in music playback
PassAnyIPv4 TCP / UDPPOL_UPDATEUPDATE_DEST123, 443Allow limited internet access for update
PassAnyIPv4 TCP / UDPPOL_INTERNET!RFC1918_NETS*Allow full internet access
Block……………Rules for security (discussed later)

In my home setup I end up with less than 20 rules in the allow (pass) section. More importantly, the complete communication policy still fits on a single page. I can read it, understand it, and see the overall design.

Topology

The topology must support both identity and policy. For this, two things are required:

  1. Devices that need to be separated from each other must be placed on different routed interfaces or VLANs (to enforce identity)
  2. Traffic between devices that needs to be separated must cross the firewall (to enforce policy)

Fortunately, for separate routed interfaces and VLANs, 1) also gives us 2): traffic between them must cross pfSense.

In my setup there are four distinct scenarios:

  1. Physical devices that connect directly to the router. One device for one interface.
  2. Virtual machines that connect through VLANs. Here I use the virtual switch in the NAS to configure VLAN tags for the VMs.
  3. The Wi-Fi access point is configured with different virtual access points where each SSID is mapped to a different VLAN.
  4. For the legacy music system I use a bridge across a number of interfaces. The bridge forms one logical Layer-2 segment, acting much like a switch. Traffic can still be filtered by pfSense, but devices within the bridge no longer get strong identity separation from topology.

Note that not all VMs get an individual VLAN. For example the syslog collector and Elastic server (a simple SIEM) live on the same VLAN as they are both part of the same function. This is why there is no firewall rule between the collector and the SIEM: the communication between them stays in the virtual switch and is not subject to the router’s firewall.

Effectively, I trust these three components to preserve the segmentation:

  • pfSense, for routing and firewall enforcement
  • the NAS virtual switch, for VM VLAN assignment
  • the Wi-Fi AP, for wireless VLAN assignment

Plus the computer I use to administer the other three.

A compromise or misconfiguration of any of these could weaken or bypass the segmentation.

Why it works and what must be true

There are a number of things that must be configured correctly for the design to work as intended. Most of these are expressed in the firewall rules, either in the top or bottom set of security rules.

Using Floating Rules

The design uses floating rules exclusively. This keeps the complete ruleset on a single page, which makes it easier to review as one policy. To make evaluation predictable, all rules are defined as:

  • Inbound
  • Quick

Security Rules - Top set

Before evaluating the communication policy, I use a small set of security rules to establish the assumptions the design depends on. These rules are placed above the policy rules.

No IPv6

The design is based on IPv4. Rather than accidentally leaving an unsegmented IPv6 path, I disable the allow IPv6 option under System → Advanced → Networking. I also add an explicit rule to clarify intent and reduce cognitive load for myself.

Inter- facesProtocolSourceDestinationPortDescription
BlockAnyIPv6***Block anything IPv6

Explicitly block unsolicited inbound traffic from WAN

Even though pfSense blocks inbound WAN traffic by default, I add an explicit rule - again to clarify intent and reduce my cognitive load.

Inter- facesProtocolSourceDestinationPortDescription
BlockWANIPv4***Block anything initiating from WAN

Protect against spoofing of IP addresses across subnets

To support micro-segmentation we need a sufficiently trustworthy identity, because if a device can impersonate another identity it may inherit that device’s privileges.

A device can choose its own MAC address and source IP address. The design therefore cannot trust the address merely because DHCP assigned it. For each routed client interface, I explicitly block packets whose source address does not belong to the subnet permitted on that interface.

This effectively locks the device to the address range permitted on its interface. If that interface serves only one device, the topology gives us a much stronger identity boundary.

Inter- facesProtocolSourceDestinationPortDescription
BlockLAN1IPv4!LAN1 subnets**Anti spoofing
BlockLAN2IPv4!LAN2 subnets**Anti spoofing
BlockLAN3IPv4!LAN3 subnets**Anti spoofing
BlockVLAN10IPv4!VLAN10 subnets**Anti spoofing
BlockVLAN20IPv4!VLAN20 subnets**Anti spoofing

Note that this restricts devices to their assigned subnets, not to individual DHCP addresses. Addresses within the same subnet must therefore be considered interchangeable from a security perspective.

These rules should have logging enabled. A match indicates either a misconfiguration or a possible attempt at IP spoofing. Both are worth investigating.

Unfortunately, this adds some unavoidable topology-specific bulk to the top of the ruleset. The important part is that it remains isolated from the policy rules below, so the policy itself stays readable.

When adding a new interface, remember to add its source-address restriction before enabling the corresponding policy permissions.

Explicitly enable management

Managing should only be allowed through dedicated interfaces, to reduce the risk of a compromise.

Inter- facesProtocolSourceDestinationPortDescription
PassLAN1IPv4POL_MGMTThis Firewall (self)MGMT_ ROUTERAllow management of pfSense from only this interface
PassLAN1IPv4POL_MGMTDEV_NASMGMT_ NASAllow management of NAS from only this interface
BlockAnyIPv4 TCP / UDP*This Firewall (self)MGMT_ ROUTERPrevent re-enabling mgmt below
BlockAnyIPv4 TCP / UDP*DEV_NASMGMT_ NASPrevent re-enabling mgmt below

My personal preference is to disable the anti-lockout option in pfSense and have an explicit rule, leaving one place to look.

Security Rules - Bottom set

The bottom security rule set has one main purpose: block everything not explicitly allowed by the policy rules above.

Inter- facesProtocolSourceDestinationPortDescription
BlockAnyIPv4POL_NO_LOGGING_ ON_BLOCK**Silence chatty logs
BlockAnyIPv4!127.0.0.1!127.0.0.1*Block (and log) everything not explicitly allowed above

The first rule suppresses logging for known noisy traffic that is already intentionally blocked.

Logging should be enabled on the final catch-all rule. Any match means traffic occurred that was not accounted for by the policy rules above. These logs are useful both during tuning and for later monitoring.

Note: do not accidentally block pfSense itself

There is one wrinkle when using floating rules on Any: Any also includes the loopback interface (lo0). pfSense uses loopback internally; for example, by default the firewall itself uses the DNS resolver at 127.0.0.1. Although pfSense generates rules to allow loopback traffic, these IPv4 rules are not quick, so a later user-defined quick catch-all block can override them. The catch-all policy block should therefore either exclude loopback traffic or, preferably, only apply to the network interfaces covered by the policy.

In this example I have baked the necessary exception for loopback into the blocking rule itself. This is more permissive than strictly necessary. On the other hand, it saves screen space and is easy to live with, as new interfaces are automatically included in the catch-all rule.

HOME_SUBNETS and RFC1918_NETS

HOME_SUBNETS contains the subnets assigned to my internal interfaces, whereas RFC1918_NETS contains all private IPv4 address ranges.

I define the alias RFC1918_NETS as:

  • 192.168.0.0 / 16
  • 172.16.0.0 / 12
  • 10.0.0.0 / 8

These are the private IPv4 address ranges (RFC 1918). By excluding RFC1918_NETS as a destination in the Internet access rules, I prevent devices from using those permissions to access other private networks, including networks outside my own setup.

Note that this does not strictly limit traffic to the public Internet; other reserved address ranges and the firewall’s own addresses may need separate consideration.

The Bridge and legacy music system

The bridge acts much like a switch with several interfaces for the same subnet.

For this design, filtering is performed on the logical bridge interface rather than on its individual member interfaces. This requires the following two tunables:

Tunable nameDescriptionValue
net.link.bridge.pfil_memberPacket filter on the member interface0
net.link.bridge.pfil_bridgePacket filter on the bridge interface1

Devices sharing the bridge can impersonate each other’s IP addresses. However, firewall filtering still limits which services (ports) can be reached from within the subnet of the bridge. In particular, access to the NAS management interface on ports 5000–5001 is not permitted, and management is done from an interface (LAN1) outside the bridge itself.

The reason for the bridge is to cater for a legacy music system that uses UPnP.

It is important to distinguish between UPnP discovery and control, used by my music system, and UPnP IGD/PCP, which can allow devices to request dynamic port mappings on the router. The latter is known for its security issues and I recommend to keep it disabled.

Also, to keep the rule set manageable I include 239.255.255.250 in POL_MUSIC_SYSTEM and 1900 as well as 80 and 49152:65535 in the MUSIC_CTRL port alias. This makes the rule less restrictive than strictly necessary and leaves other services within that range reachable. But I consider these devices reasonably trustworthy. And for me, the additional risk is acceptable.

Firewall States

Since the design relies on interface boundaries to constrain identity, firewall states should also respect those boundaries. I use interface-bound states rather than floating states, so packets matching an existing connection cannot simply arrive through another interface.

Verify your setting of the State Policy option under System > Advanced on the Firewall & NAT tab.

Completing the firewall rules

For the example given here, the rule set becomes:

Inter- facesProtocolSourceDestinationPortDescription
BlockAnyIPv6***Block anything IPv6
BlockWANIPv4***Block anything initiating from WAN
BlockLAN1IPv4!LAN1 subnets**Anti spoofing
BlockLAN2IPv4!LAN2 subnets**Anti spoofing
BlockLAN3IPv4!LAN3 subnets**Anti spoofing
BlockVLAN10IPv4!VLAN10 subnets**Anti spoofing
BlockVLAN20IPv4!VLAN20 subnets**Anti spoofing
BlockMUSIC_ BRIDGEIPv4!MUSIC_BRIDGE subnets**Anti spoofing
PassLAN1IPv4POL_MGMTThis Firewall (self)MGMT_ ROUTERAllow management of pfSense from only this interface
PassLAN1IPv4POL_MGMTDEV_NASMGMT_ NASAllow management of NAS from only this interface
BlockAnyIPv4 TCP / UDP*This Firewall (self)MGMT_ ROUTERPrevent re-enabling mgmt below
BlockAnyIPv4 TCP / UDP*DEV_NASMGMT_ NASPrevent re-enabling mgmt below
PassAnyIPv4 TCP / UDPHOME_SUBNETSThis firewall (self)53Allow DNS
PassAnyIPv4 UDPHOME_SUBNETSThis firewall (self)123Allowed NTP
PassAnyIPv4 UDPPOL_SYSLOGDEV_VM_SYSLOG514Devices allowed to send syslog
PassAnyIPv4 TCP / UDPPOL_MUSIC_ SYSTEMPOL_MUSIC_ SYSTEMMUSIC_ CTRLAllowed to engage in music playback
PassAnyIPv4 TCP / UDPPOL_UPDATEUPDATE_DEST123, 443Allow limited internet access for update
PassAnyIPv4 TCP / UDPPOL_INTERNET!RFC1918_NETS*Allow full internet access
BlockAnyIPv4POL_NO_LOGGING_ ON_BLOCK**Silence chatty logs
BlockAnyIPv4!127.0.0.1!127.0.0.1*Block (and log) everything not explicitly allowed above

The exact rules will depend on which services your network needs. My goal is to keep the ruleset small enough that the complete policy can still be understood at a glance. Having all rules in one place helps enormously. Since misconfiguration is probably one of the more likely ways to weaken the segmentation, readability is not just a convenience. It is a security feature.

Verifying the Design

To verify the design, we need to perform a few targeted checks. I use two approaches:

  1. Inspecting the rules that pfSense generates for the underlying packet filter (PF), particularly those related to NAT.
  2. Testing that the anti-spoofing rules behave as intended and generate the expected logs.

This is not a penetration test, nor is the goal to verify the full functionality of pfSense. But we do need to establish that the assumptions our design depends on hold in practice.

Fortunately, this requires only a few targeted tests. For the anti-spoofing rules, we can use a single temporary, narrowly scoped pass rule to isolate the behaviour we want to verify and a simple test program for sending and receiving traffic.

Inspecting the Generated Rules

pfSense translates its configuration into rules for the underlying packet filter (PF). In addition to our floating rules, pfSense generates automatic firewall and NAT rules.

Some of these are evaluated before our floating rules, potentially allowing traffic that our policy would otherwise block. We therefore need to inspect the effective ruleset, paying particular attention to NAT and automatically generated permissions.

From the pfSense shell, run:

CommandExplanation
pfctl -srShow the effective firewall rules
pfctl -snShow the effective NAT rules
pfSsh.php playback pfanchordrillShow additional rules in PF anchors

We are particularly interested in:

  • Unexpected pass rules: Check for automatically generated permissions that could bypass our floating rules.
  • NAT port forwards: Look for unexpected redirects, particularly rdr pass, which can permit traffic without a separate firewall pass rule.
  • Dynamic rules: Check for rules created by services or packages, such as UPnP IGD/PCP.

Ordinary outbound NAT rules are expected, since they allow our internal devices to share the WAN address. They do not, by themselves, grant permission to initiate connections.

The goal is not to audit every internal rule generated by pfSense, but to establish that no unexpected permissions undermine our segmentation policy.

See also the Netgate documentation: Ordering of NAT and Firewall Processing

Test Cases for Anti-Spoofing

Preparation

The tests require two devices on different network segments: a sender and a receiver. A small test program repeatedly sends UDP packets to the receiver, for some tests using a spoofed source IP address.

To isolate the anti-spoofing mechanism from the ordinary communication policy, add a temporary floating pass rule immediately after the anti-spoofing rules but before the normal policy rules:

SettingValue
ActionPass
InterfaceSender’s interface
DirectionIn
QuickEnabled
ProtocolIPv4 UDP
SourceAny
DestinationReceiver’s IP address
Destination port39000
LoggingDisabled

This allows test packets to reach the receiver, provided they have not already been blocked by the anti-spoofing rules.

Before testing:

  1. Make sure logging is enabled on the anti-spoofing rule(s).
  2. Compile the test program. It is available as a downloadable C source file .
    • cc -std=c11 -O2 -Wall -Wextra -pedantic spooftest.c -o spooftest
  3. Start the receiver, listening on UDP port 39000. Ensure its local firewall permits the traffic.
    • ./spooftest receive
  4. Open the pfSense firewall logs and rule counters to observe the results.

The sender, when started, will repeatedly send traffic using a new UDP source port for each packet to avoid reusing existing firewall states.

Make sure the temporary pass rule is reached by the test traffic and that no existing firewall states interfere with the tests.

Test Case 1: Normal Communication

Action

Using the test program, send UDP packets using the sender’s assigned IP address.

  • ./spooftest send <destination-ip>
Expected results
  • Packets arrive at the receiver.
  • The temporary pass rule’s counters increase.
  • The anti-spoofing rule’s packet counters do not increase due to the test traffic.
  • No firewall logs are generated for the test traffic.

This also establishes that the test tool and network path work correctly.

Test Case 2: Cross-subnet IP spoofing

Action

Using the test program, send UDP packets with a forged source IP. Choose an address belonging to another configured client subnet, preferably one associated with a device that has additional permissions.

  • sudo ./spooftest send <destination-ip> <forged-source-ip>
Expected results
  • No packets arrive at the receiver.
  • The anti-spoofing rule’s packet counters increase.
  • Firewall logs show the blocked packets, including their forged source addresses and ingress interface.
  • The temporary pass rule’s counters do not increase due to the test traffic.

Note that a missing packet at the receiver is not sufficient evidence of successful filtering. The anti-spoofing rule’s counters and logs must confirm that the test traffic was actually blocked. If necessary, use packet capture to establish that the packets reached pfSense.

Tear Down

After testing, remove the temporary pass rule and clear any states it created. Remove test program and binary.

Repeat both tests for the different types of interfaces: physical, virtual and bridge.

Conclusions

These are the principles that made micro-segmentation on pfSense work for me. It takes careful topology design and quite a bit of discipline, but once implemented, I find it surprisingly easy to live with.

The approach has its limitations, particularly regarding device identity. But with those limitations understood and the necessary safeguards in place, I believe it provides a useful level of protection for many home networks and small offices.

Andreas Olsson