systemd application firewalls by example

An application firewall, unlike a gateway (router) or system level firewall, is meant to limit the networking of a single application. It can be used to prevent a compromised service from seeing into the local network, prevent programs from calling home, plug metadata leaks, or more tightly control a program’s network access.

You should first read the introduction to systemd service sandboxing and security hardening. Remember to not edit service files directly! Use the systemctl edit utility to make changes to an existing service.

The systemd firewall directives is built on Linux kernel features. The required Kernel features might not be enabled in your specific environment (especially when using a custom kernel or container). Testing is key, as it is with any network filter and security solution. You should always test to verify that your firewall set up blocks and allows the traffic you specify.

You can create basic application firewalls for your systemd services using two key service directives: IPAddressAllow and IPAddressDeny. The rest of the article will demonstrate some example applications of these directives.

Here’s a simple example for a DNS resolver stub that is blocked from connecting to Google DNS (8.8.8.8), but is allowed to connect to Cloudflare DNS (1.1.1.1) and Quad9 DNS (9.9.9.9). Other IP addresses are also allowed in this example.

[Service]
IPAddressDeny=8.8.8.8
IPAddressAllow=1.1.1.1
IPAddressAllow=9.9.9.9

You can only specify IP addresses with or without masks, but not domain names. Note that this set up doesn’t guarantee that the service won’t try to use the system’s DNS resolvers, e.g. through the getaddrinfo system call.

Here’s another example for, e.g. a file sharing service, that can bind to a port on the local host, and only accepts ingress and egrees to devices on the local network (10/24):

[Service]
IPAddressDeny=any
IPAddressAllow=localhost
IPAddressAllow=10.0.0.0/24

The keywords any, localhost, link-local, and multicast are special network names. These names are resolved by systemd. By denying traffic from the any source, you change the ruleset into an allow-list.

The above is a basic but powerful example. It’s enough to prevent a service from phoning home to its developer, or block someone outside the local network from accessing an email server.

Here’s one more example of a service that is blocked from connecting anywhere but the localhost. Environmental variables are set for the service to instruct it to use a socks proxy to exit onto the internet.

[Unit]
After=tor.service

[Service]
Environment=ALL_PROXY=socks://127.0.0.1:9050
Environment=HTTPS_PROXY=socks://127.0.0.1:9050
Environment=HTTP_PROXY=socks://127.0.0.1:9050
IPAddressDeny=any
IPAddressAllow=localhost

The above example leaves the service free to connect to any other service running on the local host. You can further restrict the service’s access by binding the proxy service to a more specific loopback address, e.g. 127.100.90.50, and then only allowing the service to connect to that address.

You can implement entirely custom filters for ingress and egress traffic using an BPF program. This is out of scope for this article, but you can refer to the IPIngressFilterPath and IPEgressFilterPath directives in the systemd.resource-control man page for more details.