iptables and netfilter: a Linux firewall intro

I've turned over a new leaf in how I approach security. I've mentioned in the past that I haven't always understood, or given too much respect to the security community. When vulnerabilities like Heartbleed come and go, my rationale is, sure - obviously OpenSSL should be patched. But what about all the other vulnerabilities that we haven't found yet? It just seemed like a lost cause.

The answer, and I'm probably late to the game for this but hey - we all come to conclusions on our own time - is "layered security". By combining multiple security efforts all at once, individual security vulnerabilities become less important, and more manageable. It becomes kind of a game: What kind of network traffic do you expect? What kind of traffic can you automatically reject, and at how many different points in your stack can you reject it?

A firewall is more specifically a tool for filtering network packets. It's best to configure it to just reject anything you don't want before it reaches your applications, even if you're not running any questionable services. On Linux systems, the standard is pretty much iptables, so I've learned how to set up and use that.

Note that nftables has been developed as a successor to iptables, but iptables is still way more widely-used than nftables, so a working knowledge of iptables remains valuable. In other words:

Besides, it seems that bpfilter is the current craze in this world over nftables. Ubuntu provides UFW, a front-end to nftables/iptables, (Ubuntu's answer to Red Hat's firewalld). ufw aims to be more intuitive to use than iptables/nftables. My process has always been to start with the basics, and then move on to the fancier things after revisiting everything, so I chose to start with iptables.

How do you look at iptables rules?

iptables -L -v
verbose listing
iptables -L
less verbose
iptables -S
compact

You might see a rule like this:

-A INPUT -s 192.168.0.0/16 -p tcp -m tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT

This tells the system to accept packets coming in on port 22 within the 192.168.*.* range of IPs. Note the IP address: iptables uses CIDR notation for IP addresses. This is a little less intuitive than something like 127.127.*.*. Read 127.127.0.0/16 like "the first 16 bits of this address are significant, and the rest are wildcards". So if you wanted to accept only the exact IP 127.127.42.12, that would be 127.127.42.12/32. Or 192.168.0.* would be 192.168.0.0/24.

There are three areas to hook into the iptables procedure: INPUT, FORWARD, and OUTPUT. These are referred to as "chains". Here's a nice ascii diagram showing the idea of how they relate:

                          _____
Incoming                 /     \         Outgoing
       -->[Routing ]--->|FORWARD|------->
          [Decision]     \_____/        ^
               |                        |
               v                       ____
              ___                     /    \
             /   \                  |OUTPUT|
            |INPUT|                  \____/
             \___/                      ^
               |                        |
                ----> Local Process ----
            
https://www.netfilter.org/documentation/HOWTO//packet-filtering-HOWTO-6.html

So, the INPUT chain deals with packets getting sent to a local process, like sshd or nginx. That's what's most interesting right now - let's start blocking access! Each table has a policy: either ACCEPT or DROP. By default, everything is ACCEPT. It's recommended to actually set things up so all policies are DROP, then open up only what's necessary. That's kind of extreme, but maybe I will start doing it that way in the future. As a first step, and to get things basically working, use a DROP policy for the INPUT table.

To make these rules persistent, you save all your iptables rules to a file, and then when the network starts up, read that file with iptables-restore. Something like this:

# cat /etc/iptables.up.rules
# Generated by iptables-save v1.6.2 on Thu May 17 13:10:56 2018
*filter
:INPUT DROP [313941:79232244]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [9333:868032]
-A INPUT -i lo -j ACCEPT
-A INPUT -m conntrack --ctstate RELATED,ESTABLISHED -j ACCEPT
-A INPUT -m conntrack --ctstate INVALID -j DROP
-A OUTPUT -o lo -j ACCEPT
-A OUTPUT -m conntrack --ctstate ESTABLISHED -j ACCEPT
COMMIT
# Completed on Thu May 17 13:10:56 2018
# cat /etc/network/if-pre-up.d/iptables 
#!/bin/sh
/sbin/iptables-restore < /etc/iptables.up.rules

Here are some good resources: