Systemd Service Hardening?

So I was learning about the options for Systemd services for restricting or in another word “hardening” services.

This made me question does Kicksecure deploy certain hardening for it’s services or certain Kicksecure specific services?

These are some interesting options I will note here:

PrivateNetwork=yes/no
Gives the service its own network namespace. It can’t see or use the host’s network interfaces unless you explicitly bind sockets. Seems useful to prevent leaks or prevent services from being ran without networking?

PrivatePIDs=yes/no
Gives a service an isolated view of the process table, which is a useful “defense in depth” measure, especially for daemons that don’t need to inspect or manage other system processes.

PrivateTmp=yes/no
Provides a private /tmp and /var/tmp for the service, isolating it from the host’s temporary directories. One service can’t read temporary files left by another service, which mitigates attacks that rely on guessing or stealing temp‑file names.

MemoryDenyWriteExecute=yes/no
Remounts parts of the filesystem read‑only (or makes the whole tree read‑only) for the service. (helps mitigate code‑injection attacks).

ProtectSystem=full/strict/yes
Remounts parts of the filesystem read‑only (or makes the whole tree read‑only) for the service.

There is many others but these few caught my eye.

I don’t think we harden specific “third-party” services yet, but we definitely use these kinds of things for applicable services in Kicksecure and Whonix. See sdwdate/usr/lib/systemd/system/sdwdate.service at master · Kicksecure/sdwdate · GitHub for instance, there’s a whole battery of hardening options enabled here, including a syscall filter.

4 Likes

@arraybolt3
On the USBGuard thread you mentioned CUPS, does systemd support a drop in file? it looks like documentation states it does?

Along with a unit file foo.service, a “drop-in” directory foo.service.d/ may exist. All files with the suffix “.conf” from this directory will be merged in the alphanumeric order and parsed after the main unit file itself has been parsed. This is useful to alter or add configuration settings for a unit, without having to modify unit files. Each drop-in file must contain appropriate section headers. For instantiated units, this logic will first look for the instance “.d/” subdirectory (e.g. “foo@bar.service.d/”) and read its “.conf” files, followed by the template “.d/” subdirectory (e.g. “foo@.service.d/”) and the “.conf” files there. Moreover, for unit names containing dashes (“-”), the set of directories generated by repeatedly truncating the unit name after all dashes is searched too. Specifically, for a unit name foo-bar-baz.service not only the regular drop-in directory foo-bar-baz.service.d/ is searched but also both foo-bar-.service.d/ and foo-.service.d/. This is useful for defining common drop-ins for a set of related units, whose names begin with a common prefix. This scheme is particularly useful for mount, automount and slice units, whose systematic naming structure is built around dashes as component separators. Note that equally named drop-in files further down the prefix hierarchy override those further up, i.e. foo-bar-.service.d/10-override.conf overrides foo-.service.d/10-override.conf.

systemd.unit

Couldn’t we create a CUPS drop in /etc/systemd/system/cups.service.d/hardening.conf that would apply some hardened options that would essentially mitigate:

WAN / public internet: a remote attacker sends an UDP packet to port 631. No authentication whatsoever.
LAN: a local attacker can spoof zeroconf / mDNS / DNS-SD advertisements (we will talk more about this in the next writeup ) and achieve the same code path leading to RCE.

I don’t think the package manager would ever touch it either with upgrades to CUPS?

1 Like

Thanks for sharing this info I was not aware of this. PrivateNetwork=yes/no looks like a good option for CUPS and others we don’t need network access.

1 Like

Yes, systemd configuration snippet drop-in support is amazing.

(As a distribution we should use folder /usr/lib/systemd/system rather than /etc.)

Any systemd hardening options:

  1. Please try to submit them upstream. (Such as to cups.) If that does not work,
  2. Submit them to Debian.
  3. Only then consider submitting these to Kicksecure.

This is for the purpose of maintainability.

See also:

This is not on our immediate roadmap: Kicksecure Security Roadmap

But anyone feel free to work on this.

2 Likes

Honestly I really doubt upstream or Debian would add anything that disables network and LAN printing such as:

CapabilityBoundingSet=CAP_NET_BIND_SERVICE
PrivateNetwork=yes

However they might be accepting to NoNewPrivileges=yes and others.

People do indeed use network printing or LAN printing (I don’t do either personally) but maybe a new thread regarding CUPS should be made like “should CUPS even come disabled by default” or the ideas mentioned here.

1 Like

FWIW, CUPS doesn’t even appear to be installed on Kicksecure 17 by default, unless I accidentally uninstalled it in my development VM during other work:

[sysmaint ~]% apt list --installed | grep cups

WARNING: apt does not have a stable CLI interface. Use with caution in scripts.

libcups2/oldstable,oldstable-security,now 2.4.2-3+deb12u8 amd64 [installed,automatic]
2 Likes

Thanks for pointing that out

I was not even thinking about CUPS cause I even have a printer nor print anything. Might be something to look into :+1:

As Patrick mentioned /usr/lib/systemd/system is better for distribution to ship.

Heres an idea here that looks like it would mitigate that

By default, it is listed on UDP port 631 and is open to the world

  • Example service conf

/usr/lib/systemd/system/cups.service.d/99-cups-harden.conf

[Service]
# Minimal capabilities – only needed to bind to privileged ports (631)
CapabilityBoundingSet=CAP_NET_BIND_SERVICE

# Give CUPS its own network namespace – no external network access
PrivateNetwork=yes

# Prevent the service from gaining new privileges (setuid, etc.)
NoNewPrivileges=yes

# Restrict network families – only IPv4/IPv6 TCP, no UDP
RestrictAddressFamilies=AF_INET AF_INET6

# Bind only to the loopback interface (change to a specific IP if you need LAN access)
# The ExecStart line in the original unit already runs /usr/sbin/cupsd
# If you want to force a bind address at the service level uncomment
# ExecStart=
# ExecStart=/usr/sbin/cupsd -o Listen=127.0.0.1:631

# Filesystem protection – keep CUPS from touching the rest of the system
ProtectSystem=full
PrivateTmp=yes
ReadOnlyPaths=/etc/cups
ReadWritePaths=/var/spool/cups /var/log/cups

# Drop any unnecessary privileges
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes