-
-
Notifications
You must be signed in to change notification settings - Fork 4.4k
Systemd firewall integration suggestions #7327
Description
Submission type
- Request for enhancement (RFE)
Systemd firewall integration: some suggestions
I saw the recent IPAddressDeny, IPAddressAllow and IPAccounting changes, but I think they could be improved. I have some suggestions which build on each other, so that they could be implemented even partially.
Zone declarations in systemd.network
Dealing with IP addresses in IPAddress* is difficult especially when doing system-level configuration for things like IoT devices, where the IP address ranges are not known beforehand. It would be easier to deal with interfaces and zones. I suggest the following change to the systemd.network file config:
[Firewall]
Zones=
A comma-separated list of the firewall zones that the matching network
interfaces belong to. All interfaces belong to zone "all". The loopback
interface belongs to zone "local". Other zone names can be defined by
just adding them as values to the Zones= keyword.
Then we could extend IPAddressDeny and friends to look (something) like this:
IPAddressDeny=zone:all
IPAddressAllow=zone:local,zone:lan
The big benefit would be that we could say that a service would be open to connections from ethernet interface or VPN interface, while not serving any clients trying to connect from the WLAN interface. Using zones is a pretty standard way for designing firewall rules.
Nftables integration
Even though eBPF is fast and allows for very fine-grained control, many (most) real-life hosts use a netfilter-based firewall. The key problem is to have a modular firewall ruleset that can be realiably extended from systemd. Nftables offers a much better support for this than iptables by having real firewall scripting ( nft), atomicity (the firewall is always in a known "complete" state), and data structures. Systemd could use this functionality for implementing service-level support for controlling port opening (and other things) in the firewall. This functionality would be orthogonal to the already supported eBPF IP address filtering.
Note that we would not get support for rules like "open port 22 to ssh.service". We would instead have rules like "open port 22 in the firewall".
OpenPorts= keyword
The syntax in systemd.service files could look like this:
OpenPorts=
Which ports to open in the firewall for the service. Syntax is either:
OpenPorts=22,200,300-305
or
OpenPorts=tcp:22,udp:200,udp:300-305
or
OpenPorts=zone:all:tcp:22,zone:local:udp:200,udp:300-305
If no “udp” or “tcp” qualifier is given, the ports are opened in the firewall
for both protocols. The requested ports are opened in the firewall when
the service is started and closed when the service is stopped.
The benefit of this would be that opening ports in the firewall would be tied to service lifecycle -- no ports would be opened if the service was not running. The previously-introduced zones would be used here. Probably just supporting tcp and udp protocols would already be enough for the vast majority of services, and the others would need to be handled via the firewall ruleset itself (by adding a ruleset fragment to a pre-configured .d directory). The OpenPorts= keyword might be implemented in several ways, but below I'm considering how to do it in nftables.
Hooking systemd services to nftables ruleset
In order to use OpenPorts= , systemd needs to control the firewall ruleset. One way to define this would be to let systemd control the firewall script by introducing a new file type, say systemd.firewall. This would be directly tied to corresponding service file, so that starting (say) nftables.service would trigger loading of nftables.firewall.
The systemd.firewall file might look like this (other stuff like logging, counters, port forwarding, etc. could be added later):
Policy=
Allowed values are "reject", "drop", or “accept”. This is the policy for the input
chain that handles the incoming packets. If a packet goes through the processing
chain without any matching rule, this verdict is applied to it. Having policy "accept"
means that OpenPorts= keyword is a no-op.
RulesetFile=
Which firewall ruleset file is loaded as the global configuration. Systemd defines a
set of variables to be used in the ruleset file.
The ruleset file, if controlled by systemd, would need to have some pre-defined syntax so that systemd could hook the services to it. Using maps for both tcp- and udp-based services might be a clean and fast approach:
#!/usr/sbin/nft
table inet filter {
map tcp_service_map {
type inet_service : verdict
}
map udp_service_map {
type inet_service : verdict
}
chain input {
type filter hook input priority 1; policy $policy;
tcp dport vmap @tcp_service_map;
udp dport vmap @udp_service_map;
}
}
It's possible to make this suitable for all protocols (like ICMP) using packet marking, but there is a related performance penalty.
How do we add systemd services OpenPort= declarations to the ruleset? There are several alternatives to this with varying complexity. One way is to generate firewall fragments and add them to the ruleset using nft's include keyword. Reloading the firewall ruleset using nft will add the fragments atomically. A fragment to open ssh port for zone "local" could look like this:
#!/usr/sbin/nft
table inet filter {
include "zones.ruleset"
chain sshd {
iif @ZONE_LOCAL accept;
}
}
add element inet filter tcp_service_map {22 : jump sshd};
(This assumes that zone definitions are in zones.ruleset.)
A heavier approach would be to write a "systemd-nft" application, which could parse nft ruleset file, consider the firewall settings in systemd unit files and create the atomic firewall transaction (possibly using libnftnl). Keeping compatibility with nft scripting makes sense, because it's developed in lock-step with kernel nftables support and it has been documented well.