Build a dynamic firewall or how to add dynamically clients to iptables
Introduction
Some weeks ago I read an article about zero trust networks. Even though I knew the concept, I thought to myself "How much of a zero trust network can I build with easy methods?". So I started to re-model my firewall to add dynamical rules to it depending on the trust level of the client. Before I start please note that this is just the first building block. What I have in mind of doing in the future and a discussion about the security issues with this architecture are in the end of this article. So if you read it and want to shout "I can easily circumvent this with xyz", please see this discussion section. And if you have something I was not thinking of, please let me know (preferable on twitter). In the following I assume everyone knows how iptables works.
TL;DR
I re-modeled my iptables script of the router to add clients dynamically depending on their trust level with the help of the isc-dhcp-server.
Infrastructure
In my home network I use a self-made router based on Debian. The iptables rules are configured with a simple bash script holding all rules, the clients get their IP address with the help of an isc-dhcp-server and bind is used as a local DNS server. Since this router has 3 separate network interfaces, I separated my wifi from my internal network and the Internet. The internal network and the wifi have different IP address ranges. The infrastructure of the network looks something like this:

Obviously, the laptops can switch from the wifi to the internal network when plugging in an ethernet cable. So we have to keep this in mind when designing the mechanism to dynamically updating the iptables rules. The old configuration I used was a classical one. Meaning I got a pool of valid IP addresses for the clients which connected to the network and configured the iptables rules for this pool statically. When I got a special client that had different iptables rules, I gave them a specific IP address and configured the rules for it accordingly.
Design
As I mentioned before, I want to have different trust levels which I assign to each client. When a client connects to the network, it should get an IP address from a pool and the iptables rules are set up according to the trust level. Also I want to be able to reset the iptables without loosing each configured dynamic client (for example when I change something at the static part of the iptables rules) and without storing some kind of state for each client. With the help of iptables chains I came up with the following design:

This is the design for the INPUT chain. The FORWARD and OUTPUT chains have the same design principle. I separated the iptables rules in 3 different categories: static chains, transit chains, dynamic chains.
The static chains contain all the rules for the static clients that do not change their IP address (like the router itself or the servers). For example the router allows each client in the internal and wifi network to get an IP address via DHCP.
The transit chains contain the rules that connect the static chains to the dynamic chains. In the image you can see that the INPUT chain contains a rule that jumps to the dynamic input chain. This chain contains all the jump rules to the chains for the dynamic clients.
The dynamic chains contain the rules for the dynamic clients. In the image you can see that each client that gets an IP address from the DHCP server gets an own input chain that contains the rules for it. Each client gets its own rules according to its trust level. In the image you can see for example that PC1 is allowed to connect to the router via SSH, whereas Laptop1 and Phone1 are not allowed.
This design allows me to change something in my iptables script on the static chains and restart it without loosing the rules for the dynamic clients. This can be done by flushing and removing all static chains, but leaving the transit chains and dynamic chains alone. Otherwise I had to write something that keeps states for each dynamic client which would also complicate everything further.
Implementation
As I mentioned in the design section, I do not want to write something that keeps states for each dynamic client. But in order to add and remove iptables rules for clients dynamically, something has to know the current state of the clients. For this, I decided to use my isc-dhcp-server installation. It has 3 events on which it can execute a script: commit, release and expiry. The commit is the event that is triggered whenever a client gets an IP address from the dhcp server. The release event is triggered when a client releases its IP address, and the expiry event triggers when a IP address lease expires. We can use this to add and delete iptables rules dynamically. The architecture for this looks as follows:

