Lab - Linux Firewalls

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>

Bibliography

Previous
Next