GICT knowledge base
How to Restrict a Linux Service with systemd Security Settings
Published
A compromised application should not be able to rewrite its executable or browse users’ home directories. Systemd can restrict those actions for an individual service while leaving the rest of the server unchanged. A service-specific drop-in keeps these settings separate from the packaged unit, making them easier to inspect and remove.
Requirements
Use an Ubuntu 24.04 server with sudo access and an existing systemd application service. The examples use myapp.service, application files under /opt/myapp, and writable state under /var/lib/myapp. Substitute your service name and configure its storage path accordingly. If you need a server, an InterServer VPS provides a suitable Linux environment.
1. Inspect the service before changing it
Read the complete unit, including existing overrides:
$ sudo systemctl cat myapp.service
$ sudo systemctl show myapp.service -p User -p Group -p DynamicUser -p ExecStart -p WorkingDirectory -p FragmentPath -p DropInPaths
Identify the executable, account, configuration files, and directories the application writes to. Check its documentation for uploads, caches, sockets, and logs. Schedule a brief maintenance window because applying the restrictions requires restarting the service.
Review ExecStartPre= and ExecStartPost= commands too. Changing User= affects ordinary startup helpers as well as the main process. A helper that previously created directories as root may need replacement with a managed directory setting, rather than broader permissions for the whole service.
Keep executable files and configuration outside home directories. An application installed in /home/deploy, including a virtual environment there, will become inaccessible with ProtectHome=yes.
If the service already uses a dedicated account, retain it. If it uses DynamicUser=yes, do not combine that arrangement with this static-account example without adapting its ownership and storage design. Applications that genuinely require root privileges need a service-specific policy rather than these settings unchanged.
2. Prepare a dedicated account and storage
For a service currently running as root without needing root privileges, check whether the proposed account exists:
$ getent passwd myapp
If it does not exist, create a system account without a login shell:
$ sudo useradd --system --user-group --home-dir /nonexistent --no-create-home --shell /usr/sbin/nologin myapp
Give that account read access to required configuration and execute access to the application. Do not make it the owner of the executable or its installation directory. Avoid recursively changing ownership across the application tree just to resolve one permission error.
Configure the application to store persistent writable data in /var/lib/myapp. Systemd will create this directory through StateDirectory=. Existing data needs an appropriate ownership review before the switch; use a new, application-specific directory rather than pointing this setting at shared storage.
Prefer logging to the journal where supported. Applications that insist on a log file, cache directory, or runtime socket may need additional managed directories such as LogsDirectory=, CacheDirectory=, or RuntimeDirectory=. Add only the paths the application actually uses.
3. Add a security drop-in
Create the override directory and open a new drop-in:
$ sudo install -d -m 0755 /etc/systemd/system/myapp.service.d
$ sudo nano /etc/systemd/system/myapp.service.d/90-security.conf
If that filename already exists, preserve its contents before editing. Enter the following, retaining the existing dedicated account name if it differs:
[Service]
User=myapp
Group=myapp
NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
StateDirectory=myapp
StateDirectoryMode=0750
UMask=0077
NoNewPrivileges=yes prevents the service and its descendants from gaining additional privileges through execution, including set-user-ID programs. It does not remove privileges the process already has, which is why the service account matters.
ProtectSystem=strict makes most of the filesystem read-only inside the service’s mount namespace. The state directory is automatically excluded from that read-only treatment. Device and API filesystems have separate handling; this directive is not a complete isolation boundary.
ProtectHome=yes hides /home, /root, and /run/user from the service. PrivateTmp=yes supplies private /tmp and /var/tmp directories, so software cannot depend on files another service placed in the host’s temporary directory.
StateDirectoryMode=0750 controls the managed directory’s permissions. UMask=0077 removes group and other permission bits from newly created files, subject to how the application subsequently manages permissions. Neither setting retroactively repairs permissions on existing files.
4. Apply the settings and verify normal operation
Check the unit syntax, reload systemd, and restart:
$ sudo systemd-analyze verify myapp.service
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp.service
$ sudo systemctl status myapp.service --no-pager
Drop-ins are processed in filename order, and a later file can change the same setting. Existing list settings can also accumulate entries. If the effective values differ from this file, inspect DropInPaths and the complete unit again before adding more overrides. Do not delete another administrator’s drop-in to force your preferred result.
Resolve reported configuration errors before restarting. Reloading alone changes systemd’s configuration but does not rebuild the sandbox of an already running process.
Inspect the effective values and recent messages:
$ sudo systemctl show myapp.service -p User -p NoNewPrivileges -p ProtectSystem -p ProtectHome -p PrivateTmp -p StateDirectory -p UMask
$ sudo journalctl -u myapp.service -n 50 --no-pager
Now perform a normal application operation that saves data, then read it back. Confirm that uploads, background jobs, and any required network connections still work. A running process is not sufficient evidence that every application feature works.
Commands launched with sudo -u myapp use the account but do not inherit the service’s filesystem namespace. Use the actual application to verify behavior inside its sandbox.
5. Resolve blocked writes without removing protection
A “Read-only file system” error usually means the application is writing outside its permitted locations. Prefer moving that data into the managed state directory. If an existing directory must remain writable, add a narrowly scoped exception to the same drop-in:
ReadWritePaths=/srv/myapp/uploads
Create that directory first and ensure the service account has normal filesystem permission to write there. After editing, repeat the reload, restart, and application check. ReadWritePaths= relaxes the namespace restriction; it does not grant ownership, bypass Unix permissions, or make an underlying read-only filesystem writable.
For “Permission denied,” inspect ownership and parent-directory traversal permissions. For a missing executable, check whether it lives beneath a hidden home directory. Do not respond by allowing writes to /, /etc, or the entire application installation.
You can inspect additional hardening suggestions with:
$ sudo systemd-analyze security myapp.service
These filesystem controls do not filter outbound connections or prevent the application from reading every world-readable system file. Network restrictions, secret handling, and application authorization remain separate concerns. Avoid putting credentials in a home directory and disabling ProtectHome merely to expose them; select a narrowly accessible configuration path instead.
The exposure score is a checklist, not proof of security. Keep network access and other capabilities that the application requires; unrelated restrictions can break legitimate functionality.
6. Roll back only this drop-in
To undo these changes, move the added file outside the drop-in directory, then reload and restart:
$ sudo mv /etc/systemd/system/myapp.service.d/90-security.conf /etc/systemd/system/myapp.security.conf.disabled
$ sudo systemctl daemon-reload
$ sudo systemctl restart myapp.service
Choose a different backup filename if that destination already exists. Other overrides remain in place. The service account and state data are retained; restore any application storage settings or ownership changes separately if needed.
Related guides
Plan a Safe Redis-to-Valkey Migration
Plan a Redis-to-Valkey migration: check version compatibility, rehearse recovery, stop writers for cutover and protect against lost writes during rollback.
Linux AdministrationHow to Restrict an SSH Key to One Command
Restrict an SSH key to one command with authorized_keys options. Protect the command and check that unwanted shell and forwarding access are denied.
Linux AdministrationHow to Schedule Tasks with systemd Timers
Schedule tasks with systemd timers. Verify service results separately from timer status, test missed-run handling and use an explicit UTC schedule.
Linux AdministrationHow to Track Configuration File Changes in Linux with auditd
Track configuration edits on Ubuntu with persistent auditd rules, identify login accounts and processes, search events, and remove rules safely.