The dhcp server executes wrapper scripts for each event with the IP address and the MAC address of the client as argument. This is done because it cannot start a script in the background. This means the script blocks the dhcp server until it is finished. To avoid this, the wrapper script does nothing else than executing the add client/remove client script in the background and just forwards the arguments. The add client script searches in a "database" (I just use a csv file for this) for a mapping of the MAC address to a trust level. If it has found one, it executes the iptables script and passes it the IP address and the corresponding trust level. The iptables script then adds the corresponding rules for the client. The remove client script which is executed for the release and expiry event just passes the IP address to the iptables script which then removes the rules for the corresponding client.
This was the high-level overview of the architecture. Now to the technical aspect. I assume that the scripts are stored under /etc/firewall/. The given code is a slimed down version of the one I use. I removed aspects such as running the dhcp server under a different user as root in order to make it easier to understand (just add an entry in the /etc/sudoers for the add client/remove client scripts). The important configuration part for the isc-dhcp-server (dhcpd.conf) looks like the following:
subnet 10.1.1.0 netmask 255.255.255.0 {
range 10.1.1.50 10.1.1.150;
option routers 10.1.1.1;
option broadcast-address 10.1.1.255;
option domain-name "h4des.org";
option domain-name-servers 10.1.1.1;
on commit {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
}
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
}
on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
}
}
subnet 192.168.0.0 netmask 255.255.255.0 {
range 192.168.0.50 192.168.0.150;
option routers 192.168.0.1;
option broadcast-address 192.168.0.255;
option domain-name "h4des.org";
option domain-name-servers 192.168.0.1;
on commit {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
}
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
}
on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
}
}
The scripts needed for the rest can be downloaded here.
Discussion and Future Work
Obviously, this is not a finished zero trust network and it has still security issues. But it is a first building block for it. The biggest security issue is relying on the MAC address to distinguish clients. Of course any adversary can easily forge this. For this a more sophisticated method has to be used (perhaps IEEE 802.1X?). But at the moment I have no idea what can be used for this in an easy way.
The next problem is even if distinguishing clients were secure, an attacker can still steal the MAC address of a client that is currently connected to the network. In order to tackle this issue, the ARP packets have to be monitored. I do not know what already exists to do this.
And connected to the IP addresses, if a client just disconnects from the network without releasing the IP address, the router still allows every traffic for it according to the trust level until the expiry event triggers. In this time frame, an attacker can abuse the iptables rules. This can be tackled by lowering the lease time of an IP address.
At the moment the iptables rules are only set in the router, however, the servers still do not distinguish between the clients. A better way would be to let the router tell the servers to add or remove iptables rules. At the moment I think the easiest way to do this is to use ssh and just execute a script on the server that handles everything. And also it would be way cooler if not only the router changes its permissions dynamically, but the servers as well (meaning the whole network does)
So I guess this was every security issue I currently had on my mind with the current concept. But if you have thought of something else, please let me know (preferable on twitter). And if you have other ideas to tackle the problems I mentioned, please let me also know.
Some weeks ago I read an article about zero trust networks. Even though I knew the concept, I thought to myself "How much of a zero trust network can I build with easy methods?". So I started to re-model my firewall to add dynamical rules to it depending on the trust level of the client. Before I start please note that this is just the first building block. What I have in mind of doing in the future and a discussion about the security issues with this architecture are in the end of this article. So if you read it and want to shout "I can easily circumvent this with xyz", please see this discussion section. And if you have something I was not thinking of, please let me know (preferable on twitter). In the following I assume everyone knows how iptables works.
TL;DR
I re-modeled my iptables script of the router to add clients dynamically depending on their trust level with the help of the isc-dhcp-server.
Infrastructure
In my home network I use a self-made router based on Debian. The iptables rules are configured with a simple bash script holding all rules, the clients get their IP address with the help of an isc-dhcp-server and bind is used as a local DNS server. Since this router has 3 separate network interfaces, I separated my wifi from my internal network and the Internet. The internal network and the wifi have different IP address ranges. The infrastructure of the network looks something like this:

Obviously, the laptops can switch from the wifi to the internal network when plugging in an ethernet cable. So we have to keep this in mind when designing the mechanism to dynamically updating the iptables rules. The old configuration I used was a classical one. Meaning I got a pool of valid IP addresses for the clients which connected to the network and configured the iptables rules for this pool statically. When I got a special client that had different iptables rules, I gave them a specific IP address and configured the rules for it accordingly.
Design
As I mentioned before, I want to have different trust levels which I assign to each client. When a client connects to the network, it should get an IP address from a pool and the iptables rules are set up according to the trust level. Also I want to be able to reset the iptables without loosing each configured dynamic client (for example when I change something at the static part of the iptables rules) and without storing some kind of state for each client. With the help of iptables chains I came up with the following design:

