DEV Community

Cover image for Setting up iptables for web apps
Sebastian Wirkijowski
Sebastian Wirkijowski

Posted on

Setting up iptables for web apps

The iptables package gives us advanced, granular control over our firewall, with important built-in features like filtering and limiting. This guide will cover a more advanced approach, leveraging the mentioned capabilities, the included conntrack module, and restrictive rules, while exploring potential security risks.

Every command we use in this guide (except man) needs elevated privileges, so I have prepended all commands with sudo to make copying everything — after careful checking, of course — easier.

We will be implementing a firewall that follows the principles laid out by the stateful firewall concept.

The configuration covered in this guide was tested on a local Ubuntu 24.04 virtual machine and on a cloud Ubuntu 22.04 virtual machine. Both with Apache2 installed and enabled for HTTP/HTTPS rule testing. Testing included making and receiving HTTP/HTTPS requests, checking apt connectivity, using the dig command and using ping from client to virtual machine.

This configuration is compatible with the LAMP, LEMP and MERN stacks, and should work with other similar stacks as well.

Originally, this was part of my “Setting up a LAMP (the stack)” guide, but I decided to split it into a separate publication to avoid overwhelming newcomers.

This guide is also available on medium.

Manual and documentation

Terminal screenshot — iptables manual search preview

You are encouraged to check out the documentation of commands we will be using in this guide. Linux command documentation and manuals can be accessed by using man iptables.

  • Use up and down arrow keys or Page Up/Page Down to navigate through the manual.
  • Press / while the manual is open and type your search term to highlight instances of that term or phrase. This is especially significant for newcomers, since long documentation can get overwhelming really quick.
  • To exit the manual, press q.

Some manuals also have examples on how to perform certain actions.

Alternatively, you can use iptables -h for summarized details and list of options.

How do firewalls work?

All firewall environments use the same or similar terminology. The concepts remain unchanged. When we set up firewall rules, we add those rules to a chain, on a specific table. There is no need to elaborate on tables at this point, since usually, and in this case, we will be only working on the default “filter” table.

The usual chains are:

  • INPUT — packets heading into our machine,
  • FORWARD — packets passing through (mainly used by routers),
  • OUTPUT — packets that our machine sends out.

Since we are setting up a firewall for a web server machine, I’m assuming that you want to host a website or other web application. This means that we need to open ports 80 (HTTP) and 443 (HTTPS). On top of that, we need to open the SSH/SFTP port 22 (SFTP is essentially FTP over SSH, which means it also uses port 22), to keep remote communication with our machine open. Those ports only use the TCP protocol. We will also allow limited ICMP (pings) traffic and DNS lookups (port 53 via UDP).

Remember, if you changed your SSH port, you have to use your custom port instead of the default 22!

In a standard firewall solution, rules are processed in the same order in which they are added. In a command line interface (CLI), this is especially important. In a graphical user interface (GUI), usually you are allowed to move things around, but in both cases ordering mistakes can cause major problems, including complete lockouts.

If you do lose remote access, most vendors that offer virtual machines also provide a web terminal or direct access via on-site support. If that’s not the case, don’t be ashamed to scrap the installation and try again from the start. It’s all part of the learning process.

Working with iptables

As you can probably see in the manual, there's a lot of commands, flags, and parameters going on. Let's go through some of the basic commands that will be helpful. We will skip the rule appending part, since we cover them in great detail later.

To get current rules from all chains, with their corresponding position, use:

sudo iptables -L -vn --line-numbers
Enter fullscreen mode Exit fullscreen mode
  • The -L parameter means “list”, and it lists all the rules in the selected chain. If no chain is specified, all chains will be displayed.
  • The -v parameter means “verbose”, this provides additional details about each rule.
  • The -n parameter means “numeric”, this will prevent iptables from attempting to resolve hostnames and display IP addresses and ports in their numerical form.
  • Finally, the --line-numbers option adds a line number to each rule to the output. This is especially helpful when you need to delete or modify a specific rule.

To delete a specific rule from a specific chain, you should look it up via the previous command, and then use:

sudo iptables -D {chain} {rule-id}
Enter fullscreen mode Exit fullscreen mode

Rule persistency

Rules defined directly via iptables are ephemeral, which means that they are temporary, and exist only for the duration of the session. If we reboot our server without saving them, they will disappear.

Because of that, we also need the iptables-persistent package on top of iptables. It’s basically a systemd service that loads your rules every time the machine starts.

To save the configuration, we can use the following:

sudo netfilter-persistent save
Enter fullscreen mode Exit fullscreen mode

Danger: Rules take effect immediately after you add them! This command only saves them to load them back after restart!

The setup

Usually, you would want to set up INPUT and OUTPUT rules for specific ports, so your server can both receive and respond to requests from the internet. We will be taking it a bit farther.

