DMZ-NFT-Firewall >
Electrical-Forenics Home ray@RayFranco.com                       601.529.7473
   © Dr. Ray Franco, PhD, PE  -  208 Fairways Dr., Vicksburg, MS 39183

Updated on October 24, 2024

Simple Firewall

The first thing you need to do is enable routing on the Linux machine that will be used as a firewall [1].

Then for each network interface on the router, set up the following:

  1. IP Address
  2. Subnet Mast
  3. Default Gateway
  4. DNS Server (optional)

References:

  1. Linux-Router

Objectives

  1. Only allow NTP, DNS, HTTP and HTTPS out and through the firewall - LAN_to_Internet.
  2. Only allow return traffic into and through the firewall.
  3. Allow everything into and out of the firewall itself.

Network Diagram

                              Internet
                                  |
                                  |
                         --------------------
                         |    ISP Router    |
                         |   192.168.37.2   |
                         --------------------
                                  |
                                  |
                     Firewall     |
                         --------------------
                         |       eth0       |
                         |  192.168.37.52   |
                         |                  |
                         |    Firewall      |
                         |                  |
                         | 192.168.200.254  |
                         |   USB/eth1       |
                         -------------------
                                  |
                                  |
                      LAN         |
                        ---------------------
                        |       eth0        |
                        |   192.168.200.3   |
                        |                   |
                        |   Workstation     |
                        |                   |
                        | Default Gateway = |
                        | 192.168.200.254   |
                        ---------------------
          

Code

#!/usr/bin/nft -f

flush ruleset

table inet router {

#------------------- Acronyms and Port #'s---------------------
# NTP - Network Time Protocol - Port 123
# DNS - Domain Name Server - Port 53
# HTTP  - Hyper Text Transport Protocol - Port 80
# HTTPS - Hyer Text Transport Protocol Secure - Port 443

# ----------------- Definations and Sets ------------------------
define Internet_Interface = eth0
define LAN_Interface = eth1

set LAN_IP_Addresses {
   type ipv4_addr
   elements = {192.168.200.3}
}

# --------------------- Pre Rounting --------------------------
   chain prerouting {
       type nat hook prerouting priority filter; policy accept;
   }

# -------------- Input to the Firewall Itself-----------------
   chain input  {
       type filter hook input priority filter; policy accept;
   }

# ------------- Output to the Firewall Itself ----------------
   chain output {
       type filter hook output  priority filter; policy accept;
   }


#-------------------- Firewall Code --------------------------
#--------------------- LAN_to_Internet -----------------------
   chain LAN_to_Internet {
      # allow DNS out
      udp dport 53 ct state new counter accept
      tcp dport 53 ct state new counter accept
      # allow HTTP/HTTPS  out
      tcp dport {80, 443} ct state new counter accept
      udp dport {80, 443} ct state new counter accept
      # allow NTP out
      udp dport 123 ct state new counter accept
      # drop everything else
      counter drop
   }


# -------------------- Forward ----------------------------------
   chain forward {
       #  --- This will allow Debian update and upgrade ---
       type filter hook forward priority filter; policy drop;
       # Common Code
       ct state vmap { established:accept, related:accept, invalid:drop }
       ip protocol icmp accept

       # LAN_to_Internet
       #  Since there are only two network interfaces,
       #  it is only neccessary to test for one of them.
       # The line below will only work when Nordvpn is disabled
       oifname $Internet_Interface counter jump LAN_to_Internet
       # This line will work with Nordvpn enabled
       ip saddr @LAN_IP_Addresses counter jump LAN_to_Internet

       # Internet_to_LAN
       # Only allow return traffic in and through the firewall
       oifname $LAN_Interface counter drop
   }

# ------------------- Post Routing -----------------------------
   chain postrouting  {
       type nat  hook postrouting priority filter; policy accept;
        masquerade
   }

}
          

Modifications [1]

In the chain LAN_to_Interface, the following two statements:
  tcp dport {80, 443} ct state new counter accept
  udp dport {80, 443} ct state new counter accept
Can be replace by the single statement:
  meta l4proto { tcp, udp } th dport 53 accept

Likewise, the following two statements:
  tcp dport {80, 443} ct state new counter accept
  udp dport {80, 443} ct state new counter accept
Can be replaced by the single statement:
  meta l4proto { tcp, udp } th dport {80, 443} ct state new counter accept

References:

  1. StackExchange - How to match both UDP and TCP for given ports in one line with nftables

Red Hat - DMZ Firewall via NFTables Script

This example is from Red Hat Linux Release 8 Chapter 42. Getting started with nftables. More precisely, it is from Chapter 42.7 - Example Protecting a LAN and DMZ using an nftables Script.

42.7.1. Network Conditions

The network has the following conditions:

Security requirements to the firewall script

The following are the requirements to the nftable firewall:

Network Diagram

Red Hat did not provide a diagram; this is my work.


                                                       Internet

                                                         / \
                                                          |
                                                          |     
                                                          |              
                                     -------------------------------------------
                                    |             INT_DEV = enp1s0             |
                                    | IPv4=203.0.113.1 and IPv6=2001:db8a::1   |
                                    |                                          |
                                    |           Linx Router/Firewall           |
                                    |                                          |
                                    |    DMZ_DEV                      LAN-DEV  |
                                    |    ensp8s0                      enp7s0   |
                                    -------------------------------------------    
                             _______________|                           |_______________
       Public Addresses      |                                                         |      Private Addresses
     ======================= | ===================          ========================== | ==========================
    ||                       |                    ||      ||                           |                          ||
    ||                   ----------               ||      ||                       ----------                     ||
    ||                   | Switch |               ||      ||                       | Switch |                     ||
    ||                   ----------               ||      ||                       ----------                     ||
    ||             ________|    |_______          ||      ||         _______________|  |  |_____________          ||
    ||             |                   |          ||      ||         |                 |                |         ||
    ||     -----------------    ----------------  ||      ||   --------------    --------------    -----------    ||    
    ||     | Web Server     |   | Web Server 2 |  ||      ||   | Admin PC   |    | Admin PC   |    | More  PC |   ||  
    ||     |198.51.100.5    |   | 198.51.100.x |  ||      ||   | 10.0.0.100 |    | 10.0.0.200 |    | 10.0.0.x |   || 
    ||     |2001:db8:b::/5  |   |              |  ||      ||   --------------    --------------    ===========    || 
    ||     -----------------     --------------   ||      ||                                                      ||
    ||                                            ||      ||                                                      ||
    ||         DMZ Zone 198.51.100.0/24           ||      ||                    LAN 10.0.0.0/24                   ||                  
    ||         and  2001:db8:b::/56               ||      ||                                                      ||
     ==============================================        ========================================================                                
                             
          

Red Hat's Script

# Remove all rules
flush ruleset


# Table for both IPv4 and IPv6 rules
table inet nftables_svc {

  # Define variables for the interface name
  define INET_DEV = enp1s0
  define LAN_DEV  = enp7s0
  define DMZ_DEV  = enp8s0


  # Set with the IPv4 addresses of admin PCs
  set admin_pc_ipv4 {
    type ipv4_addr
    elements = { 10.0.0.100, 10.0.0.200 }
  }


  # Chain for incoming trafic. Default policy: drop
  chain INPUT {
    type filter hook input priority filter
    policy drop

    # Accept packets in established and related state, drop invalid packets
    ct state vmap { established:accept, related:accept, invalid:drop }

    # Accept incoming traffic on loopback interface
    iifname lo accept

    # Allow request from LAN and DMZ to local DNS server
    iifname { $LAN_DEV, $DMZ_DEV } meta l4proto { tcp, udp } th dport 53 accept

    # Allow admins PCs to access the router using SSH
    iifname $LAN_DEV ip saddr @admin_pc_ipv4 tcp dport 22 accept

    # Last action: Log blocked packets
    # (packets that were not accepted in previous rules in this chain)
    log prefix "nft drop IN : "
  }


  # Chain for outgoing traffic. Default policy: drop
  chain OUTPUT {
    type filter hook output priority filter
    policy drop

    # Accept packets in established and related state, drop invalid packets
    ct state vmap { established:accept, related:accept, invalid:drop }

    # Accept outgoing traffic on loopback interface
    oifname lo accept

    # Allow local DNS server to recursively resolve queries
    oifname $INET_DEV meta l4proto { tcp, udp } th dport 53 accept

    # Last action: Log blocked packets
    log prefix "nft drop OUT: "
  }


  # Chain for forwarding traffic. Default policy: drop
  chain FORWARD {
    type filter hook forward priority filter
    policy drop

    # Accept packets in established and related state, drop invalid packets
    ct state vmap { established:accept, related:accept, invalid:drop }

    # IPv4 access from LAN and internet to the HTTPS server in the DMZ
    iifname { $LAN_DEV, $INET_DEV } oifname $DMZ_DEV ip daddr 198.51.100.5 tcp dport 443 accept

    # IPv6 access from internet to the HTTPS server in the DMZ
    iifname $INET_DEV oifname $DMZ_DEV ip6 daddr 2001:db8:b::5 tcp dport 443 accept

    # Access from LAN and DMZ to HTTPS servers on the internet
    iifname { $LAN_DEV, $DMZ_DEV } oifname $INET_DEV tcp dport 443 accept

    # Last action: Log blocked packets
    log prefix "nft drop FWD: "
  }


  # Postrouting chain to handle SNAT
  chain postrouting {
    type nat hook postrouting priority srcnat; policy accept;

    # SNAT for IPv4 traffic from LAN to internet
    iifname $LAN_DEV oifname $INET_DEV snat ip to 203.0.113.1
  }
}

    

My Comments

The only line that I thought needed an explanation was:
 
oifname $INET_DEV meta l4proto { tcp, udp } th dport 53 accept

This single statement will work on both udp and tcp DNS queries. DNS queries general use udp; however, if all of the data will not fit in a single packet then it may use tcp. l4proto refers to level 4 protocol. Level 4 is the transport layer, and tcp and upd are level 4 protocols. The 'th" stands for transport header [1].

The one thing this example did not include was the Network Time Protocol (NAT).

References:

  1. StackExchange - How to match both UDP and TCP for given ports in one line with nftables

Classic perimetral firewall example

The following is nftables.org's example of a DMZ firewall. Nftables.org is the organization responsible for nftables.

This example assumes a classic perimetral firewall, which is connected to 3 networks: internet, DMZ, and workstation LAN.

You could either put your ruleset all in the same file or split it in different text files for better maintenance.

In this example we will use several files:


                                                       Internet

                                                         / \
                                                          |
                                                          |     
                                                          |              
                                     -------------------------------------------
                                    |                   bond0                  |
                                    |                  nic_inet                |
                                    |                                          |
                                    |           Linx Router/Firewall           |
                                    |                                          |
                                    |   nic_dmz                     nic_lan    |
                                    |   bond1                       bond2      |
                                    --------------------------------------------    
                             _____________|                         |____________
       Private Addresses     |                                                   |      Private Addresses
     ======================= | ===================          ==================== | ==================
    ||                       |                    ||      ||                     |                  ||
    ||                   ----------               ||      ||                ----------              ||
    ||                   | Swtich |               ||      ||                | Switch |              ||
    ||                   ----------               ||      ||                ----------              ||
    ||             ________|    |_______          ||      ||         _________|   |______           ||
    ||             |                   |          ||      ||         |                  |           ||
    ||     -----------------   ----------------   ||      ||   ----------------   --------------    ||    
    ||     | Web Server 1  |   |              |   ||      ||   | WorkStation1 |   |             |   ||  
    ||     |   10.0.1.2    |   | Web Server 2 |   ||      ||   | 10.0.2.2     |   | Workstation |   || 
    ||     |  fe00:1::2    |   |              |   ||      ||   | fe00:2::2    |   |             |   || 
    ||     -----------------     --------------   ||      ||   ----------------   --------------    ||
    ||                                            ||      ||                                        ||
    ||            DMZ Zone 10.0.1.0/24            ||      ||               LAN 10.0.2.0/24          ||                  
    ||             and  fe00:1::/64               ||      ||               and fe00:2::/64          ||
     ==============================================        ==========================================             
                               
                             
          

File ruleset.nft

flush ruleset

include "./defines.nft"


table inet filter {
        chain global {
                ct state established,related accept
                ct state invalid drop
                ip protocol icmp accept
                ip6 nexthdr icmpv6 accept
                udp dport 53 accept
        }

        include "./inet-filter-sets.nft"
        include "./inet-filter-forward.nft"
        include "./inet-filter-local.nft"
}
        

File defines.nft:

# interfaces
define nic_inet = bond0
define nic_dmz = bond1
define nic_lan = bond2

# network ranks
define net_ipv4_dmz = 10.0.1.0/24
define net_ipv6_dmz = fe00:1::/64
define net_ipv4_lan = 10.0.2.0/24
define net_ipv6_lan = fe00:2::/64

# some machines
define server1_ipv4 = 10.0.1.2
define server1_ipv6 = fe00:1::2
define workstation1_ipv4 = 10.0.2.2
define workstation1_ipv6 = fe00:2::2

      

File net-filter-sets.nft:

set myset_ipv4 {
         type ipv4_addr;
         elements = { $server1_ipv4 , $workstation1_ipv4 }
}

set myset_ipv6 {
         type ipv6_addr;
         elements = { $server1_ipv6 , $workstation1_ipv6 }
}
      

File inet-filter-forward.nft:

chain dmz_in {
        # your rules for traffic to your dmz servers
        ip saddr @myset_ipv4
        ip6 saddr @myset_ipv6
}

chain dmz_out {
        # your rules for traffic from the dmz to internet
}

chain lan_in {
        # your rules for traffic to your LAN nodes
}

chain lan_out {
        # your rules for traffic from the LAN to the internet
}

chain forward {
        type filter hook forward priority 0; policy drop;
        jump global
        oifname vmap { $nic_dmz : jump dmz_in , $nic_lan : jump lan_in }
        oifname $nic_inet iifname vmap { $nic_dmz : jump dmz_out , $nic_lan : jump lan_out }
}
          

File inet-filter-local.nft:

chain input {
        type filter hook input priority 0 ; policy drop;
        jump global
        # your rules for traffic to the firewall here
}

chain output {
        type filter hook output priority 0 ; policy drop;
        jump global
        # your rules for traffic originated from the firewall itself here
}
              

As all of these are plain text files, you can even think of maintaining them with some kind of control version

This page was last edited on 1 June 2018, at 13:10.

My Comments:

This is what is wrong with Linux. This organization is responsible for nftables. They do not define the problem - what is the nftable firewall susppose to do? They may have done this to make the code more general. As it is, it is just a skeleton to be completed by the user. In my opinion, this is not a primer or tutorial.

It does not seem logical to me to group IP addresses in DMZ with IP address in LAN - What were they thinking?

Both the servers in the DMZ and the workstation in LAN are in the IP private address space. Consequently, there has to be network addresses translation (NAT) in order to communicate with the Internet. Yet, there is no postrouting code to accomplish NAT in this example.

Classic Perimeter Skeleton - Filled In

Monotux.tech took the skeleton firewall provided by nftables.org and used it for his home router: nftables multi network (home) router primer.

Monotux network is more complicated than the classic DMZ firewall by the nftable.org, and he makes the same huge mistake that nftables.org did. That is he does not define the network and what is to be accomplished by the firewall.

However, in his defense, he does flush out a lot more of the details than the nftable.org. He provides the code for Network Address Translation (nat), and code for DNSSEC (Domain Name Server Security) and DoT (DNS over TLS) and MQTT (Message Queue Telemetry Transport).

Firewall Objectives

Monotux does not clearly state the objectives of his firewall. You have to read and understand his code, to try to understand what he was trying to accomplish.

Network Diagram

The diagram is my work.


                                                       
                                                         WAN
                                                         / \
                                                          |
                                                          |     
                                                          |              
                                     -------------------------------------------
                                    |                    eth0                   |
                                    |                   if_wan                  |
                                    |                                           |
                                    |           Linx Router/Firewall            |
                                    |                                           |
                                    |   if_iot        if_clients    if_services |
                                    |    eth1           eth2             eth3   |
                                    -------------------------------------------    
                    ______________________|            |                   |____________
  Private Addresses |                                  |                             |      Private Addresses
     ============== | ===========      =============================         ========= | ============
    ||              |           ||    ||                           ||       ||         |           ||
    ||         ----------       ||    ||                           ||       ||   --------------    ||
    ||         | Swtich |       ||    ||                           ||       ||   |  Server    |    ||
    ||         ----------       ||    ||                           ||       ||   |  DNS       |    ||
    ||        ___|   |___       ||    ||                           ||       ||   |  NTP       |    ||
    ||        |         |       ||    ||      Clients              ||       ||   |  MQTT      |    ||
    ||     -------   -------    ||    ||                           ||       ||   |  D-base    |    ||    
    ||     |     |   |     |    ||    ||                           ||       ||   -------------     ||  
    ||     | IoT |   | IoT |    ||    ||                           ||       ||                     || 
    ||     |     |   |     |    ||    ||                           ||       ||                     || 
    ||     -------   -------    ||    ||                           ||       ||                     ||
    ||                          ||    || net_Clients =             ||       ||                     ||
    ||        net_iot =         ||    || 198.168.254.0/24          ||       ||  net_services =     ||
    ||   198.168.255.0/24       ||    ||                           ||       ||  198.168.253.0/24   ||
     ============================      =============================         =======================
     
          

SSH to Firewall

Monotux's Code:

  # /etc/nftables.conf
  flush ruleset

  # replace these
  define if_wan = eth0
  define if_iot = eth1
  define if_clients = eth2
  define if_services = eth3

  define net_iot = 192.168.255.0/24
  define net_clients = 192.168.254.0/24
  define net_services = 192.168.253.0/24

  define host_server = 192.168.253.254

  # Covers IPv4 and IPv6
  table inet filter {
      # A named set
      set ports_mqtt {
        type inet_service; flags interval;
        elements = { 1883,8883 }
      }

      # Allow DNSSEC, HTTP(s) and DoT out from our firewall
      set firewall_out_tcp_accepted {
          type inet_service; flags interval;
          elements = { 53, 80, 443, 853 }
      }

      # Allow plain DNS & NTP from our firewall
      set firewall_out_udp_accepted {
          type inet_service; flags interval;
          elements = { 53, 123 }
      }

      # This is due to one of the quirks with netfilter (same applies
      # for iptables), you have to accept established and related
      # connections explicitly. Making it a separate chain like this
      # will allow us to quickly jump to it.
      #
      # We also allow ICMP for both v4 and v6.
      chain global {
              ct state established,related accept
              ct state invalid drop
              ip protocol icmp accept
              ip6 nexthdr icmpv6 accept
      }

      chain reject_politely {
          reject with icmp type port-unreachable
      }

      # Control what is allowed into the iot network, if any
      chain iot_in {}

      # ...and what is allowed out
      chain iot_out {
          # Accept MQTT traffic to our internal MQTT tracker
          tcp dport @ports_mqtt ip daddr $host_server ip saddr $net_iot ct state new accept
      }

      # Control what is allowed into our services network
      chain services_in {
          # Allow forwarded MQTT traffic from our IOT net to our server
          tcp dport @ports_mqtt ip daddr $host_server ip saddr $net_iot ct state new accept
      }

      # ...and control what is allowed out from our services network
      chain services_out {
          # Allow NTP out on the internet
          udp dport 123 ip saddr $net_services ct state new accept

          # Allow HTTP/HTTPS out as well, but use an anonymous set for this
          tcp dport { 80, 443 } ip saddr $net_services ct state new accept
      }

      # Nothing accepted into our clients network
      chain clients_in {}

      chain clients_out {
          # yolo
          accept
      }

      # repeat this for your subnets

      # Here's where some interesting things happen.  This is where we
      # control what is forwarded between subnets, including using the
      # chains we defined previously.  Our default policy is drop.

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

          # First accept established & related traffic, by jumping to
          # our global chain
          jump global

          # Verdict maps! This saves me _several lines_ of rules!!11
          # This could have been written line for line as well, I guess.

          # Map the output interface to a chain.  So if traffic has been
          # forwarded to this interface this is what we allow in, if
          # that makes sense?
          oifname vmap { $if_services : jump services_in,
                         $if_iot : jump iot_in,
                         $if_clients : jump clients_in }

          # If the output interface is our external, what is allowed out
          # from each subnet?
          oifname $if_wan iifname vmap { $if_services : jump services_out,
                                         $if_iot : jump iot_out,
                                         $if_clients : jump clients_out }
      }

      # Control what is allowed on our firewall
      chain incoming {
          type filter hook input priority 0; policy drop;

          jump global

          iif lo accept

          # Allow SSH but rate limit on our external interface
          iifname $if_wan tcp dport 22 ct state new flow table ssh-ftable { ip saddr limit rate 2/minute } accept

          # Allow SSH from our clients
          iifname $if_clients tcp dport 22 ct state new accept

          # Rejections should be nice
          jump reject_politely
      }

      # Control what is allowed out from our firewall itself.
      chain outgoing {
          type filter hook output priority 100; policy drop;

          jump global

          # What should be allowed out from your firewall itself? If
          # anything is acceptable, change the policy or just write:
          # accept

          # Otherwise, specify what is allowed, some examples below
          udp dport @firewall_out_udp_accepted ct state new accept
          tcp dport @firewall_out_tcp_accepted ct state new accept

          jump reject_politely
      }
  }

  # Finally, NAT!
  table ip firewall {
    chain prerouting {
      type nat hook prerouting priority 0;

      # Port forward tcp 80/443 to our internal webserver
      iifname $if_wan tcp dport { http, https } dnat to "192.168.0.100" comment "DNAT to webserver"
    }

    #### POSTROUTING
    chain postrouting {
      type nat hook postrouting priority 100;

      # Here you can specify which nets that are allowed to do NAT.  For
      # my own network I'm not allowing my IoT or management networks to
      # reach the internet.

      ip saddr $net_clients oifname $if_wan masquerade
    }
  }
      

Sometimes I want to refer to multiple interfaces or subnets in several rules, so I use more sets. The documentation is great but here are two more examples:

  set if_all_clients {
    type ifname; flags constant;
    elements = { $if_office, $if_wifi }
  }

  set net_clients {
    type ipv4_addr; flags interval;
    elements = { $net_wifi, $net_office }
  }
    

Concatenations: Now this is really pushing what is necessary, but I liked this feature and it has saved me several lines so… :-)

     # https://wiki.nftables.org/wiki-nftables/index.php/Concatenations
  set services_in_tcp_ip_port {
      type ipv4_addr . ipv4_addr . inet_service;
      elements = {
        $host_app . $host_db   . 5432,
        $host_app . $host_tsdb . 8086,
        $net_iot  . $host_mqtt . 1883
      }
    }

  chain services_in {
    ip saddr . ip daddr . tcp dport @services_in_tcp_ip_port counter ct state new accept
  }

     

So now we need one line in our services_in chain to allow our app server to talk to our database server and time series database servers, and let out IoT network talk to our MQTT server. There's something nice with this, and apparently it's also pretty fast?

Conclusions: I started writing this months ago before realizing that would end up just poorly rewriting the official nftables wiki. I do hope that the content above might come in handy for someone else, tho.

References:

  1. Wikipedia - DNS over TLS (DoT) - Port 853
  2. Wikipedia - Domain Name System Security Extensions (DNSSEC)
  3. Wikipedia - Message Queues Telimetry Transport (MQTT) for IoT Devices
  4. Techtarget.com - MQTT -(MQ Telimetry Transport)

References:

  1.