Security Error Guide: 'Read-only file system' After systemd ProtectSystem Hardening
Fix systemd 'Read-only file system' and namespace errors after ProtectSystem/ProtectHome sandboxing: diagnose blocked writes, add ReadWritePaths, and harden a service safely.
- #security-hardening
- #troubleshooting
- #errors
- #systemd
Exact Error Message
After adding sandboxing directives to a service unit, the daemon fails to write where it used to and the journal shows:
myapp[2143]: Error: EROFS: read-only file system, open '/etc/myapp/cache/session.db'
systemd[1]: myapp.service: Main process exited, code=exited, status=1/FAILURE
systemd[1]: myapp.service: Failed with result 'exit-code'.
A related namespace failure appears when a sandbox path does not exist:
systemd[1]: myapp.service: Failed to set up mount namespacing:
/run/systemd/unit-root/var/lib/myapp: No such file or directory
What the Error Means
ProtectSystem= and related directives (ProtectHome=, ReadOnlyPaths=, ProtectKernelTunables=) tell systemd to run the service inside a mount namespace where parts of the filesystem are mounted read-only. ProtectSystem=strict makes the entire filesystem read-only except for /dev, /proc, /sys and any paths you explicitly open with ReadWritePaths=. When the service then tries to write to a location it used to write to, the kernel returns EROFS (“Read-only file system”).
This is the sandbox doing exactly what you asked: minimizing the service’s writable surface. The error is operational — the hardening exposed a write the service genuinely needs that you have not yet allowlisted. The fix is to identify the legitimate writable paths and grant them narrowly with ReadWritePaths= (or relocate state to StateDirectory=), not to remove the protection.
Common Causes
ProtectSystem=strictwithoutReadWritePaths. The whole filesystem is read-only and the service’s data/log/cache directory was never allowlisted.- State written under
/etcor/usr. The app writes runtime state into a config or program directory thatProtectSystemnow protects. ProtectHome=trueblocking a home path. The service reads/writes under/homeor/root, whichProtectHomehides or makes read-only.- Missing directory for a namespace path. A
ReadWritePaths=/StateDirectory=target does not exist, so namespace setup fails before the service starts. - PID/socket file in a protected dir. The daemon writes its pidfile or unix socket to a now-read-only location.
PrivateTmp=truesurprise. The service relied on a shared/tmpfile that is now private to its namespace.
How to Reproduce the Error
On a test host, harden a simple unit that writes state under /etc, then start it:
sudo systemctl edit --full --force testwriter.service
# Set: ExecStart=/bin/sh -c 'echo hi > /etc/testwriter/state'
# ProtectSystem=strict
sudo systemctl daemon-reload
sudo systemctl start testwriter.service
sudo systemctl status testwriter.service --no-pager
testwriter.service: Main process exited, code=exited, status=1/FAILURE
sh[2210]: /bin/sh: 1: cannot create /etc/testwriter/state: Read-only file system
Diagnostic Commands
These read-only commands reveal the sandbox in effect and where the write was blocked.
# What sandboxing directives are active on the unit?
systemctl show myapp.service -p ProtectSystem -p ProtectHome \
-p ReadWritePaths -p StateDirectory -p PrivateTmp
# Read the failure and the exact path that was denied
sudo journalctl -u myapp.service --since '15 min ago' | grep -iE 'read-only|EROFS|namespac'
# Get systemd's own analysis of the unit's exposure and directives
systemd-analyze security myapp.service 2>/dev/null | head -40
# See the merged unit definition, including drop-ins
systemctl cat myapp.service
# Confirm whether the target dir exists and who owns it
ls -ld /var/lib/myapp /etc/myapp 2>/dev/null
# Which paths is the running service actually allowed to write?
systemctl show myapp.service -p ReadWritePaths -p ReadOnlyPaths
systemd-analyze security plus the journal EROFS line tell you both how locked-down the unit is and the precise path that needs to become writable.
Step-by-Step Resolution
-
Read the blocked path. The journal
EROFS/Read-only file systemline names the exact file the service could not write. That path is what you must either relocate or allowlist. -
Confirm the sandbox with
systemctl show ... -p ProtectSystem -p ReadWritePaths.strictplus an emptyReadWritePathsmeans everything is read-only by default. -
Prefer
StateDirectory=for service state. If the data belongs under/var/lib, let systemd create and own it:[Service] StateDirectory=myappsystemd creates
/var/lib/myappwith correct ownership and makes it writable inside the namespace. Update the app to write there. -
Allowlist remaining legitimate writes narrowly with
ReadWritePaths=:ProtectSystem=strict ReadWritePaths=/var/log/myapp /var/lib/myappKeep the list as short as possible; each entry widens the writable surface.
-
Create any missing target directories before starting, so namespace setup does not fail:
sudo install -d -o myapp -g myapp /var/lib/myapp -
Reload and restart, then verify the service writes successfully and the sandbox is still strong:
sudo systemctl daemon-reload && sudo systemctl restart myapp.service systemd-analyze security myapp.service | tail -1
Prevention and Best Practices
- Add sandboxing directives incrementally and start the service after each change so you catch each blocked write early.
- Move service state to
StateDirectory=, logs toLogsDirectory=, and cache toCacheDirectory=rather than allowlisting arbitrary paths. - Keep
ReadWritePaths=minimal; every added path reduces the protectionProtectSystem=strictprovides. - Run
systemd-analyze security <unit>after hardening to confirm the exposure score and that you have not over-opened the sandbox. - Use override drop-ins (
systemctl edit) for hardening so changes are reviewable and revertible.
Related Errors
Failed to set up mount namespacing: No such file or directory— a sandbox path that does not exist.Operation not permittedfromRestrictAddressFamilies/SystemCallFilter— a different systemd sandbox layer blocking syscalls.EACCES: permission denied— ordinary Unix permissions, not the read-only namespace.avc: denied— SELinux, a separate mechanism, covered in the security hardening guides.
Frequently Asked Questions
The directory is owned by the service user and 0755 — why is it still read-only? Ownership and mode are irrelevant when ProtectSystem=strict mounts the path read-only in the service’s namespace. You must add it to ReadWritePaths= or use StateDirectory=.
Should I just set ProtectSystem=false to fix it? No. That removes the read-only protection entirely. Allowlist the specific writable paths the service needs instead.
What is the difference between true, full, and strict? true makes /usr and /boot read-only; full adds /etc; strict makes the whole filesystem read-only except /dev, /proc, /sys, and explicit ReadWritePaths=.
Why does namespace setup fail with “No such file or directory”? A path referenced by ReadWritePaths= or BindPaths= does not exist. Create it before the service starts, or use StateDirectory= which systemd creates for you.
How do I confirm my hardening is actually strong? Run systemd-analyze security <unit>; it scores the unit and flags directives you could tighten further without breaking it.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.