When someone visits our server, they send a request, by which they establish a connection. On the first request, the state is NEW, but everything after that has the ESTABLISHED or RELATED (to existing connection; auxiliary connection) state. Of course this can differ on other protocols, but HTTP/HTTPS are not that fancy.

We will be taking a balanced approach, by doing the following:

  • As a server — allow NEW and ESTABLISHED inbound 80, 443 traffic destined for our web server, and allow outgoing packets only to ESTABLISHED connections.
  • As a client — allow NEW and ESTABLISHED outbound 80, 443 traffic destined for external web servers (apt usage, external APIs), and allow only ESTABLISHED inbound connections.
  • SSH (22) — allow only NEW or ESTABLISHED inbound traffic and exclusively ESTABLISHED outbound connections. This effectively prevents our server from making its own SSH connections.
  • ICMP (protocol) & DNS (53 via UDP) — allow restricted ICMP and ESTABLISHED inbound DNS, NEW, ESTABLISHED outbound DNS. Since this is not a DNS server, we do not accept new or unrelated inbound DNS packets.
  • and drop everything that does not match any of our rules.

This setup offers a solid foundation and limits the potential exposure surface while allowing fairly free standard HTTP/HTTPS traffic flow.

Theoretically, you could also use this setup with the MERN stack if you're using NGINX as a reverse proxy. Since traffic between Node.js and NGINX is internal, it will flow smoothly over the loopback interface that will leave open.

Security considerations

The second you expose your server or machine to the internet, you will notice that there will always be someone or something scanning and probing it. Either by attempting to break in using popular insecure credentials, scanning open ports or poking your deployed web application for usual configuration mistakes or environment vulnerabilities.

It's important to highlight, that security is not an objective or destination, rather a directive that guides our actions. You cannot achieve security, but you can do your best to mitigate malicious activity. Another thing to keep in mind for the future is: security through obscurity is not real security. It’s a flawed security principle that focuses on secrecy instead of actual protection.

In some cases ICMP can be used maliciously as a tunneling method. We will mitigate this by blocking all ICMP except for inbound echo-request type messages and outgoing echo-reply, destination-unreachable messages. This will allow external machines to ping ours (useful for basic health checks), but prevent our server to send any ICMP messages except the basic responses, and limits the possibility for an ICMP tunneling attack to work, but does not completely eradicate it. If you do not plan to use ICMP, feel free to not include those rules in your configuration or remove them later.

Similar thing can be done with DNS, that’s why we are also limiting traffic on port 53 UDP.

On its own, the server should not be able to send outgoing requests without any safeguards. Opening outgoing communication raises the risk of data exfiltration instigated by viruses or code execution vulnerabilities. This configuration is a great foundation, but if you want to go the extra mile, after you install everything, you should delete the rules that allow outside connections as a client, and instead allow only outgoing client communication to specific IP addresses. We will cover this process at the end of this guide.

When it comes to rate-limiting, I would advise against using the standard iptables limit functionality, since this will not differentiate between legitimate connections and brute force attacks. Setting a rate limit that way even for packets with NEW state, can result in lockout in a situation where you are attempting to connect during a brute-force attack or just regular vulnerability probing/scanning. You should use a specialized utility, like fail2ban for that purpose instead.

Before we begin with the configuration, it’s crucial to understand that if an attacker has gained direct access to the server, our firewall will do absolutely nothing to stop them.

