Skip to content
DevOps AI ToolKit
Newsletter
All guides
AI for DevOps Security & Hardening By James Joyner IV · · 9 min read

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=strict without ReadWritePaths. The whole filesystem is read-only and the service’s data/log/cache directory was never allowlisted.
  • State written under /etc or /usr. The app writes runtime state into a config or program directory that ProtectSystem now protects.
  • ProtectHome=true blocking a home path. The service reads/writes under /home or /root, which ProtectHome hides 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=true surprise. The service relied on a shared /tmp file 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

  1. Read the blocked path. The journal EROFS/Read-only file system line names the exact file the service could not write. That path is what you must either relocate or allowlist.

  2. Confirm the sandbox with systemctl show ... -p ProtectSystem -p ReadWritePaths. strict plus an empty ReadWritePaths means everything is read-only by default.

  3. Prefer StateDirectory= for service state. If the data belongs under /var/lib, let systemd create and own it:

    [Service]
    StateDirectory=myapp

    systemd creates /var/lib/myapp with correct ownership and makes it writable inside the namespace. Update the app to write there.

  4. Allowlist remaining legitimate writes narrowly with ReadWritePaths=:

    ProtectSystem=strict
    ReadWritePaths=/var/log/myapp /var/lib/myapp

    Keep the list as short as possible; each entry widens the writable surface.

  5. Create any missing target directories before starting, so namespace setup does not fail:

    sudo install -d -o myapp -g myapp /var/lib/myapp
  6. 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 to LogsDirectory=, and cache to CacheDirectory= rather than allowlisting arbitrary paths.
  • Keep ReadWritePaths= minimal; every added path reduces the protection ProtectSystem=strict provides.
  • 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.
  • Failed to set up mount namespacing: No such file or directory — a sandbox path that does not exist.
  • Operation not permitted from RestrictAddressFamilies/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.

Free download · 368-page PDF

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.