Hobbyist admins and home-labbers sometimes want to make services on a remote host available to machines on a home network. Unfortunately, many residential ISP customers are stuck with dynamic IP addresses, even when they're not behind a CGNAT. This makes securing the remote host difficult. You'd like to configure the remote firewall to allow traffic only from friendly IP addresses, but if one or more of your addresses might change at any time, what can you do? One option is to buy a fixed IP address from your ISP, but that can be costly.
This article describes how you can configure a remote server using nftables to automatically update its firewall rules so that dynamic IP traffic is allowed with minimal downtime when the IP address changes. The solution requires making a small change to your nftables configuration and a short script that runs periodically as a cron job.
This solution requires that:
nftables is a modern, stateful firewall backend that has largely replaced the legacy iptables as the default firewall in most recent Linux distributions (Debian, Red Hat/CentOS, etc.). If you are using an old distro that still uses iptables, you should consider upgrading as it is being phased out and will eventually be deprecated. The instructions in this article assume that you have nftables installed and configured on the remote server. Information about how to migrate legacy rulesets to nftables can be found on the nftables wiki.
If the remote server is a new installation that doesn't have a firewall configured yet, you can install and configure nftables first from your distribution's repository.
On Debian-based distros:
$ sudo apt update && sudo apt install nftables
On Red Hat-based distros:
$ sudo dnf install nftables
Then ensure nftables is enabled and running:
$ sudo systemctl enable nftables
$ sudo systemctl start nftables
This is a reasonable starter configuration for nftables. It only filters incoming traffic and it limits traffic from all source IP addresses to only the ports specified in the HELLO_WORLD set. (You should modify the elements in this set to include only the ports you actually want exposed to the internet at large.) This is a modified form of the ruleset Simple ruleset for a server from the nftables wiki.
#!/usr/sbin/nft -f
# Flush all rules and start with a clean slate
flush ruleset
table inet filter {
set HELLO_WORLD {
type inet_service
# List of globally-accessible ports
elements = { 22, 80, 443 }
}
chain input {
# By default, drop all traffic unless it meets a filter
# criteria specified by the rules that follow below.
type filter hook input priority 0; policy drop;
# Allow traffic from established and related packets, drop invalid
ct state vmap { established : accept, related : accept, invalid : drop }
# Allow loopback traffic.
iifname lo accept
# accept neighbour discovery otherwise connectivity breaks
icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# Allow all traffic for designated ports
tcp dport @HELLO_WORLD accept
}
}
If the server doesn't need to expose any ports to entire internet, you can remove the HELLO_WORLD block and rule entirely. The nftables configuration is normally found in /etc/nftables.conf, but may be in a file under the etc/nftables.d/ directory. After creating or changing the configuration, be sure to reload nftables:
$ sudo systemctl reload nftables
A Dynamic DNS (DDNS) service provider can provide you with a host name and keep its DNS records up-to-date with your current IP address. Host names are usually subdomains of a domain owned by the DDNS service (such as myddns.example.com) but could be a domain or subdomain that you own if you choose to use the service's nameservers. Either way, a client running on your router or machine notifies the DDNS service through an API call whenever your dynamic IP address changes and the service then updates its A record. Most home routers and NAS's have DDNS clients built-in and pre-configured for major DDNS service providers but, if not, the DDNS service you choose will have instructions for running a client on your home machine.
Some ISPs and domain name vendors provide free or paid DDNS services for their customers. In addition, your router or network hardware manufacturer may provide DDNS service as well. For example, ASUS, Netgear, and Synology all offer this service for free. D-Link used to provide it, but discontinued it in 2020.
The most flexible free options, though, will be from dedicated DDNS providers. These services offer free basic DDNS service, often in the hope that you will use their optional paid services (such as VPS or email) when the time comes. Some may offer additional free services, such as SSL certificate registration. There are many free services available, but some of the best-known include:
Now we need to bridge the DDNS service and our nftables rules so that our DDNS machine is always allowed to access selected services on the remote server. We'll do this in three steps:
Add these two set definitions to your nftables configuration at the top of the inet filter (or ip filter) table. (See further below for a complete configuration example.)
set RESTRICTED_PORTS {
type inet_service
elements = { your services' ports }
}
set DDNS {
type ipv4_addr
elements = { your DDNS host name }
}
The elements in the RESTRICTED_PORTS set should be a comma-separated list of ports that you want open for your DDNS machine, but not open to the rest of the world. For example, to allow access to a MySQL server (port 3306) and a syslog server (port 514), you would use:
elements = { 514, 3306 }
In the DDNS set, you should add the fully-qualified domain name that your DDNS service provided, for example myhost.ddnsfree.com. This tells nftables to look up your latest IP address each time it starts. It won't dynamically update the IP address, but it will always start with the current one.
Next, add this rule to the end of your inet filter input (or ip filter input) chain:
tcp dport @RESTRICTED_PORTS ip saddr @DDNS accept
This rule tells nftables to accept traffic to destination ports listed in RESTRICTED_PORTS set if they come from the source IP address in DDNS.
If you used the example configuration from above, your full configuration will now look something like this (new additions in bold).
#!/usr/sbin/nft -f
# Flush all rules and start with a clean slate
flush ruleset
table inet filter {
set RESTRICTED_PORTS {
type inet_service
elements = { 514, 3306 }
}
set DDNS {
type ipv4_addr
elements = { myhost.ddnsfree.com }
}
set HELLO_WORLD {
type inet_service
# List of globally-accessible ports
elements = { 22, 80, 443 }
}
chain input {
# By default, drop all traffic unless it meets a filter
# criteria specified by the rules that follow below.
type filter hook input priority 0; policy drop;
# Allow traffic from established and related packets, drop invalid
ct state vmap { established : accept, related : accept, invalid : drop }
# Allow loopback traffic.
iifname lo accept
# accept neighbour discovery otherwise connectivity breaks
icmpv6 type { nd-neighbor-solicit, nd-router-advert, nd-neighbor-advert } accept
# Allow traffic from designated ports
tcp dport @HELLO_WORLD accept
tcp dport @RESTRICTED_PORTS ip saddr @DDNS accept
}
}
Reload nftables so that the new sets and rule take effect:
$ sudo systemctl reload nftables
If you list the nftables ruleset now, you should see that the DDNS set has already been populated with your current IP address:
$ sudo nft list ruleset
The next step is to write a simple script that finds your up-to-date IP address and, if necessary, modifies the definition in your nftables rules.
Change to your home directory and create a new text file called refreship.sh using nano or your favorite text editor:
$ cd ~
$ nano refreship.sh
Copy the short script below into the editor, replacing your_ddns_host_name with the fully-qualified domain name provided by your DDNS service (for example myhost.ddnsfree.com), then save and exit.
#!/bin/bash
#
# ~/refreship.sh
# Updates nftables if dynamic IP address has changed.
#
# This script must be run as root
#
HOST=your_ddns_host_name
LOOKUP=$( nslookup $HOST | grep -Po "(?<=Address: )\d+\.\d+\.\d+\.\d+" )
if ! [ "$(/usr/sbin/nft get element inet filter DDNS { $LOOKUP } 2>/dev/null)" ]; then
/usr/sbin/nft flush set inet filter DDNS
/usr/sbin/nft add element inet filter DDNS { $LOOKUP }
fi
The script, as written, assumes that the nftables executable is located at /usr/sbin/nft and that the nftables table is called inet filter.
Finally, make the script executable:
$ chmod u+x refreship.sh
Now, whenever you run refreship.sh as root, it will look up the current IP address for your DDNS machine and compare it to the last-known IP address in nftables. If the IP address has changed, it will update the nftables rule to reflect the new address. Here's a line-by-line breakdown of what the script does:
HOST=your_ddns_host_name
This sets the variable HOST to your DDNS host name. Replace "your_ddns_host_name with the domain name from your DDNS service (e.g. myhost.ddns.net). This the the only line you need to change.
LOOKUP=$( nslookup $HOST | grep -Po "(?<=Address: )\d+\.\d+\.\d+\.\d+" )
This runs the nslookup shell command to find your DDNS host's current IP address, as set by your provider. The grep command scrapes your current IP address and stores it in the variable LOOKUP.
if ! [ "$(/usr/sbin/nft get element inet filter DDNS { $LOOKUP } 2>/dev/null)" ]; then
This line checks your nftables configuration to see if it already knows about the current IP address. (Output of the command is discarded since we'll be running this script as a cron job.) If nftables already knows about this IP address, then we're done. Otherwise, your IP address has changed, so we'll execute the next two lines:
/usr/sbin/nft flush set inet filter DDNS
This line, only run if your IP address has changed, clears the old address from nftables.
/usr/sbin/nft add element inet filter DDNS { $LOOKUP }
This line, only run if your IP address has changed, writes the new address into nftables.
fi
End the if block.
All that's left now is to create a cron job so that refreship.sh automatically runs as often as you like. You will need to make an entry in the root crontab (not your own), so invoke the crontab editor using sudo:
$ sudo crontab -e
At the bottom of the crontable, you can add this entry to run refreship.sh every 15 minutes. (Choose a shorter interval if you like.) Replace your_username with your own login name. You should not use ~/refreship.sh because the script will run as root, which has /root as it's home.
*/15 * * * * /home/your_username/refreship.sh
This entry tells cron to run refreship.sh every 15 minutes of every hour of every day on every day of the week during every month. Save and exit the crontab editor and it will begin running immediately.
Now, your nftables rule will automatically adapt when your IP address changes with just a few minutes of transition time. Depending on your ISP, you can probably force an IP change for testing by cycling the power on your router or modem, waiting the number of minutes you specified in the cron job, and checking the nftables ruleset with sudo nft list ruleset.
The only downside to this solution is that there is a short lag between when your IP address changes and when the change is reflected in your firewall. If you are running a web server or API server on the remote host, you can reduce the lag by writing an API that responds immediately, but that's a more complicated solution that I may cover in a future article.
firewallcybersecuritynftablesddnsdynamic dns
The door opens, and nineteen demons, each a cross between a carrot and a sledge hammer, march out from behind it, knock you senseless, and return, the last closing the door behind it.