Firewalls are a tool that most web developers only deal with when sites are down or something is broken. Firewalls aren’t fun, and it’s easy to ignore them entirely on smaller projects.
Part of why firewalls are complicated is that what we think of as a "firewall" on a typical Linux or BSD server is responsible for much more than just blocking access to services. Firewalls (like iptables, nftables, or pf) manage filtering inbound and outbound traffic, network address translation (NAT), Quality of Service (QoS), and more. Most firewalls have an understandably complex configuration to support all of this functionality. Since firewalls are dealing with network traffic, it’s relatively easy to lock yourself out of a server by blocking SSH by mistake.
In the desktop operating system world, there has been great success in the "application" firewall paradigm. When I load a multiplayer game, I don’t care about the minutiae of ports and protocols - just that I want to allow that game to host a server. Windows, OS X, and Ubuntu all support application firewalls where applications describe what ports and protocols they need open. The user can then block access to those applications if they want.
Uncomplicated Firewall (ufw) is shipped by default with Ubuntu, but like OS X (and unlike Windows) it is not turned on automatically. With a few simple commands we can get it running, allow access to services like Apache, and even add custom services like MariaDB that don’t ship with a ufw profile. UFW is also available for other Linux distributions, though they may have their own preferred firewall tool.
Before you start
Locking yourself out of a system is a pain to deal with, whether it’s lugging a keyboard and monitor to your closet or opening a support ticket. Before testing out a firewall, make sure you have some way to get into the server should you lock yourself out. In my case, I’m using a LAMP vagrant box, so I can either attach the Virtualbox GUI with a console, or use vagrant destroy / vagrant up to start clean. With remote servers, console access is often available through a management web interface or a "recovery" SSH server like Linode’s Lish.
It’s good to run a scan on a server before you set up a firewall, so you know what is initially being exposed. Many services will bind to ‘localhost’ by default, so even though they are listening on a network port they can’t be accessed from external systems. I like to use nmap (which is available in every package manager) to run port scans.
$ nmap 192.168.0.110 Starting Nmap 6.40 ( http://nmap.org ) at 2015-09-02 13:16 EDT Nmap scan report for trusty-lamp.lan (192.168.0.110) Host is up (0.0045s latency). Not shown: 996 closed ports PORT STATE SERVICE 22/tcp open ssh 80/tcp open http 111/tcp open rpcbind 3306/tcp open mysql Nmap done: 1 IP address (1 host up) scanned in 0.23 seconds
Listening on for SSH and HTTP connections makes sense, but we probably don’t need rpcbind (for NFS) or MySQL to be exposed.
Turning on the firewall
The first step is to tell UFW to allow SSH access:
$ sudo ufw app list Available applications: Apache Apache Full Apache Secure OpenSSH $ sudo ufw allow openssh Rules updated Rules updated (v6) $ sudo ufw enable Command may disrupt existing ssh connections. Proceed with operation (y|n)? y Firewall is active and enabled on system startup $ sudo ufw status Status: active To Action From -- ------ ---- OpenSSH ALLOW Anywhere OpenSSH (v6) ALLOW Anywhere (v6)
Test to make sure the SSH rule is working by opening a new terminal window and ssh’ing to your server. If it doesn’t work, run sudo ufw disable and see if you have some other firewall configuration that’s conflicting with UFW. Let’s scan our server again now that the firewall is up:
$ nmap 192.168.0.110 Starting Nmap 6.40 ( http://nmap.org ) at 2015-09-02 13:31 EDT Note: Host seems down. If it is really up, but blocking our ping probes, try -Pn Nmap done: 1 IP address (0 hosts up) scanned in 3.07 seconds
UFW is blocking pings by default. We need to run nmap with -Pn so it blindly checks ports.
$ nmap -Pn 192.168.0.110 Starting Nmap 6.40 ( http://nmap.org ) at 2015-09-02 13:32 EDT Nmap scan report for trusty-lamp.lan (192.168.0.142) Host is up (0.00070s latency). Not shown: 999 filtered ports PORT STATE SERVICE 22/tcp open ssh Nmap done: 1 IP address (1 host up) scanned in 6.59 seconds
Excellent! We’ve blocked access to everything but SSH. Now, let’s open up Apache.
$ sudo ufw allow apache Rule added Rule added (v6)
You should now be able to access Apache on port 80. If you need SSL, allow "apache secure" as well, or just use the “apache full” profile. You’ll need quotes around the application name because of the space.
To remove a rule, prefix the entire rule you created with "delete". To remove the Apache rule we just created, run
sudo ufw delete allow apache.
UFW operates in a "default deny" mode, where incoming traffic is denied and outgoing traffic is allowed. To operate in a “default allow” mode, run sudo ufw default allow. After running this, perhaps you don’t want Apache to be able to listen for requests, and only want to allow access from localhost. Using ufw, we can deny access to the service:
$ sudo ufw deny apache Rule updated Rule updated (v6)
You can also use "reject" rules, which tell a client that the service is blocked. Deny forces the connection to timeout, not telling an attacker that a service exists. In general, you should always use deny rules over reject rules, and default deny over default allow.
Address and interface rules
UFW lets you add conditions to the application profiles it ships with. For example, say you are running Apache for an intranet, and have OpenVPN setup for employees to securely connect to the office network. If your office network is connected on eth1, and the VPN on tun0, you can grant access to both of those interfaces while denying access to the general public connected on eth0:
$ sudo ufw allow in on eth1 to any app apache $ sudo ufw allow in on tun0 to any app apache
from <interface> with
on <address> to use IP address ranges instead of interface names.
While UFW lets you work directly with ports and protocols, this can be complicated to read over time. Is it Varnish, Apache, or Nginx that’s running on port 8443? With custom application profiles, you can easily specify ports and protocols for your own custom applications, or those that don’t ship with UFW profiles.
Remember up above when we saw MySQL (well, MariaDB in this case) listening on port 3306? Let’s open that up for remote access.
Pull up a terminal and browse to
/etc/ufw/applications.d. This directory contains simple INI files. For example, openssh-server contains:
[OpenSSH] title=Secure shell server, an rshd replacement description=OpenSSH is a free implementation of the Secure Shell protocol. ports=22/tcp
We can create a mariadb profile ourselves to work with the database port.
[MariaDB] title=MariaDB database server description=MariaDB is a MySQL-compatible database server. ports=3306/tcp
$ sudo ufw app list Available applications: Apache Apache Full Apache Secure MariaDB OpenSSH
$ sudo ufw allow from 192.168.0.0/24 to any app mariadb
You should now be able to access the database from any address on your local network.
Debugging and backup
Debugging firewall problems can be very difficult, but UFW has a simple logging framework that makes it easy to see why traffic is blocked. To turn on logging, start with
sudo ufw logging medium. Logs will be written to /var/log/ufw.log. Here’s a UFW BLOCK line where Apache has not been allowed through the firewall:
Jan 5 18:14:50 trusty-lamp kernel: [ 3165.091697] [UFW BLOCK] IN=eth2 OUT= MAC=08:00:27:a1:a3:c5:00:1e:8c:e3:b6:38:08:00 SRC=192.168.0.54 DST=192.168.0.142 LEN=60 TOS=0x00 PREC=0x00 TTL=64 ID=65499 DF PROTO=TCP SPT=41557 DPT=80 WINDOW=29200 RES=0x00 SYN URGP=0
From this, we can see all of the information about the source of the request as well as the destination. When you can’t access a service, this logging makes it easy to see if it’s the firewall or something else causing problems. High logging can use a large amount of disk space and IO, so when not debugging it’s recommended to set it to low or off.
Once you have everything configured to your liking, you might discover that there isn’t anything in
/etc with your rules configured. That’s because ufw actually stores its rules in
/lib/ufw. If you look at
/lib/ufw/user.rules, you’ll see iptables configurations for everything you’ve set. In fact, UFW supports custom iptables rules too if you have one or two rules that are just too complex for UFW.
For server backups, make sure to include the
/lib/ufw directory. I like to create a symlink from
/lib/ufw. That way, it’s easy to remember where on disk the rules are stored.
Controlling inbound traffic is a great first step, but controlling outbound traffic is better. For example, if your server doesn’t send email, you could prevent some hacks from being able to reach mail servers on port 25. If your server has many shell users, you can prevent them from running servers without being approved first. What other security tools are good for individual and small server deployments? Let me know in the comments!