Linux NAT Firewall Setup

A NAT Firewall is needed in many situations to allow a private network to communicate with the larger world, the most common example of this would be a home WiFi router that provides a private network space and then links to a WAN connection provided by an ISP.

I was recently setting up such a system at work to allow systems on a private network to be able to communicate with the internet for software updates, etc. There is a lot of info out there to set this up, many older guides focus on iptables rules, but I wanted something that used the newer firewall-cmd software. After some googling and piecing together some things, I came up with the following script.

Firewall-cmd has several predefined ‘zones’ that control how the firewall behaves. For the NAT, I needed to set up the Internal Zone for my private network, and the External Zone for the WAN connection. The masquerade function here basically takes the outgoing traffic from the private network and makes it appear to be coming from the external network, and then pass returned traffic back to the correct system on the private side. Lastly, some iptables style rules are added as a ‘direct configuration’ to firewall-cmd to forward the traffic between these zones.

# Define interfaces for each zone, Internal Zone = LAN, External Zone = WAN

# Run the following commands on LINUX box that will act as a firewall or NAT gateway
firewall-cmd --set-default-zone=internal
firewall-cmd --get-active-zone 
firewall-cmd --change-interface=$EXTERNAL --zone=external --permanent
firewall-cmd --change-interface=$INTERNAL --zone=internal --permanent

# These next two lines may not be necessary but won't hurt...
firewall-cmd --remove-interface=$EXTERNAL --zone=public --permanent
firewall-cmd --remove-interface=$INTERNAL --zone=public --permanent

# set masquerading to internal and internal zones
firewall-cmd --zone=external --add-masquerade --permanent 
firewall-cmd --zone=internal --add-masquerade --permanent

# set direct rules to forward between zones
firewall-cmd --direct --permanent --add-rule ipv4 nat POSTROUTING 0 -o $EXTERNAL -j MASQUERADE
firewall-cmd --direct --permanent --add-rule ipv4 filter FORWARD 0 -i $INTERNAL -o $EXTERNAL -j ACCEPT
firewall-cmd --direct --permanent --add-rule ipv4 filter FORWARD 0 -i $EXTERNAL -o $INTERNAL -m state --state RELATED,ESTABLISHED -j ACCEPT
firewall-cmd --reload

# This next bit is needed for RHEL 9, Rocky 9, Fedora 35+
# create new policy to allow traffic forwarding between zones
firewall-cmd --permanent --new-policy policy_int_to_ext
firewall-cmd --permanent --policy policy_int_to_ext --add-ingress-zone internal
firewall-cmd --permanent --policy policy_int_to_ext --add-egress-zone external
firewall-cmd --permanent --policy policy_int_to_ext --set-priority 100
firewall-cmd --permanent --policy policy_int_to_ext --set-target ACCEPT
firewall-cmd --reload

That last block of code gets around a change to how firewall-cmd functions in newer systems. RHEL 8/Rocky Linux 8/Fedora 34 (and prior) didn’t require any additional work, but RHEL9 (etc) implement a new default policy that DENIES traffic forwarding between zones, so for this NAT to work we need to set up a new policy that ALLOWS traffic forwarding specifically between these zones. I was quite stumped at first when I set up my RHEL 9 system for NAT and it didn’t work as expected, and after some googling I found this thread which shed light on the issue, There may be a better solution for this down the road, but for now this additional policy will let things function as needed.