rss logo

Secure SSH on Linux with nftables Firewall

OpenSSH logo with a red stamp 'Protected by nftables'

In a previous article, I explained how to secure SSH with two-factor authentication (2FA). In addition to 2FA, you can strengthen SSH security by using the netfilter firewall, managed with nftables. This approach allows you to restrict access so that only trusted IP addresses can connect to your Linux server. Both protections, 2FA and nftables, are complementary and can be combined for maximum security.

Setting up nftables rules for SSH is relatively simple, so there is no reason not to implement them.

This method is especially useful if only a few known IP addresses should be allowed to connect. You can also apply Geo-Filtering, restricting access by country or region using the IPdeny IP address lists as we will see in the last part of this tutorial.

Allow SSH Access with an IP Whitelist

For example, imagine we only want to allow access from our local network (192.168.0.0/24) and two specific public IP addresses: 198.51.100.51 and 2001:db8:51d:dead:bee5::1. All other IPs will be blocked.

SSH Whitelist Firewall Diagram

Diagram of Linux nftables firewall protecting an SSH server, blocking untrusted IPs and allowing valid users
nftables firewall allowing SSH only from trusted IP addresses while blocking all others.

Configuring the /etc/nftables.conf File for SSH

💡 Note: In this example, the nftables rules are specifically focused on SSH and kept intentionally simple. The goal is only to secure the SSH service. For more advanced nftables configurations (including other protocols), see my guide: How to Configure nftables Rules from Scratch.

  • Edit the /etc/nftables.conf file:
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
        chain input {
                type filter hook input priority filter; policy accept; # Default: accept all
                iif lo accept comment "Accept any localhost traffic"
                ip saddr { 192.168.0.0/24, 198.51.100.51 } tcp dport { ssh } ct state { new, related, established } counter accept comment "Allow SSH from specific IPv4"
		ip6 saddr { 2001:db8:51d:dead:bee5::1 } tcp dport { ssh } ct state { new, related, established } counter accept comment "Allow SSH from specific IPv6"
                tcp dport { ssh } ct state { new, related, established } counter drop comment "Block all other SSH attempts"

        }
        chain forward {
                type filter hook forward priority filter; policy accept; # Default: accept all
        }
        chain output {
                type filter hook output priority filter; policy accept; # Default: accept all
        }
}
  • After editing the /etc/nftables.conf file, load the nftables rules:
root@debian:~# nft -f /etc/nftables.conf
  • Enable the nftables service so that it starts automatically at boot:
root@debian:~# systemctl enable nftables.service

Allow SSH Access with Geo‑IP Restrictions

As explained in the introduction, we will use the IPdeny lists to apply Geo-IP filtering. The IPdeny lists are freely available collections of IP address ranges grouped by country or region, which can be used to allow or block network traffic based on geographic location.

In this example, we only want to allow SSH connections from France 😇 (both IPv4 and IPv6). We will start by downloading the France IPv4 list and the France IPv6 list.

SSH Geo‑IP Filtering Diagram

Diagram showing nftables firewall allowing SSH from France and blocking IPs from Piratistan
nftables firewall configured to allow SSH from France and block connections from unauthorized regions such as “Piratistan”.

Download and Format IPdeny Lists for Geo-IP Filtering

💡 Note: This guide was tested with nftables version 1.1.3. Make sure that your version supports the include file directive before using these examples.

We need to format our IP list file so that it matches the syntax expected by nftables. For example, the IPv6 list for France🇫🇷 should look like this:

set fr_aggregated_v6 {
    type ipv6_addr
    flags interval
    elements = {
2001:0504:0118:0000:0000:0000:0000:0000/48,
2001:0660:0000:0000:0000:0000:0000:0000/29,
2a14:f100:0000:0000:0000:0000:0000:0000/29,
2a14:f400:0000:0000:0000:0000:0000:0000/29,
2a14:fa80:0000:0000:0000:0000:0000:0000/29
    }
}

By default, the IPdeny lists are provided in a raw format, like this:

2001:0504:0118:0000:0000:0000:0000:0000/48
2001:0660:0000:0000:0000:0000:0000:0000/29
2a14:f100:0000:0000:0000:0000:0000:0000/29
2a14:f400:0000:0000:0000:0000:0000:0000/29
2a14:fa80:0000:0000:0000:0000:0000:0000/29

But don't panic — I will show below how to reformat the IPdeny lists step by step!

  • First, download the list with wget and save it as fr-aggregated-v4.zone and fr-aggregated-v6.zone. For example, here is the commands for the French IPs:
root@debian:~# wget https://www.ipdeny.com/ipblocks/data/aggregated/fr-aggregated.zone -O fr-aggregated-v4.zone
root@debian:~# wget https://www.ipdeny.com/ipv6/ipaddresses/aggregated/fr-aggregated.zone -O fr-aggregated-v6.zone

Depending on the country you want to allow, you may notice that there are a large number of lines to format. Fortunately, we can use the sed command to automate this task.

  • For the IPv4 list (fr-aggregated-v4.zone), use the following command:
root@debian:~# sed -i -e 's/$/,/' -e '${s/,$/\n    }\n}/}' -e '1s/^/set fr_aggregated_v4 {\n    type ipv4_addr\n    flags interval\n    elements = {\n/' fr-aggregated-v4.zone
  • For the IPv6 list (fr-aggregated-v6.zone), use the following command:
root@debian:~# sed -i -e 's/$/,/' -e '${s/,$/\n    }\n}/}' -e '1s/^/set fr_aggregated_v6 {\n    type ipv6_addr\n    flags interval\n    elements = {\n/' fr-aggregated-v6.zone
  • Finally, move the fr-aggregated-v4.zone and fr-aggregated-v6.zone files into the /etc/nftables directory (create it first if it does not exist):
root@debian:~# mkdir -p /etc/nftables
root@debian:~# mv fr-aggregated-v4.zone fr-aggregated-v6.zone -t /etc/nftables

Adding Geo-IP SSH Rules to /etc/nftables.conf

  • Next, add the lists to the /etc/nftables.conf file by editing it as follows:
#!/usr/sbin/nft -f

flush ruleset

table inet filter {
	include "/etc/nftables/fr-aggregated-v4.zone"
	include "/etc/nftables/fr-aggregated-v6.zone"
        chain input {
                type filter hook input priority filter; policy accept; # Default: accept all
                iif lo accept comment "Allow localhost traffic"
                ip saddr { 192.168.0.0/24 } tcp dport { ssh } ct state { new, related, established } counter accept comment "Allow SSH from LAN"
                ip saddr @fr_aggregated_v4 tcp dport { ssh } ct state { new, related, established } counter accept comment "Allow SSH from France (IPv4)"
                ip6 saddr @fr_aggregated_v6 tcp dport { ssh } ct state { new, related, established } counter accept comment "Allow SSH from France (IPv6)"
                tcp dport { ssh } ct state { new, related, established } counter drop comment "Block SSH from any other IP"

        }
        chain forward {
                type filter hook forward priority filter; policy accept; # Default: accept all
        }
        chain output {
                type filter hook output priority filter; policy accept; # Default: accept all
        }
}

As with the IP Whitelist example, once you have edited the /etc/nftables.conf file, follow these steps:

  • Load the new nftables rules:
root@debian:~# nft -f /etc/nftables.conf
  • Enable the nftables service so that it starts automatically at boot:
root@debian:~# systemctl enable nftables.service