Authors: André Zúquete, João Paulo Barraca, Hélder Gomes, Vitor Cunha
Introduction
A firewall is a service allowing to filter the traffic flowing through a host. When using the Linux Operating System, the firewall rules can be implemented using the legacy iptables
or the new nftables
frontend. In both cases, the actual firewall is within the netfilter
kernel framework, allowing detailed control over the packets exchanged. The netfilter
kernel framework provides both stateless and statefull packet filtering.
Setup
In this guide, the objective is to explore the creation and exploitation of a Linux based firewall. For this purpose it is required to create two three.
- The Client will have a single network interface, connected to the FW. This element represents an Internal host, inside the local environment.
- The FW will have two network interfaces, one connected to the Client, and the other connected to a Server
- The Server will have a single network interface connected to the FW. This element represents an Internet host, outside the local environment.
To setup the environment we need to install a package called Containerlab. It allows the creation of networking envioronments supported by Docker.
The installation of Containerlab can be done with: bash -c "$(curl -sL https://get.containerlab.dev)"
Then, you need to create the networking environment. Start by creating a folder named, lab-fw
, anywhere on your filesystem:
mkdir lab-fw
cd lab-fw
Inside this folder, create a file named lab-fw.clab.yaml
(download from here ) with the following content:
name: lab-fw
topology:
kinds:
linux:
image: ubuntu
env:
DEBIAN_FRONTEND: noninteractive
exec:
- "echo 'debconf debconf/frontend select Noninteractive' | debconf-set-selections"
- "apt update"
- "apt install -y iproute2 iputils-ping curl dnsutils"
nodes:
client:
kind: linux
exec:
- "ip addr add dev eth1 192.168.1.1/24"
- "ip route del default"
- "ip route add default via 192.168.1.254"
fw:
kind: linux
exec:
- "apt install -y iptables ulogd2"
- "ip addr add dev eth1 192.168.1.254/24"
- "ip addr add dev eth2 192.168.100.254/24"
- "ip addr add dev eth3 192.168.200.1/24"
- "ip route del default"
- "ip route add default via 192.168.200.254"
stages:
create-links:
exec:
- command: ip address add dev fw-out0 192.168.200.254/24
target: host
phase: on-exit
server:
kind: linux
exec:
- "apt install -y nginx openssh-server fail2ban iptables ulogd2"
- "ip addr add dev eth1 192.168.100.1/24"
- "ip route del default"
- "ip route add default via 192.168.100.254"
- "nginx"
links:
- endpoints: ["client:eth1","fw:eth1"]
- endpoints: ["fw:eth2","server:eth1"]
- type: host
endpoint:
node: "fw"
interface: "eth3"
host-interface: fw-out0
This file defines the topology, initial packages to be installed and configurations. You can start the deployment with:
sudo containerlab deploy
After everything is deployed, you should see:
+--------------------+-------------+---------+-------------------+
| Name | Kind/Image | State | IPv4/6 Address |
+--------------------+-------------+---------+-------------------+
| clab-lab-fw-client | linux | running | 172.20.20.4 |
| | ubuntu | | 3fff:172:20:20::3 |
| clab-lab-fw-fw | linux | running | 172.20.20.2/24 |
| | ubuntu | | 3fff:172:20:20::4 |
| clab-lab-fw-server | linux | running | 172.20.20.3/24 |
| | ubuntu | | 3fff:172:20:20::2 |
+--------------------+-------------+---------+-------------------+
You can check that the containers are running using docker ps
. The topology is also available if you use containerlab graph
.
In the terminal, you can enter one node with docker exec -ti clab-lab-fw-client /bin/bash
(example for the client node).
After all containers are running, the FW should be able to ping
the Server (address 192.168.100.1
) and the Client (address 192.168.1.1
).
All containers should be able to ping
one another.
Default policies
For each iptables
chain we should define a default policy, which represents the rule that is applied when no other rule is present.
Like in any other security aspect we should choose a defensive approach, denying traffic from potentially malicious sources.
In our case we will allow outgoing traffic, generated from the FW machine.
For this purpose execute the following commands in the FW:
vm:~$ docker exec -ti clab-lab-fw-fw /bin/bash
root@fw:/# iptables -P INPUT DROP
root@fw:/# iptables -P OUTPUT ACCEPT
root@fw:/# iptables -P FORWARD DROP
You can check the correct application of the rules using the following command:
root@fw:/# iptables -nvL
You can also check that everything is working if you try to ping
the FW from the Client.
Establish basic connectivity
With the policies that are applied, the Client cannot communicate with other hosts. Not even the FW.
Check it:
root@client:/# ping 192.168.1.254
If we wish to allow the ping
command to be used, we can add a rule for this purpose.
If a packet matches this rule, the default policy will not be applied to it and the packet will be handled according to the rule.
The following commands insert rules that allow for ICMP
requests from the Client to the FW to be accepted:
root@fw:/# iptables -A INPUT -p icmp --icmp echo-request -j ACCEPT
Try to ping
the FW from the Client.
What happens if you try to ping
the Client from the FW?
Check with tcpdump
or wireshark
which packets are exchanged and describe what happens.
Because we are using an emulated network, you need to run tcpdump
inside the namespace:
For tcpdump
:
vm:~$ ip netns exec clab-lab-fw-client tcpdump -i any -n -l
For wireshark
vm:~$ ip netns exec clab-lab-fw-client wireshark
Can you create a rule that allows for the correct operation of the ping
command?
Take in consideration that while the availability of this protocol is not mandatory, blocking all ICMP
packets can have negative consequences.
Note: You can authorize all traffic originating from the Client using the statefull firewalling capabilities. Therefore, if a connection is already established or a response is know to be related to a request started from your machine (e.g., FTP data transfer), allow it.
root@fw:/# iptables -I INPUT -m state --state ESTABLISHED,RELATED -j ACCEPT
The command above uses -I
(insert) and not -A
(append).
Rules are processed in a sequential manner, so you want to control if the rule is applied at the end of all rules or in any other order.
INSERT
allows you to put the rule in any line number.
If no line number is given, the default is to place it at the fist rule.
If you did some mistake, you can replace the -I
or -A
by a -D
to delete a rule.
Filter internal traffic
Up to this moment the Client has no connectivity to the Server, because all packets will be blocked by the rules active in the FW.
In order to allow this connectivity, the FW must enable the following functionality: (i) forward IP traffic, (ii) do not block the forwarded traffic, (iii) apply a Network (Port) Address Translation (NAT) mechanism to the traffic coming from the internal network.
The current FW networking configuration already allows the host to forward traffic. You can check that using the following command to inspect the Linux kernel configuration:
root@fw:/# cat /proc/sys/net/ipv4/ip_forward
A value of 1 indicates the IP forwarding is enabled.
Allowing the Client traffic to be forwarded at the firewall level requires the creation of a rule matching that traffic.
Considering that the FW interface that communicates with the Client is named eth1
, configured with the address 192.168.1.254
, the following rules can be used:
root@fw:/# iptables -A FORWARD -i eth1 -s 192.168.1.0/24 -j ACCEPT
root@fw:/# iptables -A FORWARD -o eth1 -d 192.168.1.0/24 -j ACCEPT
Finally, enabling NAT mechanism will allow the FW to hide the Client from the Server, while providing connectivity.
The Linux OS can do NAT by having a rule stating that all packets from the Client network should be masqueraded when being routed by the FW to the Server’s network.
root@fw:/# iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth2 -j MASQUERADE
Use wireshark or tcpdump in the FW in order to analyze what is happening in interface eth2
.
## For wireshark
vm:~$ ip netns exec clab-lab-fw-fw wireshark
## For tcpdump
vm:~$ ip netns exec clab-lab-fw-fw tcpdump -i any -n -l
Check that the Client can access a service in the server.
As the server has a nginx
service running, you can use curl
to access the web page:
root@client:/# curl http://192.168.100.1
Adding Internet connection to the containers
In this setup, DNS requests, from any of the 3 hosts, will be routed through the eth3
interface of the FW host (the link to the Internet).
This interface links to a network interface of the docker host, named fw-out0
.
However, the host will not forward traffic, therefore containers cannot reach the Internet; therefore, they cannot resolve DNS names.
The following commands, when executed in the VM, deal with this limitation. Please adjust enp0s3
with the VM interface providing access to the Internet (usually, the IP is 10.0.2.15
):
vm:~$ sudo iptables -I FORWARD -i fw-out0 -o enp0s3 -j ACCEPT
vm:~$ sudo iptables -I FORWARD -i enp0s3 -o fw-out0 -j ACCEPT
Furthermore, you will also need to add a NAT rule to your VM:
vm:~$ sudo iptables -t nat -I POSTROUTING -s 192.168.200.0/24 -o enp0s3 -j MASQUERADE
In order to enable resolving Internet addresses, add the DNS server to the /etc/resolv.conf
file of the Client, FW and Server.
If you are in the University, the address must be 193.136.172.20
(as in the example). If you are outside, the address 8.8.8.8
should work.
client:~# echo 'nameserver 193.136.172.20' > /etc/resolv.conf
At this point, the Server cannot reeach the Internet because no routing is allowed by the FW for traffic originated in or targeting its network. This can be fixed with the following rules (similar to what you already did to the client):
fw:/# iptables -A FORWARD -i eth2 -s 192.168.100.0/24 -j ACCEPT
fw:/# iptables -A FORWARD -o eth2 -d 192.168.100.0/24 -j ACCEPT
Finally, you will also need to establish a NAT rule in the firewall:
fw:/# iptables -t nat -A POSTROUTING -s 192.168.100.0/24 -o eth3 -j MASQUERADE # for the server network
fw:/# iptables -t nat -A POSTROUTING -s 192.168.1.0/24 -o eth3 -j MASQUERADE # for the client network
Now, you can check the connection to the Internet from any container (Client, Server or FW) using, for instance, the ping
command to 8.8.8.8
or ping www.ua.pt
.
Filter traffic for specific hosts
Frequently, it is required to block the access to a specific host in order to comply with the security policy of the domain. As an example, consider that users should not be allowed to access social networks, such as Facebook. A rule blocking access to a host has the following shape:
root@fw:/# iptables -I FORWARD -d <ip_address> -j DROP
Using the host
command (on the VM host), find all address of www.facebook.com
and insert the appropriate rules in the FW.
Then, validate the effectiveness of the setup (if you are using ping or curl, don’t forget to use the flag -4
to force it to only use IPv4).
Using DROP vs. REJECT
In the previous situation the decision was to silently DROP
all packets.
However, other decisions can be used, such as REJECT
. The different decisions will influence how the packet is handled.
To check this, insert rules to block www.google.pt
using the REJECT
decision.
Without options, the rejection will be signaled by an ICMP port unreachable packet.
Access both services and compare the results. Wireshark can also help diagnosing the behavior of the firewall.
The option --reject-with type
allows to configure the type of the rejection packet.
You can try to use any of the following:
- `icmp-net-unreachable`
- `icmp-host-unreachable`
- `icmp-port-unreachable` (the default)
- `icmp-proto-unreachable`
- `icmp-net-prohibited`
- `icmp-host-prohibited`
- `icmp-admin-prohibited`
- `tcp-reset`
Delete the rejection rule and add a new one using a rejection message other than the default one. Compare the results.
Traffic logs
In this section we shall test a different decision target which creates records of the network activity. They may be useful to identify the communication endpoints, detecting unusual patterns or communication activity. It should be noticed that this will only allow detection after the communication actually took place, and will not allow blocking the offending exchange.
One possible approach is to configure iptables
so that some packets, both from successful connections and rejected connections are logged to a registry.
Considering the next command, if the destination address (ip_address
) is the same as the one used in the previews rules, this will log all dropped packets. Using -A
would not work in this case, as the rule would never be reached.
fw:/# iptables -I FORWARD -d <ip_address> -j NFLOG --nflog-prefix "DROP "
Logs can be picked up by the ulogd2 daemon, which stores them in the file /var/log/ulog/syslogemu.log
:
fw:/# service ulogd2 start
However, other applications can subscribe the reception of these logs.
In order to log all successful TCP connections, we will need to add rules that apply to every new packets from TCP connections that were not dropped (therefore, they are accepted).
In this case, the rules must be added to the middle of the table, after the rules that drop packets, and before the rules that accept traffic.
The following command achieves this when using the appropriate value of <N>
to specify the position.
fw:/# iptables -I FORWARD <N> -p tcp -m state --state NEW \
-j NFLOG --nflog-prefix "TCP NEW "
To list all rules and find the appropriate place to inject the previous rule, you can use the following command:
fw:/# iptables -nvL --line-numbers
As a curiosity, if we wanted to apply a rule to the remaining TCP packets we could use the following match:
fw:/# iptables -I FORWARD -m state -p tcp --state RELATED,ESTABLISHED \
-j NFLOG --nflog-prefix "TCP CONTENT "
Filter traffic from specific services
Frequently it is required to allow only a set of services over well known ports, blocking all remaining traffic. One practical example would be to allow only DNS and HTTPS traffic, which can be implemented using the following rules:
fw:/# iptables -I FORWARD -s 192.168.1.0/24 -p udp ! --dport 53 -j DROP
fw:/# iptables -I FORWARD -s 192.168.1.0/24 -p tcp ! --dport 443 -j DROP
Check the command is applied correctly by accessing a page that uses HTTP (port 80), and a page that uses HTTPS (port 443). Remove the previous block using the following commands:
fw:/# iptables -D FORWARD -s 192.168.1.0/24 -p udp ! --dport 53 -j DROP
fw:/# iptables -D FORWARD -s 192.168.1.0/24 -p tcp ! --dport 443 -j DROP
Forward traffic from the outside to internal services
Because we are using NAT, it is not possible for external hosts to access services provided by internal servers (in the Server).
Additional rules can be added in order to implement the adequate port forwarding mechanisms.
The Server container already has the openssh-server
package, providing a functional SSH Server.
## Set the root password
server:/# passwd
Then edit /etc/ssh/sshd_config
to allow password logins by adding:
PermitRootLogin yes
PasswordAuthentication yes
finally,restart SSH server:
server:/# service ssh restart
Assuming that the SSH server is already running and configured properly, we can create a rule that forwards connections to the FW into this server. Specifically we will forward TCP packets, to port 22 of the external interface of the FW, to port 22 of the Server. This represents the situation where a FW has a connection to the Internet, and exposes a specific service running in the Internal network.
This can be implemented by the following command:
fw:/# iptables -t nat -A PREROUTING -p tcp --dport 22 -d 192.168.200.1 \
-j DNAT --to 192.168.100.1
You can use your host to check if the service is available. Test if you can SSH from your host to the Server using the newly set root credentials:
vm:~$ ssh root@192.168.200.254
!! Warning !!: Do not use the root account in the wild like it is configured here. We are deliberately creating insecure services to show how defense mechanisms work in different scenarios.
Save and restore iptables
rules
The rules added to iptables
are always temporary, and will be cleared if the host reboots, or the -F
argument is specified to the command.
Therefore, it is important to save rules to a file, restore these rules at a later time.
The following commands will save the rules to a file named /etc/iptables.save
, clear the tables, list the content (they should be empty), and then restore the rules:
fw:/# iptables-save > /etc/iptables.save
fw:/# iptables -F
fw:/# iptables -t nat -F
fw:/# iptables -nvL
fw:/# iptables -t nat -nvL
fw:/# iptables-restore < /etc/iptables.save
You can edit the file /etc/iptables.save
and see how rules are saved.
If required, you can also edit the rules.
Dynamic Host Firewall
While the rules in the FW are able to apply several restrictions to traffic, they are unable to react to all attacks targeting the services exposed. One important situation that must be addressed is the exposition of the SSH service. Once a server exposes this port to the Internet, it will receive tens or hundreds of login attempts per hour. The attempts will come from human attackers, but also from programs doing large scale service discovery, login attempts with dictionary attacks. It is very important to block offending addresses, after they reach a determined number of failed connection attempts. Because this type of behavior depends on the software running on the Server, it is more practical to deploy such filtering at the Server, in the form of a dynamic set of rules.
For this purpose, we will use the fail2ban
package pre-installed in the Server. Edit /etc/fail2ban/jail.conf
and enable several jails there related to pam
and ssh
.
Restart the fail2ban
service, list the rules that are automatically created, and the jails:
server:/# service fail2ban restart
server:/# iptables -L
server:/# fail2ban-client status
server:/# fail2ban-client status sshd
Now try to access the SSH service multiple times, failing the username or password. After some tries you should be blocked. List the existing rules and the status of the jails and you should see your IP address listed:
server:/# fail2ban-client status sshd
You can unban specific IP addresses with:
server:/# fail2ban-client unban <ip_address>