Firewalls are the frontline defense of any server or network. While many sysadmins are familiar with iptables, nftables is the modern Linux firewall framework offering more power, flexibility, and performance.
One of the key features of nftables is stateful packet inspection, which lets you track the state of network connections and write precise rules that dynamically accept or reject packets based on connection status.
In this guide, we’ll deep dive into stateful firewall rules using nftables — what they are, why they matter, and how to master them for a robust, secure network.
What is Stateful Firewalling?
A stateful firewall keeps track of all active connections passing through it. It monitors the state of a connection (new, established, related, invalid) and makes decisions based on that state rather than just static IP or port rules.
This allows:
- Legitimate traffic for existing connections to pass freely
- Blocking unexpected or invalid packets
- Better security and less manual rule writing
Understanding Connection States in nftables
nftables uses the conntrack subsystem to track connections. Common states are:
| State | Description |
|---|---|
| new | Packet is trying to establish a new connection |
| established | Packet belongs to an existing connection |
| related | Packet related to an existing connection (e.g. FTP data) |
| invalid | Packet that does not belong to any connection or is malformed |
Basic Stateful Rule Syntax in nftables
The key keyword is ct state. For example:
nft add rule inet filter input ct state established,related accept
This means: allow any incoming packets that are part of an established or related connection.
Step-by-Step: Writing a Stateful Firewall with nftables
-
Create the base table and chains
nft add table inet filternft add chain inet filter input { type filter hook input priority 0 \; }
nft add chain inet filter forward { type filter hook forward priority 0 \; }
nft add chain inet filter output { type filter hook output priority 0 \; }
-
Allow loopback traffic
nft add rule inet filter input iif lo accept
-
Allow established and related connections
nft add rule inet filter input ct state established,related accept
-
Drop invalid packets
nft add rule inet filter input ct state invalid drop
-
Allow new SSH connections
nft add rule inet filter input tcp dport ssh ct state new accept
-
Drop everything else
nft add rule inet filter input drop
Why Use Stateful Filtering?
- Avoid writing long lists of rules for each connection direction
- Automatically handle protocols with dynamic ports (e.g. FTP, SIP)
- Efficient resource usage and faster lookups
- Better security by rejecting invalid or unexpected packets
Advanced Tips for Stateful nftables Rules
- Use ct helper for protocols requiring connection tracking helpers (e.g., FTP)
- Combine ct state with interface or user match for granular control
- Use counters with rules to monitor connection states
- Rate-limit new connections using limit rate with ct state new
Real-World Example: Preventing SSH Brute Force with Stateful Rules
nft add rule inet filter input tcp dport ssh ct state new limit rate 5/minute accept
nft add rule inet filter input tcp dport ssh drop
This allows only 5 new SSH connections per minute.
Troubleshooting Stateful Rules
- Use conntrack -L to list tracked connections
- Logs can help; enable logging on dropped packets temporarily
- Check if your firewall blocks ICMP (important for some connections)
- Remember some protocols may require connection helpers
Making Your nftables Rules Permanent
By default, any rules you add using nft commands are temporary — they live in memory and are lost after a reboot.
To make your nftables rules persistent, you need to save them to a configuration file and ensure they're loaded at boot.
Option 1. Using the nftables Service (Preferred on Most Distros)
Most modern Linux distributions (Debian ≥10, Ubuntu ≥20.04, CentOS/RHEL ≥8) come with a systemd service called nftables.service that automatically loads rules from /etc/nftables.conf at boot.
Do the following to make nftables load on boot:
Dump your current rules into a file:
# nft list ruleset > /etc/nftables.conf
Enable the nftables service to load them at boot:
# systemctl enable nftables
(Optional) Start the service immediately if it’s not running:
# systemctl start nftables
Check status:
# systemctl status nftables
Now your rules will survive reboots.
Alternative way to load nftables on network UP, Use Hooks in
/etc/network/if-pre-up.d/ or Custom Scripts (Advanced)
If your distro doesn't use nftables.service or you're on a minimal setup (e.g., Alpine, Slackware, older Debian), you can load the rules manually at boot:
Save your rules:
# nft list ruleset > /etc/nftables.rules
Create a script to load them (e.g., /etc/network/if-pre-up.d/nftables):
#!/bin/sh
nft -f /etc/nftables.rules
Make it executable:
chmod +x /etc/network/if-pre-up.d/nftables
This method works on systems without systemd.
Sample /etc/nftables.conf config
We first define variables which we can use later on in our ruleset:
define NIC_NAME = "eth0"
define NIC_MAC_GW = "DE:AD:BE:EF:01:01"
define NIC_IP = "192.168.1.12"
define LOCAL_INETW = { 192.168.0.0/16 }
define LOCAL_INETWv6 = { fe80::/10 }
define DNS_SERVERS = { 1.1.1.1, 8.8.8.8 }
define NTP_SERVERS = { time1.google.com, time2.google.com, time3.google.com, time4.google.com }
define DHCP_SERVER = "192.168.1.1"
Next code block shows ip filter and ip6 filter sample:
We first create an explicit deny rule (policy drop;) for the chain input and chain output.
This means all network traffic is dropped unless its explicitly allowed later on.
Next we have to define these exceptions based on network traffic we want to allow.
Loopback network traffic is only allowed from the loopback interface and within RFC loopback network space.
nftables automatically maps network protocol names to port numbers (e.g. HTTPS 443).
In this example, we only allow incoming sessions which we initiated (ct state established accept) from ephemeral ports (dport 32768-65535). Be aware an app or web server should allow newly initiated sessions (ct state new).
Certain network sessions initiated by this host (ct state new,established accept) in the chain output are explicitly allowed in the output chain. We also allow outgoing ping requests (icmp type echo-request), but do not want others to ping this host, hence ct state established in the icmp type input chain.
table ip filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
iifname "lo" ip saddr != 127.0.0.0/8 drop
iifname $NIC_NAME ip saddr 0.0.0.0/0 ip daddr $NIC_IP tcp sport { ssh, http, https, http-alt } tcp dport 32768-65535 ct state established accept
iifname $NIC_NAME ip saddr $NTP_SERVERS ip daddr $NIC_IP udp sport ntp udp dport 32768-65535 ct state established accept
iifname $NIC_NAME ip saddr $DHCP_SERVER ip daddr $NIC_IP udp sport bootpc udp dport 32768-65535 ct state established log accept
iifname $NIC_NAME ip saddr $DNS_SERVERS ip daddr $NIC_IP udp sport domain udp dport 32768-65535 ct state established accept
iifname $NIC_NAME ip saddr $LOCAL_INETW ip daddr $NIC_IP icmp type echo-reply ct state established accept
}
chain output {
type filter hook output priority 0; policy drop;
oifname "lo" accept
oifname "lo" ip daddr != 127.0.0.0/8 drop
oifname $NIC_NAME ip daddr 0.0.0.0/0 ip saddr $NIC_IP tcp dport { ssh, http, https, http-alt } tcp sport 32768-65535 ct state new,established accept
oifname $NIC_NAME ip daddr $NTP_SERVERS ip saddr $NIC_IP udp dport ntp udp sport 32768-65535 ct state new,established accept
oifname $NIC_NAME ip daddr $DHCP_SERVER ip saddr $NIC_IP udp dport bootpc udp sport 32768-65535 ct state new,established log accept
oifname $NIC_NAME ip daddr $DNS_SERVERS ip saddr $NIC_IP udp dport domain udp sport 32768-65535 ct state new,established accept
oifname $NIC_NAME ip daddr $LOCAL_INETW ip saddr $NIC_IP icmp type echo-request ct state new,established accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
}
The next code block is used to block incoming and outgoing IPv6 traffic, except ping requests (icmpv6 type echo-request) and IPv6 network discovery (nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert).
vNICs are often automatically provisioned with IPv6 addresses and left untouched. These interfaces can be abused by malicious entities to tunnel out confidential data or even a shell.
table ip6 filter {
chain input {
type filter hook input priority 0; policy drop;
iifname "lo" accept
iifname "lo" ip6 saddr != ::1/128 drop
iifname $NIC_NAME ip6 saddr $LOCAL_INETWv6 icmpv6 type { destination-unreachable, packet-too-big, time-exceeded, parameter-problem, echo-reply, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert } ct state established accept
}
chain output {
type filter hook output priority 0; policy drop;
oifname "lo" accept
oifname "lo" ip6 daddr != ::1/128 drop
oifname $NIC_NAME ip6 daddr $LOCAL_INETWv6 icmpv6 type echo-request ct state new,established accept
}
chain forward {
type filter hook forward priority 0; policy drop;
}
}
Last code block is used for ARP traffic which limits ARP broadcast network frames:
table arp filter {
chain input {
type filter hook input priority 0; policy accept;
iif $NIC_NAME limit rate 1/second burst 2 packets accept
}
chain output {
type filter hook output priority 0; policy accept;
}
}
To load up nftables rules
# systemctl restart nftables && systemctl status nftables && nft list ruleset
Test Before Save and Apply
NB !!! Always test your rules before saving them permanently. A typo can lock you out of your server !!!
Try:
# nft flush ruleset
# nft -f /etc/nftables.conf
!!! Make sure to test your ports are truly open or closed. You can use nc, telnet or tcpdump for this. !!!
Or use a screen or tmux session and set a watchdog timer (e.g., at now +2 minutes reboot) so you can recover if something goes wrong.
Conclusion
In the ever-evolving landscape of network security, relying on static firewall rules is no longer enough. Stateful filtering with nftables gives sysadmins the intelligence and flexibility needed to deal with real-world traffic — allowing good connections, rejecting bad ones, and keeping things efficient.
With just a few lines, you can build a firewall that’s not only more secure but also easier to manage and audit over time.Whether you're protecting a personal server, a VPS, or a corporate gateway, understanding ct state is a critical step in moving from "good enough" security to proactive, intelligent defense.
If you're still relying on outdated iptables chains with hundreds of line-by-line port filters, maybe it's time to embrace the modern way.
nftables isn’t just the future — it’s the present. Further on log, monitor, and learn from your own traffic.
Start with the basics, then layer on your custom rules and monitoring and enjoy your system services and newtork being a bit more secure than before.
Cheers ! 🙂







How to install and configure Jabber Server (Ejabberd) on Debian Lenny GNU / Linux
Wednesday, December 28th, 2011I've recently installed a jabber server on one Debian Lenny server and hence decided to describe my installations steps hoping this would help ppl who would like to run their own jabber server on Debian . After some research of the jabber server softwares available, I decided to install Ejabberd
The reasons I choose Ejabberd is has rich documentation, good community around the project and the project in general looks like one of the best free software jabber servers available presently. Besides that ejabberd doesn't need Apache or MySQL and only depends on erlang programming language.
Here is the exact steps I followed to have installed and configured a running XMPP jabber server.
1. Install Ejabberd with apt
The installation of Ejabberd is standard, e.g.:
debian:~# apt-get --yes install ejabberd
Now as ejabberd is installed, some minor configuration is necessery before the server can be launched:
2. Edit /etc/ejabberd/ejabberd.cfg
Inside I changed the default settings for:
a) Uncomment%%override_acls.. Changed:
%%%% Remove the Access Control Lists before new ones are added.%%%%override_acls.to
%%
%% Remove the Access Control Lists before new ones are added.
%%
override_acls.
b) Admin User from:
%% Admin user
{acl, admin, {user, "", "example.com"}}.
to
%% Admin user
{acl, admin, {user, "admin", "jabber.myserver-host.com"}}.
c) default %% Hostname of example.com to my real hostname:
%% Hostname
{hosts, ["jabber.myserver-host.com"]}.
The rest of the configurations in /etc/ejabberd/ejabberd.cfg can stay like it is, though it is interesting to read it carefully before continuing as, there are some config timings which might prevent the XMPP server from user brute force attacks as well as few other goodies like for example (ICQ, MSN , Yahoo etc.) protocol transports.
3. Add iptables ACCEPT traffic (allow) rules for ports which are used by Ejabberd
The minimum ACCEPT rules to add are:
/sbin/iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 5222 -j ACCEPT
/sbin/iptables -A INPUT -p udp -m udp --dport 5222 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 5223 -j ACCEPT
/sbin/iptables -A INPUT -p udp -m udp --dport 5223 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 5269 -j ACCEPT
/sbin/iptables -A INPUT -p udp -m udp --dport 5269 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 5280 -j ACCEPT
/sbin/iptables -A INPUT -p udp -m udp --dport 5280 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 4369 -j ACCEPT
/sbin/iptables -A INPUT -p udp -m udp --dport 4369 -j ACCEPT
/sbin/iptables -A INPUT -p tcp -m tcp --dport 53873 -j ACCEPT
Of course if there is some specific file which stores iptables rules or some custom firewall these rules has to be added / modified to fit appropriate place or chain.
4. Restart ejabberd via init.d script
debian:~# /etc/init.d/ejabberd restart
Restarting jabber server: ejabberd is not running. Starting ejabberd.
5. Create ejabberd necessery new user accounts
debian:~# /usr/sbin/ejabberdctl register admin jabber.myserver-host.com mypasswd1
etc.debian:~# /usr/sbin/ejabberdctl register hipo jabber.myserver-host.com mypasswd2
debian:~# /usr/sbin/ejabberdctl register newuser jabber.myserver-host.com mypasswd3
debian:~# /usr/sbin/ejabberdctl register newuser1 jabber.myserver-host.com mypasswd4
...
ejabberdctl ejabberd server client (frontend) has multiple other options and the manual is a good reading.
One helpful use of ejabberdctl is:
debian:~# /usr/sbin/ejabberdctl status
Node ejabberd@debian is started. Status: started
ejabberd is running
ejabberctl can be used also to delete some existent users, for example to delete the newuser1 just added above:
debian:~# /usr/sbin/ejabberdctl unregister newuser jabber.myserver-host.com
6. Post install web configurations
ejabberd server offers a web interface listening on port 5280, to access the web interface right after it is installed I used URL: http://jabber.myserver-host.com:5280/admin/
To login to http://jabber.myserver-host.com:5280/admin/ you will need to use the admin username previously added in this case:
admin@jabber.myserver-host.com mypasswd1
Anyways in the web interface there is not much of configuration options available for change.
7. Set dns SRV records
I'm using Godaddy 's DNS for my domain so here is a screenshot on the SRV records that needs to be configured on Godaddy:
In the screenshto Target is the Fually qualified domain hostname for the jabber server.
Setting the SRV records for the domain using Godaddy's DNS could take from 24 to 48 hours to propagate the changes among all the global DNS records so be patient.
If instead you use own custom BIND DNS server the records that needs to be added to the respective domain zone file are:
_xmpp-client._tcp 900 IN SRV 5 0 5222 jabber.myserver-host.com.
_xmpp-server._tcp 900 IN SRV 5 0 5269 jabber.myserver-host.com.
_jabber._tcp 900 IN SRV 5 0 5269 jabber.myserver-host.com.
8. Testing if the SRV dns records for domain are correct
debian:~$ nslookup
> set type=SRV
> jabber.myserver-host.com
...
> myserver-host.com
If all is fine above nslookup request should return the requested domain SRV records.
You might be wondering what is the purpose of setting DNS SRV records at all, well if your jabber server has to communicate with the other jabber servers on the internet using the DNS SRV record is the way your server will found the other ones and vice versa.
DNS records can also be checked with dig for example
$ dig SRV _xmpp-server._tcp.mydomain.net
[…]
;; QUESTION SECTION:
;_xmpp-server._tcp.mydomain.net. IN SRV
;; ANSWER SECTION:
_xmpp-server._tcp.mydomain.net. 259200 IN SRV 5 0 5269 jabber.mydomain.net.
;; ADDITIONAL SECTION:
jabber.mydomain.net. 259200 IN A 11.22.33.44
;; Query time: 109 msec
;; SERVER: 212.27.40.241#53(212.27.40.241)
;; WHEN: Sat Aug 14 14:14:22 2010
;; MSG SIZE rcvd: 111
9. Debugging issues with ejabberd
Ejabberd log files are located in /var/log/ejabberd , you will have to check the logs in case of any issues with the jabber XMPP server. Here is the three files which log messages from ejabberd:
debian:~$ ls -1 /var/log/ejabberd/
ejabberd.log
erl_crash.dump
sasl.log
I will not get into details on the logs as the best way to find out about them is to read them 😉
10. Testing ejabberd server with Pidgin
To test if my Jabber server works properly I used Pidgin universal chat client . However there are plenty of other multiplatform jabber clients out there e.g.: Psi , Spark , Gajim etc.
Here is a screenshot of my (Accounts -> Manage Accounts -> Add) XMPP protocol configuration
Tags: admin, apache, best free software, brute force, cfg, com, configure, custom, default hostname, default settings, DNS, doesn, dport, ejabberd, ejabberdctl, exact steps, file, GNU, goodies, hostname, hosts, init, INPUT, Install, installation, Jabber, Linux, Lists, mypasswd, necessery, override, ports, ppl, programming language, Protocol, quot, quot quot, rich documentation, server, servers, software, tcp, transports, uncomment, User, xmpp
Posted in Linux, System Administration, Various | 8 Comments »