A systemd unit can restrict filesystem visibility, process access, devices, namespaces, address families, and privilege gains. These controls are useful because they limit what a compromised service can reach after an exploit. They also create operational failure modes that do not appear in a simple systemctl start test.

The systemd.exec manual is explicit that some protections depend on kernel features and container configuration. File-system namespace settings may have no effect when namespacing is unavailable. A unit that assumes ProtectSystem=strict or PrivateUsers= is active without checking the runtime environment can overstate its isolation.

Choose controls from the service's actual contract

Start with what the service must read, write, execute, and connect to. ProtectSystem=strict makes most of the hierarchy read-only while leaving selected API paths available for separate protection. ProtectHome= hides or read-only mounts home directories. PrivateTmp= gives the service a private temporary directory. ReadWritePaths= and ReadOnlyPaths= should describe the service's real data contract.

PrivateDevices= removes access to physical devices other than pseudo-devices such as /dev/null and /dev/zero. ProtectProc=invisible and ProcSubset=pid reduce process visibility. PrivateUsers= gives a user namespace with a restricted view of users and groups. NoNewPrivileges= prevents privilege gains through setuid, setgid, or file capabilities. Pick the combination that matches how the process actually works.

Do not add a long block of hardening directives and assume that every directive applied. Read the service manager's logs, inspect /proc from inside the unit, and test file access. A bind mount or a container manager can change the result. The manual warns that the implementation may be impossible and that a unit should not rely solely on a setting for security.

Test both success and denied behavior

Create a test unit with the same User, Group, ExecStart, mounts, and capabilities as production. Test reads that should work, writes that should fail, device access that should be absent, and network families that should be blocked. Send the service a normal workload and a malformed input. A hardening rule that breaks normal operation will be removed during an incident unless the failure is understood.

Check restart behavior. A service that cannot write its runtime directory may loop. A service that cannot resolve DNS may start successfully and fail only when a request arrives. A private user namespace can change ownership or mount behavior. Test the exact service manager version and kernel used on the fleet.

Use systemd-analyze security as a starting report, not as proof that a unit is secure. It can show exposed capabilities and suggestions, but it does not know the application's authorization model. A low exposure score does not justify running an unpatched service, and a high score does not mean the service is harmless.

Keep privilege and network boundaries separate

NoNewPrivileges= and capability restrictions limit post-exploit escalation. RestrictAddressFamilies= reduces the socket types the service can create, but it does not prove that an allowed network connection is authorized. IPAddressAllow= and related network controls can narrow reachability when the service manager supports them, but they need a tested DNS and proxy path.

Run the service under a dedicated account. Keep secrets in files or credentials with permissions that match that account. Do not grant a broad read-only mount to a service that needs one configuration file. Do not make a whole directory writable because a single cache file needs to change.

The unit should also define restart limits, memory limits, and a clear working directory. Hardening that causes a restart loop is an availability problem. A service that restarts forever while a monitoring system treats the process as healthy can hide a broken policy.

Monitor the controls that matter

Collect unit start failures, denied file writes, namespace warnings, and unexpected child processes. Alert on a change from the expected service manager configuration. Keep a baseline of accessible paths and device nodes for the service account. After a kernel or systemd upgrade, repeat the tests because the implementation can change.

Treat the unit file as code. Review changes for broadened paths, added capabilities, new devices, and disabled protections. Record the reason for each exception. The goal is not the most restrictive-looking configuration. The goal is a service that can do its job while the remaining access is deliberate and observable.

References