Skip to content

Systemd firewall integration suggestions #7327

@ipuustin

Description

@ipuustin

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFE 🎁Request for Enhancement, i.e. a feature requestpid1

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions