Best Linux Firewall for WireGuard

When setting up a new WireGuard server on Linux, what’s the best firewall to use? We recommend using firewalld on WireGuard Endpoints, and nftables on WireGuard Gateways.

Note

Technically, behind the scenes, all Linux firewalls use the netfilter kernel subsystem — firewalld, UFW, nftables, iptables, etc are all just “front ends” to netfilter. However, each of these front ends maintain their own firewall rules separately — so if you try to use more than one on the same machine, you will often end up with conflicting rules.

Endpoints

Firewalld has a simple command-line interface, which makes it ideal for use on WireGuard endpoints — like an end-user workstation, or an internal service (like an internal webapp or fileshare). A GUI (Graphical User Interface) tool for firewalld, firewall-config, is also available, which can make firewalld’s options more discoverable:

Screenshot of Firewall Configuration GUI

User Endpoint

On an end-user workstation, you’d typically install the firewalld package from its OS (Operating System) package manager, and then use NetworkManager to assign a firewalld zone to each connection; either with the graphical NetworkManager control panel or applet, or the nmcli command-line tool.

Firewalld’s built-in public zone makes for a good default to use with a workstation’s physical network connections (like eth0 or wlan0); and its internal zone provides a good default for a workstation’s WireGuard interfaces (like wg0). These zones will prevent inbound access to each interface (except for a few commonly-used services like SSH and DHCP). As long as the endpoint always initiates WireGuard connections (as you would typically expect for an end-user device), you don’t need to adjust these rules to allow outbound WireGuard access.

You can assign NetworkManager connections to firewalld zones like the following:

$ nmcli connection show
NAME         UUID                                  TYPE       DEVICE
System eth0  1dd9a779-d327-56e1-8454-c65e2556c12c  ethernet   eth0
wg0          1c047315-acdb-4f52-9432-277ebf58c1b0  wireguard  wg0
$ sudo nmcli connection modify 'System eth0' connection.zone public
$ sudo nmcli connection modify wg0 connection.zone internal

Once you’ve assigned the zones, you can review which zones are active via the firewall-cmd --get-active-zones command, and view the settings for each zone via the firewall-cmd --info-zone=[name] command:

$ sudo firewall-cmd --get-active-zones
internal
  interfaces: wg0
public
  interfaces: eth0
$ sudo firewall-cmd --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
$ sudo firewall-cmd --info-zone=internal
internal (active)
  target: default
  icmp-block-inversion: no
  interfaces: wg0
  sources:
  services: dhcpv6-client mdns samba-client ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:

Service Endpoint

With a service endpoint, unlike an end-user endpoint, the endpoint is typically the recipient of connections, rather than the initiator. So while, like with an end-user endpoint, firewalld’s built-in public zone is a good fit for the server’s physical network connections and the internal zone provides a good fit for the server’s WireGuard interfaces, you’d typically also need to open up some additional service ports in those zones to allow inbound WireGuard access.

On a server with a WireGuard interface listening for external connections on port 51820, and a webserver listening for connections tunneled through WireGuard on port 80, you might run the following series of commands to configure firewalld:

$ sudo firewall-cmd --zone=public --add-port=51820/udp
success
$ sudo firewall-cmd --zone=public --add-interface=eth0
success
$ sudo firewall-cmd --zone=internal --add-service=http
success
$ sudo firewall-cmd --zone=internal --add-interface=wg0
success

This will prevent access to almost all of the server’s network services except through WireGuard, and prevent access even through WireGuard to all but a small set of the server’s network services. Firewalld’s public and internal zones do allow access to a few additional network services by default (like SSH and DHCP) — you can view and remove them with the following series of commands:

$ sudo firewall-cmd --get-active-zones
internal
  interfaces: wg0
public
  interfaces: eth0
$ sudo firewall-cmd --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services: dhcpv6-client ssh
  ports: 51820/udp
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
$ sudo firewall-cmd --zone=internal --remove-service=dhcpv6-client --remove-service=ssh
success
$ sudo firewall-cmd --info-zone=public
public (active)
  target: default
  icmp-block-inversion: no
  interfaces: eth0
  sources:
  services:
  ports: 51820/udp
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
$ sudo firewall-cmd --info-zone=internal
internal (active)
  target: default
  icmp-block-inversion: no
  interfaces: wg0
  sources:
  services: dhcpv6-client http mdns samba-client ssh
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
$ sudo firewall-cmd --zone=internal --remove-service=dhcpv6-client \
  --remove-service=mdns --remove-service=samba-client --remove-service=ssh
success
$ sudo firewall-cmd --info-zone=internal
internal (active)
  target: default
  icmp-block-inversion: no
  interfaces: wg0
  sources:
  services: http
  ports:
  protocols:
  forward: yes
  masquerade: no
  forward-ports:
  source-ports:
  icmp-blocks:
  rich rules:
Tip

Once you have firewalld working the way you like, save your current firewalld configuration with the following command, so that the server will automatically apply it when it boots up:

$ sudo firewall-cmd --runtime-to-permanent
success

See the How to Use WireGuard with Firewalld article for some more advanced examples of using firewalld with WireGuard.

Gateways

On WireGuard servers that forward connections to other servers, like the hub of a WireGuard hub-and-spoke network, or the site gateway in a WireGuard point-to-site network, the server’s firewalld configuration can become complicated fast. At that point, it becomes just as easy to use nftables to manage the server’s firewall as it would be to use firewalld.

Nftables is kind of like the “2.0” version of the venerable iptables firewall tool. It’s a little more complicated than iptables to do one single thing (like to set up masquerading on a network interface, or forward a particular port), but it’s a generally easier to build and manage a server’s full set of firewall rules with nftables than with iptables.

WireGuard Hub

On a WireGuard server, you’d typically install the nftables package from its OS package manager, configure its firewall ruleset via the /etc/nftables.conf file (or /etc/sysconfig/nftables.conf on Fedora-based distros), and apply the ruleset with the following command:

$ sudo systemctl restart nftables

The following nftables config file provides a good starter ruleset for a WireGuard hub:

#!/usr/sbin/nft -f
flush ruleset

define pub_iface = "eth0"
define wg_iface = "wg0"
define wg_port = 51820

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # accept all loopback packets
        iif "lo" accept
        # accept all icmp/icmpv6 packets
        ip protocol icmp accept
        ip6 nexthdr ipv6-icmp accept
        # accept all packets that are part of an already-established connection
        ct state established,related accept

        # accept all SSH packets received on a public interface
        iif $pub_iface tcp dport ssh accept
        # accept all WireGuard packets received on a public interface
        iif $pub_iface udp dport $wg_port accept

        # reject with polite "port unreachable" icmp response
        reject
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # forward all packets transiting the WireGuard network
        iifname $wg_iface oifname $wg_iface accept

        # reject with polite "host unreachable" icmp response
        reject with icmpx type host-unreachable
    }
}

It blocks all connections to the server itself except for ICMP packets (Internet Control Message Protocol, used for network diagnostics and signaling), SSH connections, and WireGuard connections; and it allows unrestricted forwarding of connections through the WireGuard tunnel to and from the other WireGuard hosts connected to it.

Tip

Once you have your nftables config working the way you like, you can load it automatically on boot with the following command:

$ sudo systemctl enable nftables

See the Hub and Spoke section of the How to Use WireGuard with Nftables article for an explanation of these rules, as well as examples of how to restrict which WireGuard connections are forwarded through the hub.

WireGuard Site Gateway

For a WireGuard server that provides access to a private site (or to the public Internet) from connected WireGuard clients, the following nftables config file makes for a good starter ruleset:

#!/usr/sbin/nft -f
flush ruleset

define pub_iface = "eth0"
define wg_iface = "wg0"
define wg_port = 51820

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        # accept all loopback packets
        iif "lo" accept
        # accept all icmp/icmpv6 packets
        ip protocol icmp accept
        ip6 nexthdr ipv6-icmp accept
        # accept all packets that are part of an already-established connection
        ct state established,related accept

        # accept all SSH packets received on a public interface
        iif $pub_iface tcp dport ssh accept
        # accept all WireGuard packets received on a public interface
        iif $pub_iface udp dport $wg_port accept

        # reject with polite "port unreachable" icmp response
        reject
    }

    chain forward {
        type filter hook forward priority 0; policy drop;

        # forward all packets that are part of an already-established connection
        ct state established,related accept
        # forward all packets from the WireGuard network to the site network
        iifname $wg_iface oif $pub_iface accept

        # reject with polite "host unreachable" icmp response
        reject with icmpx type host-unreachable
    }
}
table inet nat {
    chain postrouting {
        type nat hook postrouting priority 100; policy accept;
        # masquerade all packets from the WireGuard network to the site network
        iifname $wg_iface oif $pub_iface masquerade
    }
}

Like the WireGuard Hub example in the previous section, this ruleset blocks all connections to the server itself except for ICMP packets, SSH connections, and WireGuard connections; and it allows unrestricted forwarding of connections through the WireGuard tunnel from its WireGuard clients to the other hosts at the site. Additionally, it masquerades connections from its WireGuard clients to the site (ie substitutes its own site IP address in place of client IP addresses), ensuring that the other hosts at the site can return traffic back to the WireGuard clients without having to adjust any routing configuration at the site.

See the Point to Site section of the How to Use WireGuard with Nftables article for an explanation of these rules, as well as examples of how to restrict which WireGuard connections are forwarded to the site.