How To

Run Systemd Services Without Root on Linux

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.

Original content from computingforgeeks.com - post 48456

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 in ReadWritePaths= are writable
  • ProtectHome=yes – Makes /home, /root, and /run/user inaccessible to the service
  • PrivateTmp=yes – Gives the service its own /tmp directory, isolated from other processes
  • NoNewPrivileges=yes – Prevents the process (and child processes) from gaining additional privileges through setuid, setgid, or file capabilities
  • SystemCallFilter=@system-service – Limits system calls to the set needed by typical services, blocking dangerous calls like reboot or mount

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.

DirectivePurposeRecommended Value
User=Run service as specified userDedicated service account
Group=Run service under specified groupMatching group for the user
DynamicUser=Create ephemeral user at runtimeyes for stateless services
NoNewPrivileges=Block privilege escalation via setuid/setgidyes (always)
ProtectSystem=Make filesystem read-onlystrict
ProtectHome=Hide /home, /root, /run/useryes
PrivateTmp=Isolate /tmp from other processesyes
PrivateDevices=Remove access to physical devicesyes
ProtectKernelTunables=Block writes to /proc and /sysyes
ProtectKernelModules=Prevent loading kernel modulesyes
ProtectControlGroups=Block cgroup modificationsyes
SystemCallFilter=Restrict allowed system calls@system-service
AmbientCapabilities=Grant specific Linux capabilitiesOnly what is needed
CapabilityBoundingSet=Limit maximum possible capabilitiesMatch AmbientCapabilities=
RestrictAddressFamilies=Limit network socket typesAF_INET AF_INET6
LockPersonality=Lock execution domainyes
MemoryMax=Cap memory usageBased 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.

Related Articles

pfSense How To Configure OpenVPN Server on pfSense / OPNsense Security Why Decentralized Tools Are Quietly Becoming the Backbone of Modern Cyber Resilience Networking Install Pritunl VPN Server on Ubuntu 24.04 / 22.04 CentOS Install OpenSSL 3.x from Source on RHEL 10 / Rocky Linux 10 / AlmaLinux 10

Leave a Comment

Press ESC to close