One of the standard ways to employ the netfilter packet filtering suite is iptables and its upgraded version, nftables. While not the simplest, their userspace tools provide perhaps the most comprehensive and standardized way of configuring firewall rules.
In this tutorial, we show how to limit the traffic of a system to only the SSH protocol. First, we discuss remote access and why SSH is usually the method of choice. Next, we delve into the configuration to entirely isolate a system except for SSH. Finally, we put together all the iptables rules for the purpose, along with some customization.
For brevity, we use iptables to also refer to its successor, nftables.
We tested the code in this tutorial on Debian 11 (Bullseye) with GNU Bash 5.1.4 and iptables v1.8.7 (nf_tables). It should work in most POSIX-compliant environments.
2. Remote Access
Because of advances like virtualization, containerization, and cloud service providers, it’s not uncommon for systems to spawn automatically. Consequently, we need a predefined way to enter them. For example, once a new Linux environment is up, we usually already have SSH access.
Of course, the way we gain access to a remote environment is critical to its security. In every case, as with any real place, we need at least one entrance and one exit, a door of sorts. In computer science terms, this means we need to allow traffic over at least one port number.
While there are alternatives for the way to gain access, administrators overwhelmingly prefer the SSH protocol not only due to its security but also its versatility. In fact, we can use secure shell connections for much more than interactive shell access.
Critically, we can use SSH to modify further filtering rules for a given system, meaning it can safely be the only allowed remote access protocol.
3. Exclusive SSH Access
After discussing the motivation behind doing so, we can configure exclusive remote access via the secure shell protocol. While working with iptables, we must be conscious that one wrong command can lock us out of the system. Still, if we are careful and apply the rules in the proper order, we can set everything up over an already-existing SSH connection.
3.1. Check Current Rules
To start, it’s good practice to check the current rules in effect:
$ iptables -L Chain INPUT (policy ACCEPT) target prot opt source destination Chain FORWARD (policy ACCEPT) target prot opt source destination Chain OUTPUT (policy ACCEPT) target prot opt source destination
Here, we have no predefined filtering in the listing (-L, –list) for any of the chains.
3.2. Allow Local Traffic
Since this traffic should not pose a security risk to our overall access permissions, we can make an exception for it:
$ iptables -A INPUT -i lo -j ACCEPT $ iptables -A OUTPUT -o lo -j ACCEPT
In this case, we add (-A, –append) rules for both the INPUT and OUTPUT chains to ACCEPT (-j ACCEPT, –jump ACCEPT) traffic on the lo interface as both start point (-o, –out-interface) and endpoint (-i, –in-interface).
3.3. Allow SSH
Now, we are ready to open our system to SSH traffic. Importantly, we use the default port 22, but SSH can run on any number of ports.
The commands to allow SSH via iptables introduce several new concepts:
$ iptables -A INPUT -p tcp -m tcp --dport 22 -j ACCEPT $ iptables -A OUTPUT -p tcp --sport 22 -m state --state ESTABLISHED -j ACCEPT
Once again, we add rules to the INPUT and OUTPUT chains. For both, we ACCEPT only TCP (-p tcp, –protocol tcp) traffic from (–sport, –source-port) and to (–dport or –destination-port) port 22.
In addition, we use extension modules to further match (-m, –match) and verify:
- incoming data as TCP
- outgoing data as having the ESTABLISHED state (–state)
In essence, the latter means we only allow outgoing traffic through an already-established TCP connection from the specified source port.
3.4. Set Main Policy
Before continuing with the final settings, we must ensure two things:
- SSH access works
- we have a way to restore access in case of mistakes
As a rule of thumb, it’s best to set the INPUT chain to DROP by default. In this case, we’re even more restrictive by applying the same policy (-P, –policy) to outgoing traffic via the OUTPUT chain.
To be safe, we can set everything on one line and reset it after a timeout period, so we can ensure our connection doesn’t drop permanently:
$ iptables -P INPUT DROP; iptables -P OUTPUT DROP; sleep 30; iptables -P INPUT ACCEPT; iptables -P OUTPUT ACCEPT
This way, we have 30 seconds to confirm SSH access (still) works as expected even with the new policy. If it doesn’t, we’re allowed back in. Otherwise, we can set the policies permanently:
$ iptables -P INPUT DROP $ iptables -P OUTPUT DROP
Set in this manner, iptables prevents any data from going in or out of the system on all interfaces by default unless it matches a rule which allows that.
4. Configure the Rules
Keeping in mind our policy checks, let’s put together all rules from the previous section:
$ LOIF=lo $ SSHPORT=22 $ iptables -A INPUT -i $LOIF -j ACCEPT $ iptables -A OUTPUT -o $LOIF -j ACCEPT $ iptables -A INPUT -p tcp -m tcp --dport $SSHPORT -j ACCEPT $ iptables -A OUTPUT -p tcp --sport $SSHPORT -m state --state ESTABLISHED -j ACCEPT $ iptables -P INPUT DROP $ iptables -P OUTPUT DROP
As long as our SSH port $SSHPORT and loopback interface name $LOIF are correct, we should be able to apply the rules above directly, even over an already established SSH session.
In this article, we went over the iptables rules for leaving a machine open only to SSH traffic.
In conclusion, because the secure shell protocol is a de facto standard for remote administration, we can initialize a system by excluding all other traffic.