This is the design for the INPUT chain. The FORWARD and OUTPUT chains have the same design principle. I separated the iptables rules in 3 different categories: static chains, transit chains, dynamic chains.
The static chains contain all the rules for the static clients that do not change their IP address (like the router itself or the servers). For example the router allows each client in the internal and wifi network to get an IP address via DHCP.
The transit chains contain the rules that connect the static chains to the dynamic chains. In the image you can see that the INPUT chain contains a rule that jumps to the dynamic input chain. This chain contains all the jump rules to the chains for the dynamic clients.
The dynamic chains contain the rules for the dynamic clients. In the image you can see that each client that gets an IP address from the DHCP server gets an own input chain that contains the rules for it. Each client gets its own rules according to its trust level. In the image you can see for example that PC1 is allowed to connect to the router via SSH, whereas Laptop1 and Phone1 are not allowed.
This design allows me to change something in my iptables script on the static chains and restart it without loosing the rules for the dynamic clients. This can be done by flushing and removing all static chains, but leaving the transit chains and dynamic chains alone. Otherwise I had to write something that keeps states for each dynamic client which would also complicate everything further.
Implementation
As I mentioned in the design section, I do not want to write something that keeps states for each dynamic client. But in order to add and remove iptables rules for clients dynamically, something has to know the current state of the clients. For this, I decided to use my isc-dhcp-server installation. It has 3 events on which it can execute a script: commit, release and expiry. The commit is the event that is triggered whenever a client gets an IP address from the dhcp server. The release event is triggered when a client releases its IP address, and the expiry event triggers when a IP address lease expires. We can use this to add and delete iptables rules dynamically. The architecture for this looks as follows:

The dhcp server executes wrapper scripts for each event with the IP address and the MAC address of the client as argument. This is done because it cannot start a script in the background. This means the script blocks the dhcp server until it is finished. To avoid this, the wrapper script does nothing else than executing the add client/remove client script in the background and just forwards the arguments. The add client script searches in a "database" (I just use a csv file for this) for a mapping of the MAC address to a trust level. If it has found one, it executes the iptables script and passes it the IP address and the corresponding trust level. The iptables script then adds the corresponding rules for the client. The remove client script which is executed for the release and expiry event just passes the IP address to the iptables script which then removes the rules for the corresponding client.
This was the high-level overview of the architecture. Now to the technical aspect. I assume that the scripts are stored under /etc/firewall/. The given code is a slimed down version of the one I use. I removed aspects such as running the dhcp server under a different user as root in order to make it easier to understand (just add an entry in the /etc/sudoers for the add client/remove client scripts). The important configuration part for the isc-dhcp-server (dhcpd.conf) looks like the following:
subnet 10.1.1.0 netmask 255.255.255.0 {
range 10.1.1.50 10.1.1.150;
option routers 10.1.1.1;
option broadcast-address 10.1.1.255;
option domain-name "h4des.org";
option domain-name-servers 10.1.1.1;
on commit {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
}
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
}
on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
}
}
subnet 192.168.0.0 netmask 255.255.255.0 {
range 192.168.0.50 192.168.0.150;
option routers 192.168.0.1;
option broadcast-address 192.168.0.255;
option domain-name "h4des.org";
option domain-name-servers 192.168.0.1;
on commit {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_commit_wrapper.sh", ClientIP, ClientMac);
}
on release {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_release_wrapper.sh", ClientIP, ClientMac);
}
on expiry {
set ClientIP = binary-to-ascii(10, 8, ".", leased-address);
set ClientMac = binary-to-ascii(16, 8, ":", substring(hardware, 1, 8 ) );
execute ("/etc/firewall/dhcpd/on_expiry_wrapper.sh", ClientIP, ClientMac);
}
}
The scripts needed for the rest can be downloaded here.
Discussion and Future Work
Obviously, this is not a finished zero trust network and it has still security issues. But it is a first building block for it. The biggest security issue is relying on the MAC address to distinguish clients. Of course any adversary can easily forge this. For this a more sophisticated method has to be used (perhaps IEEE 802.1X?). But at the moment I have no idea what can be used for this in an easy way.
The next problem is even if distinguishing clients were secure, an attacker can still steal the MAC address of a client that is currently connected to the network. In order to tackle this issue, the ARP packets have to be monitored. I do not know what already exists to do this.
And connected to the IP addresses, if a client just disconnects from the network without releasing the IP address, the router still allows every traffic for it according to the trust level until the expiry event triggers. In this time frame, an attacker can abuse the iptables rules. This can be tackled by lowering the lease time of an IP address.
At the moment the iptables rules are only set in the router, however, the servers still do not distinguish between the clients. A better way would be to let the router tell the servers to add or remove iptables rules. At the moment I think the easiest way to do this is to use ssh and just execute a script on the server that handles everything. And also it would be way cooler if not only the router changes its permissions dynamically, but the servers as well (meaning the whole network does)

So I guess this was every security issue I currently had on my mind with the current concept. But if you have thought of something else, please let me know (preferable on twitter). And if you have other ideas to tackle the problems I mentioned, please let me also know.