Transparent Linux Bridge Firewall with nftables: Network Control Without Changing IPs or Topology

Network tutorial - IT technology blog
Network tutorial - IT technology blog

A familiar situation: you’re asked to insert a firewall between the router and switch of a system that’s running smoothly. The client won’t allow any IP address changes, and you can’t modify the gateway on any servers. This is a problem many sysadmins face — and a transparent firewall is exactly the answer.

The Real Problem: Inserting a Firewall Without Breaking the Topology

I once ran into exactly this scenario: a production system with ~50 servers using the 192.168.10.0/24 subnet, gateway at 192.168.10.1 (the client’s router). The requirement was to control traffic between server groups and filter certain dangerous protocols — but absolutely no IP changes and no reconfiguration of any running devices.

Traditional firewalls operate at Layer 3 — they receive packets, handle routing, then forward them. That means you have to change the gateway on every machine in the network to the firewall’s IP. With 50 production servers? Nobody wants to do that during business hours, let alone deal with the downtime risk and rollback time if something goes wrong.

Root Cause Analysis: Why a Traditional Firewall Won’t Work Here

The problem is that a Layer 3 (routing-based) firewall forces you to change the topology:

  • You need two different IPs on two interfaces (one facing the LAN, one facing the uplink)
  • Every device in the network must change its default gateway to the firewall’s IP
  • DNS, monitoring, backup — everything that depends on the old gateway will be disrupted in a chain reaction

Bridge firewall (transparent firewall) operates at Layer 2 — it’s “transparent” like an ordinary switch. From the perspective of machines in the network, this firewall simply doesn’t exist. Packets pass through it without needing to know it’s there.

Technically, when you create a Linux bridge, the kernel forwards Ethernet frames between member interfaces without modifying MAC or IP addresses. nftables can hook into the bridge netfilter to inspect and filter these frames before they’re forwarded.

Approaches to the Problem

Option 1: ebtables (legacy, not recommended)

ebtables is a packet filtering tool at the bridge layer from the iptables era. The syntax is complex, it’s not unified with iptables/ip6tables, and it’s gradually being deprecated. I used it a few years ago and nothing is worse than having to maintain three separate rulesets for IPv4, IPv6, and bridge traffic.

Option 2: iptables + physdev module

Using iptables with the physdev module to match packets traversing the bridge. This works, but requires enabling several complex sysctls and the syntax is hard to remember and easy to get wrong:

# Old approach with iptables physdev — not recommended
iptables -I FORWARD -m physdev --physdev-in eth1 --physdev-out eth2 \
  -p tcp --dport 22 -j ACCEPT

Option 3: nftables bridge (modern, recommended)

nftables supports native bridge filtering starting from kernel 4.17+. A single ruleset can handle IPv4, IPv6, and Ethernet frames all at once. This is the approach I use on every new system I deploy.

The Best Approach: Deploying a Transparent Firewall with nftables

Step 1: Prepare the System

The Linux server needs at least 2 network interfaces (for example: eth0 connected to the router/uplink, eth1 connected to the internal LAN switch). Check the kernel version and load the required modules:

# Check kernel version (requires >= 4.17 for nftables bridge support)
uname -r

# Load bridge netfilter modules
modprobe br_netfilter
modprobe nft_bridge_meta

# Verify modules are loaded
lsmod | grep -E "br_netfilter|nft_bridge"

Step 2: Create the Linux Bridge

Create the bridge and add both interfaces to it. The bridge doesn’t need an IP address — but if you want to SSH into the firewall for management, you should assign a management IP:

# Create bridge br0
ip link add name br0 type bridge

# Add member interfaces to the bridge
ip link set eth0 master br0
ip link set eth1 master br0

# Bring all interfaces up
ip link set eth0 up
ip link set eth1 up
ip link set br0 up

# (Optional) Assign management IP if you need SSH access to the firewall
ip addr add 192.168.10.254/24 dev br0

# Disable STP if the topology has no loops (avoids 30s delay on startup)
ip link set br0 type bridge stp_state 0

To make this persistent across reboots, configure it via Netplan (Ubuntu 22.04+):

# /etc/netplan/01-bridge.yaml
network:
  version: 2
  ethernets:
    eth0:
      dhcp4: false
    eth1:
      dhcp4: false
  bridges:
    br0:
      interfaces: [eth0, eth1]
      addresses: [192.168.10.254/24]
      dhcp4: false
      parameters:
        stp: false
netplan apply

Step 3: Enable sysctl Settings for Bridge Netfilter

This is the most commonly overlooked step. By default, the kernel doesn’t pass bridge traffic through netfilter. You must enable this explicitly — otherwise, none of your rules will do anything:

cat > /etc/sysctl.d/99-bridge-firewall.conf << 'EOF'
net.bridge.bridge-nf-call-iptables = 1
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-arptables = 1
net.ipv4.ip_forward = 1
EOF

sysctl --system

Step 4: Write the nftables Ruleset

This is the core of the configuration. The nftables bridge family filters at Layer 2 (Ethernet frames), while the ip/ip6 family filters at Layer 3 (IP packets traversing the bridge):

cat > /etc/nftables.conf << 'EOF'
#!/usr/sbin/nft -f

flush ruleset

# Bridge table — filters at Layer 2 (Ethernet frames)
table bridge filter {
    chain forward {
        type filter hook forward priority filter; policy accept;

        # Block unexpected VLAN tags (802.1Q)
        ether type 0x8100 counter drop comment "Block unexpected VLAN tags"

        # Log ARP traffic to detect ARP spoofing
        ether type arp counter log prefix "ARP: "
    }
}

# IP table — filters at Layer 3
table ip filter {
    chain forward {
        type filter hook forward priority filter; policy drop;

        # Allow established/related connections (most important)
        ct state established,related accept
        ct state invalid drop

        # Allow ICMP with rate limiting
        ip protocol icmp limit rate 10/second accept

        # Allow SSH from the management network
        ip saddr 192.168.10.0/24 tcp dport 22 accept

        # Allow HTTP/HTTPS
        tcp dport { 80, 443 } accept

        # Allow DNS and NTP
        udp dport { 53, 123 } accept
        tcp dport 53 accept

        # Log and drop everything else
        counter log prefix "FW-DROP: " drop
    }
}

# IPv6 table
table ip6 filter {
    chain forward {
        type filter hook forward priority filter; policy drop;
        ct state established,related accept
        ct state invalid drop
        ip6 nexthdr icmpv6 accept
        counter log prefix "FW6-DROP: " drop
    }
}
EOF

# Check syntax before applying
nft -c -f /etc/nftables.conf

# Apply ruleset
nft -f /etc/nftables.conf

# Enable service to auto-load on reboot
systemctl enable --now nftables

# View the active ruleset
nft list ruleset

Step 5: Isolate Traffic Between Server Groups

This is where transparent firewall really shines — controlling traffic between machines on the same subnet without needing VLANs or any other changes. For example: only allow app servers to connect to the DB server:

# Create a named set containing app server IPs
nft add set ip filter app_servers { \
  type ipv4_addr\; \
  elements = { 192.168.10.10, 192.168.10.11, 192.168.10.12 }\; \
}

# Only app_servers are allowed to connect to the DB (192.168.10.50) on port 3306
nft add rule ip filter forward \
  ip daddr 192.168.10.50 tcp dport 3306 \
  ip saddr @app_servers accept

nft add rule ip filter forward \
  ip daddr 192.168.10.50 tcp dport 3306 drop

Step 6: Monitoring and Debugging

When first deploying, monitor the logs to make sure no legitimate traffic is being blocked. For the first week, I usually set the policy to accept with logging to observe traffic patterns before switching to drop:

# View firewall logs in real-time
journalctl -f | grep "FW-DROP:"

# View counters for each rule (to see which rules are matching the most traffic)
nft list ruleset | grep counter

# Reset counters to measure traffic over a specific time period
nft reset counters table ip filter

# Test connectivity from machines in the network
ping -c 3 8.8.8.8
curl -I https://google.com
telnet 192.168.10.50 3306

When I need to quickly calculate subnets for writing rules across different IP ranges, I often use toolcraft.app/en/tools/developer/ip-subnet-calculator — just enter a CIDR and it instantly shows the network range, broadcast address, and host count. Very handy when you need to carve up rules for multiple segments at once without doing the math by hand.

Important Considerations for Production Use

  • Performance: A bridge firewall adds processing overhead at the software layer. For typical 1Gbps links this is negligible, but above 10Gbps+ you should consider hardware offload or XDP.
  • Single point of failure: A transparent firewall is a SPOF for the entire network. In serious production environments, pair it with keepalived/VRRP for HA — so if the firewall dies, the network doesn’t go completely down.
  • VLAN trunk: If the network uses VLAN trunking (802.1Q), you need to enable VLAN filtering on the bridge and handle each VLAN separately. This is significantly more complex than a flat network.
  • Logging volume: The catch-all log rule at the end of the chain can generate a massive amount of log data during scans or attacks. Consider adding a rate limit to the logging: limit rate 5/minute log prefix "FW-DROP: ".

A transparent firewall with nftables is the cleanest solution I’ve found when you need to add a security layer without disrupting the existing topology. No need to explain to the client why they have to change their gateway, no long maintenance window — plug it in, configure it, done. And if you ever want to remove it, just detach the bridge and the network immediately returns to its original state.

Share: