Iptables Simple Stateful Firewall for WireGuard

While I recommend using Nftables with WireGuard on new Linux servers, if you are going to use iptables instead, I’d recommend following Arch Linux’s Simple Stateful Firewall template, which provides a solid foundation to which you can add your own custom firewall rules. This guide will show you how to add firewall rules to it for a few fundamental WireGuard patterns:

Base IPv4 Ruleset

For any server in a WireGuard network, we’d start with the core IPv4 Simple Stateful Firewall ruleset (including the fw-interfaces and fw-open chains defined in section 3 of the Simple Stateful Firewall wiki page), and add two more rules — one to allow inbound SSH access, and the second to allow inbound WireGuard access:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
COMMIT

If the server is running WireGuard on a port other than 51820, adjust the port in the last rule to match its WireGuard listen port. If you don’t want to allow SSH access to the server at all, omit the second-to-last rule; or if you want to allow SSH access, but only through WireGuard, change the second-to-last rule to the following (where wg0 is the name of the server’s WireGuard interface):

-A TCP -i wg0 -p tcp --dport 22 -j ACCEPT

On Arch Linux, you’d save this ruleset as /etc/iptables/iptables.rules; on a Debian-based system with the iptables-persistent package, you’d save this ruleset as /etc/iptables/rules.v4; or on a Fedora-based system with the iptables-services package, you’d save this ruleset as /etc/sysconfig/iptables.

Apply the ruleset by running the following command as root (adjusting the path to the ruleset as appropriate for the Linux distro):

# iptables-restore < /etc/iptables/iptables.rules
Caution
If you’re accessing the server remotely through SSH, and you’ve adjusted the SSH-access rule to allow access only through WireGuard, make sure you’re currently SSHing into the server via its WireGuard IP address, and not its public IP address, before applying this ruleset — otherwise you may find you’ve locked yourself out without a working WireGuard configuration!

Base IPv6 Ruleset

For IPv6, we’d start with the core IPv6 Simple Stateful Firewall ruleset, and add the same two rules as with IPv4:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
COMMIT

On Arch Linux, you’d save this ruleset as /etc/iptables/ip6tables.rules; on a Debian-based system, you’d save this ruleset as /etc/iptables/rules.v6; or on a Fedora-based system, you’d save this ruleset as /etc/sysconfig/ip6tables.

Apply the ruleset by running the ip6tables-restore command:

# ip6tables-restore < /etc/iptables/ip6tables.rules

Point to Point

For a server running as a simple endpoint in a WireGuard network (like any point in a point-to-point network, or a spoke in a hub-and-spoke network), we’d add a rule to the TCP or UDP chains of our base rulesets for each network service we want to expose. For example, say we want to expose a webserver running on TCP port 80 to all the members of our WireGuard network; and expose VNC running on port 5900 to once specific host in the WireGuard network.

In that case, we’d add two rules to our base IPv4 ruleset:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A TCP -i wg0 -p tcp --dport 80 -j ACCEPT
-A TCP -i wg0 -p tcp --dport 5900 -s 10.1.2.3 -j ACCEPT
COMMIT

And two rules to our base IPv6 ruleset:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A TCP -i wg0 -p tcp --dport 80 -j ACCEPT
-A TCP -i wg0 -p tcp --dport 5900 -s fd10:1:2::3 -j ACCEPT
COMMIT

The first rule, -A TCP -i wg0 -p tcp --dport 80 -j ACCEPT, allows access to the server’s TCP port 80 through its WireGuard interface at wg0 to any member of the WireGuard network.

The second rule, -A TCP -i wg0 -s 10.1.2.3 -p tcp --dport 5900 -j ACCEPT in our IPv4 ruleset and -A TCP -i wg0 -s fd10:1:2::3 -p tcp --dport 5900 -j ACCEPT in our IPV6 ruleset, allows one particular host in the WireGuard network access to the server’s TCP port 5900: the host with the private WireGuard IPv4 address of 10.1.2.3, or the private WireGuard IPv6 address of fd10:1:2::3.

Hub and Spoke

For a host serving as the hub in a hub-and-spoke WireGuard network, we’d typically add a rule to to its fw-interfaces chain to allow unrestricted forwarding between connections coming in and going out the hub’s WireGuard interface (wg0).

This is what our IPv4 ruleset will look like with this rule:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o wg0 -j ACCEPT
COMMIT

And our IPv6 ruleset will have the exact same rule:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o wg0 -j ACCEPT
COMMIT

Fine-Grained Filtering With Fw-Open

However, if we wanted to do some fine-grained filtering of connections on the hub, we might instead use the fw-open chain to allow connections just to a few specific servers in the WireGuard network, and block connections to any other spokes not explicitly authorized. (With the Simple Stateful Firewall template, the fw-interfaces chain is meant to define course-grained forwarding rules based simply on which interface traffic is sent through; whereas the fw-open chain is meant to define fine-grained forwarding rules, opening up access to individual servers or ports.)

For example, if our WireGuard network includes a webserver at 10.1.2.3, a VNC server at 10.1.2.10, and a mailserver at 10.1.2.34, we might use the following IPv4 ruleset to enable access through the WireGuard network to those 3 specific servers, and deny access to everything else:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d 10.1.2.3,10.1.2.10,10.1.2.34 -j ACCEPT
COMMIT

We’d want our IPv6 ruleset to mirror this rule, using the IPv6 addresses for those 3 servers (or alternatively, include no rules for our IPv6 fw-interfaces or fw-open chains, and require that all WireGuard traffic use IPv4 addresses instead):

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d fd10:1:2::3,fd10:1:2::10,fd10:1:2::34 -j ACCEPT
COMMIT

We could even go further with these fine-grained rules, and define a separate rule for each specific server and port authorized for access (similar to the WireGuard Access Control With Itables article). For example, with the IPv4 ruleset, we might allow access only to TCP ports 80 and 443 of our webserver at 10.1.2.3, and TCP port 25 of our mailserver at 10.1.2.34; and further lock down access to TCP port 5900 of our VNC server at 10.1.2.10 so that only the spoke with a private WireGuard IP address of 10.1.2.11 can access it:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d 10.1.2.3 -p tcp --dport 80 -j ACCEPT
-A fw-open -i wg0 -d 10.1.2.3 -p tcp --dport 443 -j ACCEPT
-A fw-open -i wg0 -d 10.1.2.10 -p tcp --dport 5900 -s 10.1.2.11 -j ACCEPT
-A fw-open -i wg0 -d 10.1.2.34 -p tcp --dport 25 -j ACCEPT
COMMIT

With the IPv6 ruleset, we could do the same, but using the private IPv6 WireGuard addresses for the spokes:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d fd10:1:2::3 -p tcp --dport 80 -j ACCEPT
-A fw-open -i wg0 -d fd10:1:2::3 -p tcp --dport 443 -j ACCEPT
-A fw-open -i wg0 -d fd10:1:2::10 -p tcp --dport 5900 -s fd10:1:2::11 -j ACCEPT
-A fw-open -i wg0 -d fd10:1:2::34 -p tcp --dport 25 -j ACCEPT
COMMIT

(Or alternatively, you might omit the fw-open rules from your IPv6 ruleset, forcing all the spokes to use only IPv4 WireGuard addresses; or omit the fw-open rules from your IPv4 ruleset, and force all the spokes to use only IPv6 WireGuard addresses.)

Point to Site

For a host serving as the site gateway in a point-to-site WireGuard network, we’d typically add a rule to to its fw-interfaces chain to allow unrestricted forwarding between connections coming in its WireGuard interface (eg wg0) and going out its LAN interface (eg eth0).

This is what our IPv4 ruleset will look like with this rule:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o eth0 -j ACCEPT
COMMIT

And our IPv6 ruleset will have the exact same rule:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o eth0 -j ACCEPT
COMMIT

Fine-Grained Filtering

However, we might want to do some fine-grained filtering of connections on the site gateway, and use the fw-open chain to instead allow connections just to a few specific servers at the site, and block connections to any other hosts not explicitly authorized.

For example, if the site includes a webserver at 192.168.1.3, a VNC server at 192.168.1.10, and a mailserver at 192.168.1.34, we might use the following IPv4 ruleset to enable access through the WireGuard network to those 3 specific servers, and deny access to everything else:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d 192.168.1.3,192.168.1.10,192.168.1.34 -j ACCEPT
COMMIT

We’d want our IPv6 ruleset to mirror this rule, using the IPv6 addresses for those 3 servers (or alternatively, include no rules for our IPv6 fw-interfaces or fw-open chains, and require that all WireGuard traffic use IPv4 addresses instead):

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d fdc0:a8:1::3,fdc0:a8:1::10,fdc0:a8:1::34 -j ACCEPT
COMMIT

Point to Site with Masquerading

The above Point to Site example assumes that the LAN router at the site has been configured to route WireGuard addresses (like the 10.1.2.0/24 and fd10:1:2::/56 blocks in the example rulesets) back to the WireGuard site gateway. If that’s not the case, we’ll have to add an additional masquerading rule to the WireGuard site gateway’s rulesets.

Also, if the “site” is the Internet, and the site gateway is being used as an Internet access point for the WireGuard network (aka “Point to Internet”), then we’ll also need to add the same masquerading rule.

This masquerading rule will automatically translate the source IP address of connections forwarded from the WireGuard network to the site LAN, so that traffic from those connections on the LAN will use the LAN IP address of the gateway; and when servers at the site reply back to the gateway with return traffic (using the LAN IP address of the gateway as the destination IP address of that traffic), this rule will automatically translate back the destination IP address of the return traffic to the WireGuard IP address of the original source of the connection. (For Internet traffic, this masquerading rule should be added using the gateway’s Internet-facing interface, so as to masquerade traffic with its public IP address.)

This is what our IPv4 ruleset will look like, with a rule (in its filter table) to allow all connections incoming through its WireGuard interface (wg0) to go out its site LAN interface (eth0), plus a rule (in its nat table) to masquerade all traffic forwarded out its site LAN interface (eth0):

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o eth0 -j ACCEPT
COMMIT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT

Our IPv6 ruleset will have the same additions:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p ipv6-icmp -m icmp6 --icmpv6-type 128 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -s fe80::/10 -p ipv6-icmp -j ACCEPT
-A INPUT -p udp --sport 547 --dport 546 -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp -m tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp6-adm-prohibited
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp6-adm-prohibited
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-interfaces -i wg0 -o eth0 -j ACCEPT
COMMIT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT

You can add fine-grained filtering to these rulesets (as described in the Point to Site and Hub and Spoke sections above); if you do, be sure to put the filtering rules in the filter table, like the following IPv4 ruleset:

*filter
:INPUT DROP [0:0]
:FORWARD DROP [0:0]
:OUTPUT ACCEPT [0:0]
:TCP - [0:0]
:UDP - [0:0]
:fw-interfaces - [0:0]
:fw-open - [0:0]
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A INPUT -p icmp -m icmp --icmp-type 8 -m conntrack --ctstate NEW -j ACCEPT
-A INPUT -p udp -m conntrack --ctstate NEW -j UDP
-A INPUT -p tcp --tcp-flags FIN,SYN,RST,ACK SYN -m conntrack --ctstate NEW -j TCP
-A INPUT -p udp -j REJECT --reject-with icmp-port-unreachable
-A INPUT -p tcp -j REJECT --reject-with tcp-reset
-A INPUT -j REJECT --reject-with icmp-proto-unreachable
-A FORWARD -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -j fw-interfaces
-A FORWARD -j fw-open
-A FORWARD -j REJECT --reject-with icmp-host-unreachable
-A TCP -p tcp --dport 22 -j ACCEPT
-A UDP -p udp --dport 51820 -j ACCEPT
-A fw-open -i wg0 -d 192.168.1.3,192.168.1.10,192.168.1.34 -j ACCEPT
COMMIT
*nat
:PREROUTING ACCEPT [0:0]
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
:POSTROUTING ACCEPT [0:0]
-A POSTROUTING -o eth0 -j MASQUERADE
COMMIT