ReadWritePaths= is frequently added after a service fails under ProtectSystem=strict. It restores write access to a selected path inside the service's namespace. The setting does not mean the rest of the host is safe, and it cannot make a read-only filesystem writable by itself.
What the setting restores
ProtectSystem=strict makes most of the host filesystem read-only for the service. ReadWritePaths= then allows a path to retain the same access mode it has on the host. ReadOnlyPaths= can mark a larger tree read-only, with a writable child nested inside it when the service needs that layout.
The systemd manual describes two hard limits. First, a path listed in ReadWritePaths= cannot gain write access to a filesystem whose superblock is mounted read-only. The mount namespace can change the mount point's status, not the underlying storage policy. Second, host mounts that arrive later can appear below a path marked read-only and may still be writable inside the unit.
The second limit is the one most likely to surprise an operator. A service may start with a correct-looking namespace, then receive a new host mount below its writable exception. The application now has access that the original review did not account for.
Choose the narrowest path
The useful unit grants writes to the application's state directory, not to its whole runtime or home directory. A runtime socket should normally come from RuntimeDirectory=. A cache belongs in CacheDirectory=. Those settings create managed paths with ownership and lifecycle rules. A broad ReadWritePaths=/var avoids a startup failure and leaves every writable subdirectory available.
The path list supports a - prefix for missing paths and a + prefix for paths relative to a configured root directory. Those options are convenient for templates and portable units, but they also make silent absence harder to spot. A service that expects its state directory should fail when the directory is missing, unless the application has an explicit recovery path.
Privilege can undo the namespace
The mount namespace is not a permanent barrier. The systemd manual recommends removing or filtering CAP_SYS_ADMIN and mount system calls when the service does not need them. A process that can mount a filesystem can alter its view. A process with enough authority can also change host state.
The common hardening baseline therefore uses several settings together:
[Service]
User=app
ProtectSystem=strict
ReadOnlyPaths=/
ReadWritePaths=/var/lib/example /run/example
NoNewPrivileges=yes
CapabilityBoundingSet=
SystemCallFilter=~@mount
UMask=0027
The example still depends on the application's directories and the service unit's existing state. The empty capability bounding set will break some services. Monitor the service's system call failures and capability checks instead of assuming the policy is portable.
Verify the effective paths
Check the expanded unit and the running process:
systemctl cat example.service
systemctl show example.service -p ProtectSystem -p ReadWritePaths -p ReadOnlyPaths
systemd-analyze security example.service
Then perform a write test in the approved directory and a read-only test in a directory that should remain protected. Use disposable data. Do not test by changing a production database or deleting a live file.
For the live service, inspect its mount namespace from the host:
nsenter --target "$(systemctl show -p MainPID --value example.service)" --mount mount
The command itself needs privileges and should be logged. It reveals the service's view, not the host's complete mount table.
Record exceptions
Every writable path should have an owner and a reason. When a package upgrade adds a file below that path, the service may write to it. When a storage administrator adds a mount, the namespace can change. A quarterly review should compare the unit's writable paths with the application's current data model.
ReadWritePaths= is most effective when it is an exception list with a short explanation. Treating it as a general fix for read-only errors turns a narrow control into a broad bypass.