Skip to main content

Werner Strydom

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:

Software:

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:

  1. Configure the management interface (enp2s0) with a static IP or DHCP
  2. Enable SSH server
  3. 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.

References