When several background services share a physical server, an application usually needs to write only to its state directory. Yet overly permissive directory ownership can also let it modify deployed application files. A dedicated service account establishes only the first boundary. Making the filesystem read-only from the service’s perspective, with write access retained only where necessary, further limits accidental changes. Acceptance testing must go beyond “the service started successfully”: it must also prove that writes to prohibited locations actually fail.
1. List required writes before configuring the sandbox
This walkthrough uses Ubuntu Server 24.04 LTS with systemd managing system-level services. You need administrator privileges and Python 3. All paths belong to a separate test setup; no existing production units are modified. Login credentials for an AmdMetal dedicated server are available in the control panel after payment. Use SSH to connect to Linux.
Start by separating application code, persistent state, runtime files, and temporary files. A deployment account should update the code, while the service account should have read-only access. The state directory holds data that must survive reboots. Track sockets, process ID files, and other runtime files separately from temporary files. If the application logs to standard output, the logging system can collect that output without opening an entire log directory tree for writes.
The key setting is ProtectSystem=strict. Within the service’s mount namespace, it makes the filesystem hierarchy read-only, with exceptions for certain API filesystems and other designated areas. ReadWritePaths then retains write access to specified directories. This does not change the host’s global mount state, nor is it a complete process isolation solution.
Read-only access does not provide confidentiality. Readable files are not automatically hidden, and network access is not automatically restricted. A write allowlist is no substitute for directory permissions, either.
2. Establish a permission baseline that distinguishes failure causes
The test uses two directories: one that must remain writable, and another that ordinary permissions allow the account to write to but the sandbox must block. This rules out a false positive caused by a lack of permissions in the first place. Before proceeding, confirm that no existing service uses the account name or paths below. These are test resources, not the machine’s default login account.
sudo useradd --system --no-create-home \
--shell /usr/sbin/nologin sandbox-check
sudo install -d -o root -g root -m 0755 \
/srv/sandbox-check
sudo install -d -o sandbox-check -g sandbox-check -m 0750 \
/srv/sandbox-check/blocked \
/var/lib/sandbox-check
sudo runuser -u sandbox-check -- /usr/bin/python3 -c \
'import tempfile, os; f, p = tempfile.mkstemp(dir="/srv/sandbox-check/blocked"); os.close(f); os.unlink(p)'
The final command should exit successfully. It creates a randomly named temporary file in the directory that will later be blocked, then immediately deletes it. If it fails, check directory traversal permissions, ACLs, and the underlying mount properties first. Until that baseline works, you cannot attribute later failures to the sandbox.
The deployed script itself must be owned by an administrator so that the service account cannot rewrite the verification logic. The test directory deliberately has an owner with write access solely to demonstrate where the restriction comes from. Production code directories must remain unwritable by the service account; do not carry this test-only exception into production.
3. Configure the write allowlist and a separate probe
Save the following script as /srv/sandbox-check/probe.py, set its owner to an administrator, and set its permissions to 0644. Writing to the allowed path must succeed. Writing to the blocked path must return a read-only filesystem error; any other error fails the acceptance test.
import errno
import os
import tempfile
def create_and_remove(directory):
fd, path = tempfile.mkstemp(prefix="probe-", dir=directory)
try:
with os.fdopen(fd, "w") as stream:
stream.write("sandbox-check\n")
stream.flush()
os.fsync(stream.fileno())
finally:
os.unlink(path)
create_and_remove("/var/lib/sandbox-check")
try:
create_and_remove("/srv/sandbox-check/blocked")
except OSError as exc:
if exc.errno != errno.EROFS:
raise
else:
raise RuntimeError("Write boundary was not enforced")
print("Allowed write passed; blocked write returned EROFS")
Random filenames avoid overwriting existing data. The fsync call checks the write operation chain; it does not constitute a performance test or a test of durability during power loss. The probe output reports the acceptance result without including credentials or application data.
Create /etc/systemd/system/sandbox-check.service:
[Unit]
Description=Filesystem sandbox verification
[Service]
Type=oneshot
User=sandbox-check
Group=sandbox-check
ExecStart=/usr/bin/python3 /srv/sandbox-check/probe.py
ProtectSystem=strict
ReadWritePaths=/var/lib/sandbox-check
PrivateTmp=true
NoNewPrivileges=true
PrivateTmp=true gives the service an isolated view of temporary directories. Therefore, “only one persistent directory is writable” does not mean “the process has only one writable location.” NoNewPrivileges=true restricts gaining additional privileges by executing programs, but it does not revoke file access permissions already granted.
4. Verify both successful writes and denied writes
Validate the unit first, then load and run it:
sudo systemd-analyze verify /etc/systemd/system/sandbox-check.service
sudo systemctl daemon-reload
sudo systemctl start sandbox-check.service
sudo systemctl show sandbox-check.service \
-p Result -p ExecMainStatus -p ProtectSystem -p ReadWritePaths
sudo journalctl -u sandbox-check.service -b --no-pager -n 30
Expect Result=success, ExecMainStatus=0, and the probe’s success message in the journal. It is normal for a one-shot service to show as not running after it finishes. This unit is not configured to run continuously or remain active after exit.
Inspecting configuration values alone does not prove that the boundary works. Valid evidence must include all three results: the same account can write outside the sandbox, a write to the allowed directory succeeds inside the sandbox, and a write to the blocked directory returns EROFS. An EACCES error proves only that ordinary permissions or another security policy denied access; it does not pass this acceptance test.
Test real application workflows for production services
Once the probe passes, prepare a separate hardening configuration for the actual service. Run an application test covering uploads, cache generation, state persistence, and temporary file cleanup. If the application updates data by writing a temporary file and then renaming it, the destination directory itself must also be writable. An exception for a single file is often insufficient.
Also check where symbolic links actually resolve. A link that appears in the configuration to be inside the state directory may ultimately point to a different, read-only path. Do not mask that problem by broadening permissions on the entire parent directory.
5. Troubleshoot, revert, and record the results
If the service fails before it starts, read the journal first to determine whether the mount namespace could not be created, a path is missing, or a configuration setting is misspelled. For failures after startup, investigate the location of the file operation: is the application unexpectedly writing cache files into the code directory, relying on shared temporary files, or requiring an additional runtime directory?
If a production service needs a runtime directory, use RuntimeDirectory to let systemd manage its lifecycle, then test actual access. Do not simply allowlist all of /var or /srv. Grant access individually according to confirmed write requirements.
After a configuration change, daemon-reload only reloads unit definitions. It does not apply the new sandbox to processes that are already running. A production service needs a controlled restart to create a new process environment, followed by another round of positive and negative tests.
To undo the experiment, stop the unit, remove the unit file created for this test, and reload the definitions. Clean up the test data directories and account separately, after confirming that no process is using them. Do not use a generic command that recursively deletes production directories. For a production service, restore the configuration saved before the changes, then restart and verify it.
Finally, record the unit’s effective configuration, the service account, the write allowlist, the probe’s exit status, and the results of real application tests. The acceptance goal is not a higher security score. It is successful completion of every required write, together with explainable, repeatable evidence of denial for every write that should be prohibited.
Frequently asked questions
Does ReadWritePaths grant otherwise missing write permissions?
No. It preserves writable paths inside the read-only filesystem sandbox without changing ownership, mode bits, or ACLs. The service account must also have ordinary filesystem permissions.
Does ProtectSystem=strict prevent services from reading secrets?
No. It primarily restricts writes; it does not automatically hide readable files or block network access. Secret permissions, path hiding, and network restrictions require separate controls.
AMD EPYC 7543P Dedicated Server
From $458.7/month