Configuring a Transparent Firewall with nftables
A transparent firewall operates at Layer 2, bridging network segments while filtering traffic. Unlike a routed firewall, it requires no IP address changes on connected devices. This guide documents the configuration of a transparent firewall using Ubuntu 24.04 and nftables.
Prerequisites
Hardware:
- Multi-port network appliance (this guide uses Sophos XG 210 Rev. 3)
- Minimum 4 Ethernet ports: 1 management, 1 upstream, 2+ downstream
- Console access for initial setup
Software:
- Ubuntu 24.04 Server (minimal installation)
- systemd-networkd for network configuration
- nftables for packet filtering
Network Topology
┌─────────────────────────────────┐
│ Firewall (br0) │
│ │
┌───────────┐ │ ┌─────────┐ ┌──────────┐ │ ┌───────────┐
│ Router │───┼──│ enp3s0 │─────│ br0 │──┼───│ Downstream│
│192.0.2.1 │ │ │upstream │ │ (bridge) │ │ │ Hosts │
└───────────┘ │ └─────────┘ └──────────┘ │ └───────────┘
│ │ │
│ ┌─────────┐ ┌────┴─────┐ │
│ │ enp2s0 │ │enp4s0 │ │
│ │ mgmt │ │enp5s0 │ │
│ │192.0.2.10│ │enp6s0 │ │
│ └─────────┘ │downstream│ │
│ └──────────┘ │
└─────────────────────────────────┘
Port assignments:
| Interface | Role | Description |
|---|---|---|
| enp2s0 | Management | Out-of-band SSH access (192.0.2.10/24) |
| enp3s0 | Upstream | Connects to router/internet |
| enp4s0 | Downstream | Internal devices |
| enp5s0 | Downstream | Internal devices |
| enp6s0 | Downstream | Internal devices |
Step 1: Install Ubuntu Server
Install Ubuntu 24.04 Server with minimal packages. During installation:
- Configure the management interface (enp2s0) with a static IP or DHCP
- Enable SSH server
- Skip additional package installation
Step 2: Migrate from Netplan to systemd-networkd
Ubuntu 24.04 uses netplan by default. Before configuring the bridge, migrate to systemd-networkd for direct control over network configuration.
Create the management interface configuration on your workstation.
20-enp2s0-management.network:
[Match]
Name=enp2s0
[Network]
DHCP=yes
[Route]
Destination=192.0.2.0/24
Gateway=192.0.2.1
Upload and apply the configuration:
cat 20-enp2s0-management.network | ssh sysadmin@192.0.2.10 \
"sudo tee /etc/systemd/network/20-enp2s0-management.network"
ssh sysadmin@192.0.2.10 "sudo systemctl enable systemd-networkd"
ssh sysadmin@192.0.2.10 "sudo systemctl start systemd-networkd"
Verify the management interface has an IP address:
ssh sysadmin@192.0.2.10 "ip addr show enp2s0"
Once confirmed, remove netplan:
ssh sysadmin@192.0.2.10 "sudo rm /etc/netplan/*.yaml"
ssh sysadmin@192.0.2.10 "sudo apt remove --purge netplan.io -y"
ssh sysadmin@192.0.2.10 "sudo systemctl restart systemd-networkd"
Verify SSH still works before proceeding.
Step 3: Configure the Bridge
Create the following files on your workstation, then upload them to the firewall.
br0.netdev defines the bridge device:
[NetDev]
Name=br0
Kind=bridge
br0.network configures the bridge (no IP address for transparent mode):
[Match]
Name=br0
[Network]
enp3s0.network attaches the upstream port to the bridge:
[Match]
Name=enp3s0
[Network]
Bridge=br0
Upload the bridge configuration:
cat br0.netdev | ssh sysadmin@192.0.2.10 "sudo tee /etc/systemd/network/br0.netdev"
cat br0.network | ssh sysadmin@192.0.2.10 "sudo tee /etc/systemd/network/br0.network"
cat enp3s0.network | ssh sysadmin@192.0.2.10 "sudo tee /etc/systemd/network/enp3s0.network"
Attach downstream ports to bridge. Create a file for each downstream interface:
enp4s0.network:
[Match]
Name=enp4s0
[Network]
Bridge=br0
enp5s0.network:
[Match]
Name=enp5s0
[Network]
Bridge=br0
Create similar files for any additional downstream interfaces (enp6s0, etc.).
Upload the files to the firewall:
cat enp4s0.network | ssh sysadmin@192.0.2.10 "sudo tee /etc/systemd/network/enp4s0.network"
cat enp5s0.network | ssh sysadmin@192.0.2.10 "sudo tee /etc/systemd/network/enp5s0.network"
Restart systemd-networkd to apply the bridge configuration:
sudo systemctl restart systemd-networkd
Verify the bridge has members:
brctl show
Expected output shows all interfaces attached to br0:
bridge name bridge id STP enabled interfaces
br0 8000.020000000001 no enp3s0
enp4s0
enp5s0
enp6s0
If the interfaces column is empty, see Troubleshooting.
Step 4: Enable Bridge Netfilter
Connection tracking at the bridge level requires the br_netfilter kernel
module. Without this module, stateful rules (ct state established,related)
do not function, and return traffic (including DNS responses) gets dropped.
Create the module loading configuration on your workstation.
br_netfilter.conf:
br_netfilter
Create the sysctl configuration.
99-bridge-netfilter.conf:
net.bridge.bridge-nf-call-iptables = 0
net.bridge.bridge-nf-call-ip6tables = 1
net.bridge.bridge-nf-call-arptables = 0
Upload and apply:
cat br_netfilter.conf | ssh sysadmin@192.0.2.10 \
"sudo tee /etc/modules-load.d/br_netfilter.conf"
cat 99-bridge-netfilter.conf | ssh sysadmin@192.0.2.10 \
"sudo tee /etc/sysctl.d/99-bridge-netfilter.conf"
ssh sysadmin@192.0.2.10 "sudo modprobe br_netfilter"
ssh sysadmin@192.0.2.10 "sudo sysctl --system"
Verify the module loaded:
ssh sysadmin@192.0.2.10 "lsmod | grep br_netfilter"
Expected output includes br_netfilter and nf_conntrack_bridge.
Step 5: Configure nftables
Create the nftables configuration on your workstation.
nftables.conf:
#!/usr/sbin/nft -f
flush ruleset
table bridge filter {
define upstream = "enp3s0"
define downstream = { "enp4s0", "enp5s0", "enp6s0" }
chain forward {
type filter hook forward priority filter; policy drop;
# Allow ARP (required for Layer 2 address resolution)
ether type arp accept
# Allow established and related traffic (enables DNS, TCP, etc.)
ct state established,related accept
# Downstream to Downstream: allow internal communication
iifname $downstream oifname $downstream accept
# Downstream to Upstream: allow outbound access
iifname $downstream oifname $upstream accept
# Upstream to Downstream: allow ICMP ping for diagnostics
iifname $upstream oifname $downstream icmp type echo-request accept
}
}
table inet filter {
chain input {
type filter hook input priority filter; policy drop;
# Management interface: allow all traffic
iifname "enp2s0" accept
# Loopback: allow
iifname "lo" accept
# Established connections: allow
ct state established,related accept
# ICMP ping: allow for diagnostics
icmp type echo-request accept
}
}
Upload and apply:
cat nftables.conf | ssh sysadmin@192.0.2.10 "sudo tee /etc/nftables.conf"
ssh sysadmin@192.0.2.10 "sudo systemctl enable nftables"
ssh sysadmin@192.0.2.10 "sudo systemctl start nftables"
Step 6: Verify Configuration
Check the bridge status:
ssh sysadmin@192.0.2.10 "brctl show"
Expected output:
bridge name bridge id STP enabled interfaces
br0 8000.020000000001 no enp3s0
enp4s0
enp5s0
enp6s0
View active nftables rules:
ssh sysadmin@192.0.2.10 "sudo nft list ruleset"
Test connectivity from a downstream host (not from the firewall):
# Ping the upstream gateway
ping 192.0.2.1
# Test DNS resolution
dig example.com
Verify upstream hosts cannot initiate connections to downstream. From an upstream host, attempt to ping a downstream host:
ping 198.51.100.10 # Should timeout or be unreachable
Traffic Policy Summary
| Direction | Action | Notes |
|---|---|---|
| Downstream → Downstream | Allow | Internal communication |
| Downstream → Upstream | Allow | Internet access |
| Upstream → Downstream | Drop | Except ICMP echo-request |
| Any → Firewall (bridge) | Drop | Bridge has no IP on br0 |
| Any → Firewall (mgmt) | Allow | Via enp2s0 only |
Troubleshooting
No connectivity through the bridge
Symptom: Downstream hosts cannot reach the upstream network. DNS, ping, and all other traffic fails.
Cause: The bridge has no member interfaces. Check with bridge link show
or brctl show:
bridge link show
If no output appears, the bridge has no members attached.
Solution:
First, verify the .network files exist for each interface:
ls -la /etc/systemd/network/
Each bridge member needs a .network file with Bridge=br0. Restart
systemd-networkd after creating or fixing the files:
sudo systemctl restart systemd-networkd
bridge link show
If interfaces still do not attach, manually add them:
sudo ip link set enp3s0 up
sudo ip link set enp4s0 up
sudo ip link set enp5s0 up
sudo ip link set enp6s0 up
sudo ip link set enp3s0 master br0
sudo ip link set enp4s0 master br0
sudo ip link set enp5s0 master br0
sudo ip link set enp6s0 master br0
sudo ip link set br0 up
bridge link show
Note: Manual ip link commands do not persist across reboots. Ensure the
systemd-networkd configuration is correct for persistence. Check for
conflicting network managers (NetworkManager, netplan) that may be claiming
the interfaces before systemd-networkd can attach them to the bridge.
DNS not working from downstream hosts
Symptom: Downstream hosts can ping external IPs but DNS queries timeout.
Cause: The br_netfilter module is not loaded. Without it, connection
tracking (ct state established,related) does not function at the bridge
level. DNS queries go out, but responses are dropped by the default policy.
Solution:
sudo modprobe br_netfilter
lsmod | grep br_netfilter
Ensure /etc/modules-load.d/br_netfilter.conf exists for persistence.
Bridge traffic not being filtered
Symptom: All traffic passes through the bridge regardless of rules.
Cause: nftables is not loaded or the bridge table rules are not active.
Solution:
sudo nft list ruleset
sudo systemctl status nftables
If rules are missing, reload:
sudo nft -f /etc/nftables.conf
Cannot SSH to firewall
Symptom: SSH connection refused or timeout.
Cause: The management interface (enp2s0) may not have an IP, or the inet filter input chain is dropping traffic.
Solution:
ip addr show enp2s0
sudo nft list chain inet filter input
Ensure the management interface has the expected IP and the input chain allows traffic on that interface.