Running services as root is a security risk. If a process gets compromised, the attacker gains full control of the system. Systemd provides several ways to run services as non-root users – from the simple User= directive to full sandboxing with DynamicUser= and security hardening options. These features let you follow the principle of least privilege without writing wrapper scripts or sudo hacks.
This guide covers every method for running systemd services without root on Ubuntu 24.04, Debian 13, RHEL 10, and Rocky Linux 10. We go through dedicated service users, system-level User= directives, DynamicUser=, user-level services with systemd --user, login lingering, security hardening directives, and ambient capabilities for binding to privileged ports. The full reference for execution environment options is in the systemd.exec documentation.
Prerequisites
- A Linux server running Ubuntu 24.04, Debian 13, RHEL 10, or Rocky Linux 10
- Root or sudo access (needed for creating system services and users)
- Basic familiarity with managing systemd services with systemctl
- A sample application or script to run as a service (we use a Python HTTP server in examples)
Step 1: Create a Dedicated Service User
The first step is creating a dedicated system user for your service. System users have no login shell and no home directory by default – they exist only to own and run a specific process. This limits the blast radius if the service is compromised.
Create a system user called myapp with no login shell:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp
Verify the user was created and confirm it has no login shell:
id myapp
The output shows the user’s UID and GID in the system range (typically below 1000):
uid=998(myapp) gid=998(myapp) groups=998(myapp)
If your application needs to write data, create a directory and assign ownership:
sudo mkdir -p /opt/myapp/data
sudo chown -R myapp:myapp /opt/myapp
Step 2: Write a Systemd Service Unit with User= and Group=
The most common way to run a systemd service as non-root is the User= and Group= directives in the [Service] section. Systemd starts the process as the specified user instead of root – no sudo needed, no shell wrapper required.
Create a unit file for the service:
sudo vi /etc/systemd/system/myapp.service
Add the following service definition:
[Unit]
Description=My Application Service
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 -m http.server 8080
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
The key directives here are User=myapp and Group=myapp. Systemd drops privileges to this user before executing the command in ExecStart=. The process cannot escalate back to root.
Reload systemd, enable, and start the service:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp.service
Check that the service is running as the myapp user:
systemctl status myapp.service
The status output confirms the service is active and shows the process owner:
● myapp.service - My Application Service
Loaded: loaded (/etc/systemd/system/myapp.service; enabled; preset: enabled)
Active: active (running) since Sun 2026-03-22 10:15:32 UTC; 5s ago
Main PID: 4521 (python3)
Tasks: 1 (limit: 4647)
Memory: 12.3M
CPU: 89ms
CGroup: /system.slice/myapp.service
└─4521 /usr/bin/python3 -m http.server 8080
You can also verify the process owner directly with ps:
ps -eo user,pid,cmd | grep myapp
This confirms the Python process runs under the myapp user, not root:
myapp 4521 /usr/bin/python3 -m http.server 8080
Step 3: Use DynamicUser= for Sandboxed Services
DynamicUser=yes tells systemd to create a temporary, ephemeral user for the service at runtime. The user is allocated from a pool of UIDs and removed when the service stops. This is ideal for stateless services that do not need persistent file ownership – no manual user creation, no cleanup.
Create a unit file that uses dynamic users:
sudo vi /etc/systemd/system/myapp-dynamic.service
Add the following configuration:
[Unit]
Description=My App with Dynamic User
After=network.target
[Service]
Type=simple
DynamicUser=yes
StateDirectory=myapp-dynamic
ExecStart=/usr/bin/python3 -m http.server 9090
Restart=on-failure
[Install]
WantedBy=multi-user.target
When DynamicUser=yes is set, systemd automatically enables several security features: ProtectSystem=strict, ProtectHome=yes, and PrivateTmp=yes. The service gets a minimal, locked-down environment by default.
The StateDirectory=myapp-dynamic directive creates /var/lib/myapp-dynamic owned by the dynamic user. This is the only writable persistent directory the service gets. Use CacheDirectory= and LogsDirectory= for other writable paths.
Start and verify:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp-dynamic.service
systemctl status myapp-dynamic.service
Step 4: Run User-Level Services with systemd –user
System services in /etc/systemd/system/ require root to install. User-level services run entirely under your own user account – no root or sudo needed at all. They are managed with systemctl --user and live in ~/.config/systemd/user/.
This is useful for developers running background processes, personal services, or any workload that does not need system-wide access. The services start when you log in and stop when you log out (unless you enable linger – covered in the next step).
Create the user service directory and unit file:
mkdir -p ~/.config/systemd/user
Create the unit file:
vi ~/.config/systemd/user/myapp-user.service
Add the following service definition (note: no User= directive needed since it runs as your user):
[Unit]
Description=My User-Level Application
[Service]
Type=simple
WorkingDirectory=%h/myapp
ExecStart=/usr/bin/python3 -m http.server 8081
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
The %h specifier expands to the user’s home directory. The WantedBy=default.target is the user-level equivalent of multi-user.target.
Reload and start the user service:
systemctl --user daemon-reload
systemctl --user enable --now myapp-user.service
Check its status:
systemctl --user status myapp-user.service
The output shows the service running under your user account without any root involvement:
● myapp-user.service - My User-Level Application
Loaded: loaded (/home/jmutai/.config/systemd/user/myapp-user.service; enabled; preset: enabled)
Active: active (running) since Sun 2026-03-22 10:20:00 UTC; 3s ago
Main PID: 5102 (python3)
Tasks: 1 (limit: 4647)
Memory: 10.1M
CPU: 72ms
CGroup: /user.slice/user-1000.slice/[email protected]/app.slice/myapp-user.service
└─5102 /usr/bin/python3 -m http.server 8081
View logs for user services with journalctl --user:
journalctl --user -u myapp-user.service -f
Step 5: Enable Linger for Persistent User Services
By default, user services stop when the user logs out. This is a problem for background daemons that need to run 24/7. The loginctl enable-linger command tells systemd to keep your user instance running even after all sessions end.
Enable linger for your user account:
sudo loginctl enable-linger $USER
Verify linger is enabled:
loginctl show-user $USER --property=Linger
The output confirms linger is active:
Linger=yes
With linger enabled, your systemctl --user services start at boot and survive logouts. This makes user-level services viable for production workloads where you do not want to run as root but still need persistence.
To disable linger later:
sudo loginctl disable-linger $USER
Step 6: Security Hardening Directives for Non-Root Services
Running as non-root is a good start, but systemd offers additional sandboxing directives that restrict what the service process can do. These are set in the [Service] section and work with both User= and DynamicUser= services. The full list is documented in the systemd.exec man page.
Here is a hardened service unit that combines non-root execution with multiple security restrictions:
sudo vi /etc/systemd/system/myapp-hardened.service
Add the following configuration with security directives:
[Unit]
Description=Hardened Application Service
After=network.target
[Service]
Type=simple
User=myapp
Group=myapp
WorkingDirectory=/opt/myapp
ExecStart=/usr/bin/python3 -m http.server 8080
# Filesystem restrictions
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/opt/myapp/data
# Privilege restrictions
NoNewPrivileges=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
# Network restrictions
RestrictAddressFamilies=AF_INET AF_INET6
ProtectHostname=yes
# System call filtering
SystemCallFilter=@system-service
SystemCallArchitectures=native
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
Breaking down the key directives:
ProtectSystem=strict– Mounts the entire filesystem as read-only. Only paths listed inReadWritePaths=are writableProtectHome=yes– Makes/home,/root, and/run/userinaccessible to the servicePrivateTmp=yes– Gives the service its own/tmpdirectory, isolated from other processesNoNewPrivileges=yes– Prevents the process (and child processes) from gaining additional privileges through setuid, setgid, or file capabilitiesSystemCallFilter=@system-service– Limits system calls to the set needed by typical services, blocking dangerous calls likerebootormount
Use systemd-analyze security to audit how well your service is hardened:
systemd-analyze security myapp-hardened.service
This produces a score from 0 (fully exposed) to 10 (fully hardened), with a breakdown of each security feature and whether it is enabled:
NAME DESCRIPTION EXPOSURE
✓ PrivateDevices= Service has no access to hardware devices 0.2
✓ PrivateTmp= Service uses private /tmp 0.1
✓ ProtectControlGroups= Service cannot modify control groups 0.0
✓ ProtectHome= Service has no access to home dirs 0.1
✓ ProtectKernelModules= Service cannot load kernel modules 0.0
✓ ProtectKernelTunables= Service cannot alter kernel settings 0.0
✓ ProtectSystem= Service has strict fs protection 0.1
...
→ Overall exposure level for myapp-hardened.service: 2.1 OK
A score below 3.0 is considered well-hardened. If you see high exposure scores, add more restrictive directives based on the report output.
Step 7: Grant Specific Capabilities with AmbientCapabilities
Some services need specific root-like abilities without full root access. The most common case is binding to ports below 1024 (like port 80 or 443). Linux capabilities let you grant fine-grained privileges to a non-root process. The AmbientCapabilities= directive passes these capabilities to the service.
To allow a non-root service to bind to port 80, add these directives to the [Service] section:
[Service]
User=myapp
Group=myapp
AmbientCapabilities=CAP_NET_BIND_SERVICE
CapabilityBoundingSet=CAP_NET_BIND_SERVICE
ExecStart=/usr/bin/python3 -m http.server 80
AmbientCapabilities= grants the capability to the process. CapabilityBoundingSet= restricts the maximum set of capabilities the process can ever obtain – setting it to the same value means the process can only have CAP_NET_BIND_SERVICE and nothing else.
Other useful capabilities for non-root services:
CAP_NET_RAW– Send raw packets (needed for ping, packet capture)CAP_DAC_READ_SEARCH– Read any file regardless of permissions (for backup tools)CAP_SYS_TIME– Set the system clock (for NTP services)CAP_CHOWN– Change file ownership
Always combine AmbientCapabilities= with NoNewPrivileges=yes and CapabilityBoundingSet= to prevent privilege escalation. Grant only the specific capability needed – never grant CAP_SYS_ADMIN as it is essentially equivalent to root.
Step 8: Practical Example – Run a Web App as Non-Root
Let’s put everything together with a real-world example: running a Python Flask application on port 8080 as a non-root user with full security hardening. This pattern applies to any web application – Node.js, Go, Java, or Ruby.
Create the application user and directory structure:
sudo useradd --system --no-create-home --shell /usr/sbin/nologin webapp
sudo mkdir -p /opt/webapp/{app,data,logs}
sudo chown -R webapp:webapp /opt/webapp
Create the systemd unit file with all security hardening options:
sudo vi /etc/systemd/system/webapp.service
Add the following production-ready configuration:
[Unit]
Description=Production Web Application
After=network.target
Wants=network-online.target
[Service]
Type=simple
User=webapp
Group=webapp
WorkingDirectory=/opt/webapp/app
ExecStart=/opt/webapp/app/venv/bin/python -m gunicorn --bind 0.0.0.0:8080 --workers 4 app:app
# Restart policy
Restart=on-failure
RestartSec=5
StartLimitIntervalSec=60
StartLimitBurst=3
# Filesystem hardening
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/opt/webapp/data /opt/webapp/logs
# Privilege hardening
NoNewPrivileges=yes
PrivateDevices=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectControlGroups=yes
ProtectHostname=yes
LockPersonality=yes
RestrictRealtime=yes
RestrictSUIDSGID=yes
RemoveIPC=yes
# Network hardening
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX
IPAddressDeny=any
IPAddressAllow=0.0.0.0/0 ::/0
# System call filter
SystemCallFilter=@system-service
SystemCallArchitectures=native
# Resource limits
MemoryMax=512M
TasksMax=100
[Install]
WantedBy=multi-user.target
This unit file combines every technique covered in this guide. The service runs as the webapp user with strict filesystem protection, kernel hardening, system call filtering, and resource limits. If the application gets compromised, the attacker has minimal access to the rest of the system.
Deploy and verify the service:
sudo systemctl daemon-reload
sudo systemctl enable --now webapp.service
systemctl status webapp.service
Run a security audit on the service to check hardening coverage:
systemd-analyze security webapp.service
If you need the application to listen on port 80 or 443 directly (without a reverse proxy), add AmbientCapabilities=CAP_NET_BIND_SERVICE and CapabilityBoundingSet=CAP_NET_BIND_SERVICE to the [Service] section. In most production setups, a reverse proxy like Nginx handles port 80/443 and forwards traffic to the application on a high port, making capabilities unnecessary. See our guide on running containers as systemd services for containerized deployments.
Systemd Security Directives Reference
This table summarizes the most useful security directives for non-root systemd services. Refer to the systemd.service documentation for the full list of service options.
| Directive | Purpose | Recommended Value |
|---|---|---|
User= | Run service as specified user | Dedicated service account |
Group= | Run service under specified group | Matching group for the user |
DynamicUser= | Create ephemeral user at runtime | yes for stateless services |
NoNewPrivileges= | Block privilege escalation via setuid/setgid | yes (always) |
ProtectSystem= | Make filesystem read-only | strict |
ProtectHome= | Hide /home, /root, /run/user | yes |
PrivateTmp= | Isolate /tmp from other processes | yes |
PrivateDevices= | Remove access to physical devices | yes |
ProtectKernelTunables= | Block writes to /proc and /sys | yes |
ProtectKernelModules= | Prevent loading kernel modules | yes |
ProtectControlGroups= | Block cgroup modifications | yes |
SystemCallFilter= | Restrict allowed system calls | @system-service |
AmbientCapabilities= | Grant specific Linux capabilities | Only what is needed |
CapabilityBoundingSet= | Limit maximum possible capabilities | Match AmbientCapabilities= |
RestrictAddressFamilies= | Limit network socket types | AF_INET AF_INET6 |
LockPersonality= | Lock execution domain | yes |
MemoryMax= | Cap memory usage | Based on application needs |
Conclusion
Running systemd services without root is straightforward once you know the available options. Use User= and Group= for most services, DynamicUser= for stateless workloads, and systemctl --user with linger for user-space daemons. Always add security hardening directives – they cost nothing in performance and drastically reduce the attack surface.
For production deployments, combine non-root execution with ProtectSystem=strict, NoNewPrivileges=yes, and SystemCallFilter=@system-service as a baseline. Use systemd-analyze security to audit each service and aim for an exposure score below 3.0. If you are managing many services, review how to filter systemd logs with journalctl for centralized monitoring and troubleshooting, and consider systemd timers for scheduled tasks that also need to run without root.