Configuration

  1. If you do not have the iptables installed, let’s install it with apt:

    sudo apt install iptables iptables-persistent
    

    During the iptables-persistent installation process, you will be asked to save and load the current iptables rules for IPv4 and IPv6. At this point it is save to confirm both prompts.

  2. (Optional) If you don’t want to keep your previous rules and do not have the default action set to DROP or REJECT on input or output chains, remove all current rules:

    Danger! This may cause loss of access to your machine!

    sudo iptables -F
    
  3. Set up the inbound rules. Feel free to paste this whole block directly:

    sudo iptables -A INPUT -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
    sudo iptables -A INPUT -p tcp --dport 443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
    sudo iptables -A INPUT -p tcp --dport 22 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
    sudo iptables -A INPUT -p icmp --icmp-type echo-request -j ACCEPT; \
    sudo iptables -A INPUT -p udp --sport 53 -m conntrack --ctstate ESTABLISHED -j ACCEPT
    

    Notice the ; operator, it allows for command chaining. With it, commands will execute even if the previous command fails. On top of that, we are using the \ multiline operator to make the command chain more legible.

    To break down what we just did:

    • The -A flag means “append”, this tells the application that we want to add this rule at the end of a specific chain (INPUT).
    • The -p parameter means “protocol”, it defines which protocol is this rule for. All of our ports work on TCP, so we only allow packets via TCP.
    • The --sport parameter can be found in the iptables-extensions(8) manual, and defines which source (”s”) ports to allow.
    • The --dport parameter defines the destination port of the packet.
    • The -m parameter means “match”, in our case it matches the packet’s state to either NEW or ESTABLISHED. This will only allow new or already established connections to communicate with our machine. We’re using the conntrack module, that’s why it’s --ctstate.
    • The -j parameter means “jump”, and specifies what to do when the packet matches our rule. Here we want to accept everything.
  4. Set up the outbound rules:

    sudo iptables -A OUTPUT -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
    sudo iptables -A OUTPUT -p tcp --sport 443 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
    sudo iptables -A OUTPUT -p tcp --sport 22 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
    sudo iptables -A OUTPUT -p icmp --icmp-type echo-reply -j ACCEPT; \
    sudo iptables -A OUTPUT -p icmp --icmp-type destination-unreachable -j ACCEPT; \
    sudo iptables -A OUTPUT -p udp --dport 53 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
    
  5. Set up rules that allow outside connections as a client:

    sudo iptables -A INPUT -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
    sudo iptables -A INPUT -p tcp --sport 443 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
    sudo iptables -A OUTPUT -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
    sudo iptables -A OUTPUT -p tcp --dport 443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
    

    You will notice that those rules have swapped source, destination ports and connection states. That’s because when we are connecting to a remote server, our machine is acting as a client, so in the opposite way as with Apache.

  6. Allow internal (loopback) traffic:

    sudo iptables -A INPUT -i lo -j ACCEPT; \
    sudo iptables -A OUTPUT -o lo -j ACCEPT
    

    Without those rules, your database will not be able to communicate with your web application, because we also set up a general DROP rule further down the road.

  7. Before setting the default DROP rules, check if your configuration contains the intended rules:

    sudo iptables -L -vn
    

    The parameters are as follows:

    • The -L parameter means “list”, and it lists all the rules in the selected chain. If no chain is specified, all chains will be displayed.
    • The -v parameter means “verbose”, this provides additional details about each rule.
    • The -n parameter means “numeric”, this will prevent iptables from attempting to resolve hostnames and display IP addresses and ports in their numerical form.

    The output should looks like this:

    Terminal screenshot — active iptables rules

  8. Change default behavior to drop all other packets:

    Danger! This may cause loss of access to your machine!

    sudo iptables -P INPUT DROP; \
    sudo iptables -P OUTPUT DROP; \
    sudo iptables -P FORWARD DROP
    

    This will drop all traffic that do not match our previous rules and is a critical component of an effective firewall. Naturally we want to drop all FORWARD chain packets, since this is basically a resource provider and not a router. Under any circumstances there should be no forwarding going on, and if there is anything like that happening, it’s highly likely that there is some malicious activity going on, like tunneling.

  9. Save the the configuration:

    sudo netfilter-persistent save
    

    This will first save the rules we just set up to a file and update the configuration.


Preventing unwanted outgoing HTTP/HTTPS

Following up on the idea that the server itself should not be making new connections on itself (acting as a client), we should disable the potentially unsafe rules to lock our server down, while allowing the bare minimum it needs to serve web content.

As mentioned before, if you are calling any external APIs on the server side, which means in your PHP code (or on your server-side rendered JavaScript app), you will have to leave those rules in to avoid connectivity problems. Removing them will also prevent all packages that pull external data to your machine from working, most importantly apt/apt-get and curl.

Fortunately, at least for administrative purposes, there are solutions. The ones that I personally know of are:

  1. Port Knocking — this relies on sending a sequence of “knock” requests to the server, after which the server will: open up a port only for the IP address making the knocks (SSH) and set up rules that allow client HTTP/HTTPS requests. You can learn more from the Practical Guide to Port Knocking article available on cs.fyi.
  2. Scripting — make a script that appends allow client HTTP/HTTPS rules after you log in, and deletes them after you’re done with your tasks. This can be automated, but can also be done manually.

In all cases there will be some overhead.

Use this code snippet to delete the client rules:

sudo iptables -D INPUT -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
sudo iptables -D INPUT -p tcp --sport 443 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
sudo iptables -D OUTPUT -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
sudo iptables -D OUTPUT -p tcp --dport 443 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT
Enter fullscreen mode Exit fullscreen mode

And this snippet to put them back in when you need them:

sudo iptables -A INPUT -p tcp --sport 80 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
sudo iptables -A INPUT -p tcp --sport 443 -m conntrack --ctstate ESTABLISHED -j ACCEPT; \
sudo iptables -A OUTPUT -p tcp --dport 80 -m conntrack --ctstate NEW,ESTABLISHED -j ACCEPT; \
sudo iptables -A OUTPUT -p tcp --dport 443 -m conntrack --ctstate NEW,ESTABLISHED -j
Enter fullscreen mode Exit fullscreen mode

Congratulations! You have improved your server’s security.

  • You’ve hardened the system by implementing a robust firewall.
  • You’ve gained fundamental knowledge of how firewalls work.
  • You’ve also gained insights into both general and specific security concerns (like tunneling, exfiltration, rate-limitting)

Spotted a mistake? Let me know!

Thank you for reading. Hopefully this guide provided some value :)

Top comments (0)