Run systemd Service as a Specific User and Group in Linux

Create a system-level systemd service that starts at boot and runs as a dedicated non-root user and group on Ubuntu, Debian, Fedora, RHEL, Rocky Linux, AlmaLinux, CentOS Stream and Oracle Linux.

Published

Updated

Read time 15 min read

Reviewed byDeepak Prasad

Run a systemd system service as a dedicated non-root user and group on Linux

A system service managed by PID 1 normally runs as root when you do not configure an execution identity. You can instead place User= and Group= in the [Service] section of a custom unit under /etc/systemd/system so the long-running process starts as a dedicated non-root account.

Those directives come from systemd itself, not from a single vendor distribution. The walkthrough below was run on Rocky Linux 10.2. The core User=, Group=, ExecStart=, and systemctl workflow is portable across current systemd-based distributions—including Ubuntu, Debian, Fedora, RHEL, Rocky Linux, AlmaLinux, CentOS Stream, Oracle Linux, openSUSE, and Arch Linux. Availability and behaviour of newer sandboxing, credential, and directory-management directives can depend on the installed systemd version.

Tested on: Rocky Linux 10.2 (Red Quartz); systemd 257; service account golinuxcloud; executable /usr/local/bin/golinuxcloud-worker; unit golinuxcloud-worker.service.

NOTE
This article covers a system service stored under /etc/systemd/system. A service managed with systemctl --user is a different configuration model and is compared briefly below.

Quick reference

What you need Command or directive
Run the main process as a named account User=account and Group=account in [Service]
Create a non-login service account useradd --system --user-group --shell "$(command -v nologin)" account
Install a custom system unit /etc/systemd/system/myapp.service
Find a packaged unit path systemctl show example.service -p FragmentPath -p DropInPaths
Override a packaged unit safely systemctl edit packaged.service
Create runtime and state directories RuntimeDirectory=myapp and StateDirectory=myapp
Reload after manual unit edits systemctl daemon-reload
Confirm the running identity systemctl show myapp.service -p User -p Group -p MainPID

Packaged units normally live in a vendor unit directory such as /usr/lib/systemd/system, while administrator units and overrides belong under /etc/systemd/system.


systemd compatibility across Linux distributions

User= and Group= are systemd directives rather than Debian- or Red Hat-specific features. Administrator-created system units and drop-ins should be placed under /etc/systemd/system on mainstream systemd distributions.

Distribution packages normally install vendor units under /usr/lib/systemd/system; some systems may also expose or recognize /lib/systemd/system. Use systemctl cat service-name or systemctl show -p FragmentPath service-name to find the active unit instead of assuming its vendor path.

Distribution family Examples Administrator unit path Core workflow
Debian family Debian, Ubuntu, Linux Mint /etc/systemd/system User=, Group=, systemctl
Enterprise Linux RHEL, Rocky, AlmaLinux, CentOS Stream, Oracle Linux /etc/systemd/system Same
Fedora Fedora Linux /etc/systemd/system Same
SUSE family openSUSE Leap, Tumbleweed, SLES /etc/systemd/system Same
Arch family Arch Linux and derivatives using systemd /etc/systemd/system Same

No extra repository is normally required. systemd and the user-management utilities ship with the base operating system. If you are new to unit files, read the beginner systemd overview before creating a custom service.

Check the installed systemd version when you rely on newer directives:

bash
systemd --version

Sample output:

output
systemd 257 (257-23.el10_2.1.rocky.0.1-ga8848ef)

For a specific execution directive, consult the manual on your system:

bash
man systemd.exec

Minor differences between distributions are environmental, not syntactic:

  • network-online.target ordering works the same in the unit file, but the wait-online helper that must be enabled differs (systemd-networkd-wait-online on some Debian servers, NetworkManager-wait-online on many desktops and Enterprise Linux hosts).
  • SELinux is common on RHEL, Rocky Linux, AlmaLinux, Fedora and Oracle Linux. Ubuntu and Debian may use AppArmor instead; the User= / Group= unit syntax is unchanged.

System service, user service, or delegated systemctl access?

Requirement Correct approach
Start at boot and run as a dedicated non-root account System service with User= and Group=
Run only inside a user's systemd manager systemctl --user
Continue a user service when the user logs out User service plus loginctl enable-linger
Allow a user to start or restart a system service Carefully scoped sudoers or PolicyKit rule
Create a temporary service account automatically Consider DynamicUser=yes

User= changes the identity of the service process. It does not grant that user permission to run systemctl start, edit the unit, or manage other system services. Keep systemctl --user, lingering, sudoers, and PolicyKit as separate topics unless your workload genuinely needs them.


Create a dedicated system user and group

Use a service-specific account rather than an interactive personal account. The useradd command can create a non-login system user with a matching primary group. Resolve the non-login shell path first:

bash
nologin_shell=$(command -v nologin)

command -v nologin commonly returns /usr/sbin/nologin on Debian-family systems and may resolve to /sbin/nologin on other distributions. Confirm that the returned path is listed in /etc/shells when local security policy requires it.

Create the service account with that shell:

bash
useradd --system --user-group --home-dir /var/lib/golinuxcloud --shell "$nologin_shell" golinuxcloud

The example uses the Shadow useradd implementation common on mainstream Linux distributions. Confirm the result with id and getent group. When a matching group is not created automatically, create it first with groupadd --system and pass it through useradd --gid:

bash
groupadd --system golinuxcloud
bash
useradd --system --gid golinuxcloud --home-dir /var/lib/golinuxcloud --shell "$(command -v nologin)" golinuxcloud

--home-dir records /var/lib/golinuxcloud as the account's home directory; it does not create the directory because this command does not use --create-home. In this example, systemd creates /var/lib/golinuxcloud when the service starts through StateDirectory=golinuxcloud.

The command exits silently on success when the account is new. Confirm the UID, primary group, and login shell:

bash
id golinuxcloud

Sample output:

output
uid=993(golinuxcloud) gid=993(golinuxcloud) groups=993(golinuxcloud)

Check that NSS can resolve the account before you reference it in a unit file:

bash
getent passwd golinuxcloud

Sample output:

output
golinuxcloud:x:993:993::/var/lib/golinuxcloud:/sbin/nologin
bash
getent group golinuxcloud

Sample output:

output
golinuxcloud:x:993:

A service account normally does not need an interactive password. The non-login shell blocks normal shell logins. The --system flag creates an account intended for daemons. The service name, user name, and group name can differ, but matching names are easier to audit. Group= sets the process primary group; add extra memberships with SupplementaryGroups= when a shared resource requires another group—see add or remove a user from a group for membership changes.

NOTE
The user and group named by User= and Group= must normally exist before the service starts. DynamicUser=yes is an alternative for services that do not require a permanent account, but treat it as an advanced option.

Prepare the service executable and writable directories

This walkthrough uses one consistent example:

Item Value
Service golinuxcloud-worker.service
User golinuxcloud
Group golinuxcloud
Executable /usr/local/bin/golinuxcloud-worker
Persistent data /var/lib/golinuxcloud
Runtime files /run/golinuxcloud

Create a simple long-running worker that prints its effective UID and GID, logs heartbeats to the journal, and handles SIGTERM cleanly:

bash
vi /usr/local/bin/golinuxcloud-worker
text
#!/bin/bash
set -euo pipefail

trap 'echo "golinuxcloud-worker: received SIGTERM, exiting"; exit 0' TERM INT

echo "golinuxcloud-worker: effective UID=$(id -u) GID=$(id -g) user=$(id -un) group=$(id -gn)"

while true; do
  echo "golinuxcloud-worker: heartbeat at $(date -Is)"
  sleep 30
done

Keep the executable owned by root. The service account needs read and execute permission only; root ownership stops a compromised service user from rewriting the program systemd launches:

bash
chown root:root /usr/local/bin/golinuxcloud-worker
bash
chmod 0755 /usr/local/bin/golinuxcloud-worker

Confirm ownership and mode:

bash
ls -l /usr/local/bin/golinuxcloud-worker

Sample output:

output
-rwxr-xr-x. 1 root root 292 Jul 11 16:48 /usr/local/bin/golinuxcloud-worker

Parent directories need search permission, and paths the process writes must be writable by the configured user or group. When the unit starts, systemd creates /run/golinuxcloud and /var/lib/golinuxcloud if necessary and makes them accessible to the configured service identity. Their default directory mode is normally 0755; use RuntimeDirectoryMode= or StateDirectoryMode= when the service requires a more restrictive mode. For private service state, 0750 or 0700 may be preferable:

ini
RuntimeDirectoryMode=0750
StateDirectoryMode=0750

Create the systemd service as a specific user and group

Create the unit file under the administrator unit directory:

bash
vi /etc/systemd/system/golinuxcloud-worker.service
ini
[Unit]
Description=GoLinuxCloud worker running as a non-root user

[Service]
Type=simple
User=golinuxcloud
Group=golinuxcloud
ExecStart=/usr/local/bin/golinuxcloud-worker
Restart=on-failure
RestartSec=5
RuntimeDirectory=golinuxcloud
StateDirectory=golinuxcloud

[Install]
WantedBy=multi-user.target

User= sets the service UNIX user identity. Group= sets its primary group; when omitted, systemd uses the user's default group. SupplementaryGroups= adds extra groups when shared resources require them. ExecStart= should normally use an absolute path. RuntimeDirectory= and StateDirectory= create service-owned paths under /run and /var/lib at unit start if they do not already exist. Restart=on-failure restarts after unexpected failures. WantedBy=multi-user.target wires the unit into a normal system boot target.

User=, Group=, supplementary credentials, runtime directories, state directories, and dynamic users are execution-environment features defined by systemd.exec.

Do not add these directives to a basic long-running service unless you have a specific reason:

ini
DefaultDependencies=no
TimeoutStartSec=0
RemainAfterExit=yes
After=network.target
WantedBy=default.target

DefaultDependencies=no removes standard ordering and shutdown protections. An unlimited startup timeout is unnecessary for a normal daemon. RemainAfterExit=yes suits one-shot units that should stay logically active after the main command exits—not a Type=simple worker. Network ordering belongs only when the program genuinely needs connectivity. When startup requires the locally configured network to be online, use Wants=network-online.target together with After=network-online.target, and enable the wait-online unit your distribution provides. This does not guarantee that a particular remote server, DNS name, or application endpoint is reachable; the program must still retry external connections. Add that ordering through a drop-in under /etc/systemd/system/<service>.service.d/ rather than editing packaged files.


Validate, start, and verify the service identity

Check the unit syntax before the first start:

bash
systemd-analyze verify /etc/systemd/system/golinuxcloud-worker.service

A valid unit produces no output and exits with status 0.

Reload systemd so it picks up the new file:

bash
systemctl daemon-reload

The reload succeeds silently when the unit directory is readable.

Start the service:

bash
systemctl start golinuxcloud-worker.service

Inspect state and recent log lines:

bash
systemctl status golinuxcloud-worker.service

Sample output:

output
● golinuxcloud-worker.service - GoLinuxCloud worker running as a non-root user
     Loaded: loaded (/etc/systemd/system/golinuxcloud-worker.service; disabled; preset: disabled)
     Active: active (running) since Sat 2026-07-11 16:48:11 IST; 2s ago
   Main PID: 5569 (golinuxcloud-wo)
      Tasks: 2 (limit: 24717)
     Memory: 1M (peak: 5.3M)
        CPU: 60ms
     CGroup: /system.slice/golinuxcloud-worker.service
             ├─5569 /bin/bash /usr/local/bin/golinuxcloud-worker
             └─5578 sleep 30

Jul 11 16:48:11 rocky1 systemd[1]: Started golinuxcloud-worker.service - GoLinuxCloud worker running as a non-root user.
Jul 11 16:48:11 rocky1 golinuxcloud-worker[5569]: golinuxcloud-worker: effective UID=993 GID=993 user=golinuxcloud group=golinuxcloud
Jul 11 16:48:11 rocky1 golinuxcloud-worker[5569]: golinuxcloud-worker: heartbeat at 2026-07-11T16:48:11+05:30

The journal shows the worker started as UID 993, matching the golinuxcloud account.

bash
journalctl -u golinuxcloud-worker.service -b

Sample output:

output
Jul 11 16:48:11 rocky1 systemd[1]: Started golinuxcloud-worker.service - GoLinuxCloud worker running as a non-root user.
Jul 11 16:48:11 rocky1 golinuxcloud-worker[5569]: golinuxcloud-worker: effective UID=993 GID=993 user=golinuxcloud group=golinuxcloud
Jul 11 16:48:11 rocky1 golinuxcloud-worker[5569]: golinuxcloud-worker: heartbeat at 2026-07-11T16:48:11+05:30

Read the identity configured on the unit:

bash
systemctl show golinuxcloud-worker.service -p User -p Group -p MainPID

Sample output:

output
MainPID=5569
User=golinuxcloud
Group=golinuxcloud

systemctl status alone does not prove the running process uses the expected credentials. Confirm the main PID:

bash
pid=$(systemctl show --value -p MainPID golinuxcloud-worker.service)
bash
ps -o user:20,group:20,pid,ppid,cmd -p "$pid"

Sample output:

output
USER                 GROUP                    PID    PPID CMD
golinuxcloud         golinuxcloud           5569       1 /bin/bash /usr/local/bin/golinuxcloud-worker

Inspect supplementary groups when they matter for file or socket access:

bash
grep -E '^(Uid|Gid|Groups):' /proc/"$pid"/status

Sample output:

output
Uid:	993	993	993	993
Gid:	993	993	993	993
Groups:	993

The real, effective, saved-set, and filesystem user and group IDs all match the dedicated service account.


Enable the service at boot and manage its lifecycle

Enable and start the unit in one step:

bash
systemctl enable --now golinuxcloud-worker.service

Sample output:

output
Created symlink '/etc/systemd/system/multi-user.target.wants/golinuxcloud-worker.service' → '/etc/systemd/system/golinuxcloud-worker.service'.

The symlink under multi-user.target.wants is what you expect for a conventional system service.

bash
systemctl is-enabled golinuxcloud-worker.service

Sample output:

output
enabled
bash
systemctl is-active golinuxcloud-worker.service

Sample output:

output
active

After a reboot, confirm the service is still active and still running as the configured account:

bash
systemctl status golinuxcloud-worker.service
bash
systemctl show golinuxcloud-worker.service -p User -p Group -p MainPID

Normal lifecycle commands use the same unit name. After editing the unit, run systemctl daemon-reload before restart:

bash
systemctl restart golinuxcloud-worker.service
bash
systemctl stop golinuxcloud-worker.service
bash
systemctl disable --now golinuxcloud-worker.service

Change the user or group of an existing service safely

Before editing a packaged unit, find where systemd loaded it from:

bash
systemctl show example.service -p FragmentPath -p DropInPaths

FragmentPath identifies the loaded main unit file. DropInPaths lists active override files. The loaded vendor path may differ by distribution; administrator overrides still belong under /etc/systemd/system.

Inspect the effective merged configuration:

bash
systemctl cat example.service

systemctl cat displays the combined source files systemd actually uses.

Do not edit vendor units in their package directory directly. On every systemd distribution, administrator-created units and drop-ins under /etc/systemd/system are the supported override path; drop-ins survive package upgrades more safely than editing packaged files.

Inspect the walkthrough unit:

bash
systemctl cat golinuxcloud-worker.service

Sample output:

output
# /etc/systemd/system/golinuxcloud-worker.service
[Unit]
Description=GoLinuxCloud worker running as a non-root user

[Service]
Type=simple
User=golinuxcloud
Group=golinuxcloud
ExecStart=/usr/local/bin/golinuxcloud-worker
Restart=on-failure
RestartSec=5
RuntimeDirectory=golinuxcloud
StateDirectory=golinuxcloud

[Install]
WantedBy=multi-user.target

For a packaged unit named example.service, open an editor drop-in:

bash
systemctl edit example.service

Example override:

ini
[Service]
User=golinuxcloud
Group=golinuxcloud

After a successful systemctl edit, current systemd versions normally reload the manager configuration automatically. Run systemctl daemon-reload explicitly when you create or modify unit files manually, or when you want to make the reload step visible in an operational procedure.

bash
systemctl daemon-reload
bash
systemctl restart example.service
WARNING
Do not add User= or Group= to a packaged service merely as generic hardening. Many services initially require root to bind ports, access devices, create namespaces, adjust limits, or perform privileged setup before dropping privileges internally. Check the vendor unit and application documentation first.

Changing the identity of an existing packaged service can break access to configuration files, PID or socket paths, log directories, privileged ports, devices, capabilities, or mandatory-access-control rules. Do not move every vendor service from root to non-root without checking its upstream design.


Manage permissions, mandatory access control, and service hardening

Three layers commonly block a non-root service:

  1. UNIX owner, group, and mode
  2. systemd execution restrictions
  3. Mandatory access control where enabled (SELinux on many Enterprise Linux and Fedora systems; AppArmor on many Ubuntu and Debian installs)

Walk the executable path and inspect permissions on disk:

bash
namei -l /usr/local/bin/golinuxcloud-worker

Sample output:

output
f: /usr/local/bin/golinuxcloud-worker
dr-xr-xr-x root root /
drwxr-xr-x root root usr
drwxr-xr-x root root local
drwxr-xr-x root root bin
-rwxr-xr-x root root golinuxcloud-worker

On SELinux-enabled distributions, inspect the security context:

bash
ls -ldZ /usr/local/bin/golinuxcloud-worker /var/lib/golinuxcloud /run/golinuxcloud

Sample output:

output
drwxr-xr-x. 2 golinuxcloud golinuxcloud system_u:object_r:var_run_t:s0   40 Jul 11 16:48 /run/golinuxcloud
-rwxr-xr-x. 1 root         root         unconfined_u:object_r:bin_t:s0  292 Jul 11 16:48 /usr/local/bin/golinuxcloud-worker
drwxr-xr-x. 2 golinuxcloud golinuxcloud system_u:object_r:var_lib_t:s0 4096 Jul 11 16:48 /var/lib/golinuxcloud

On Ubuntu or Debian with AppArmor, profile denials appear in the kernel log instead of SELinux AVC messages. A quick check:

bash
sudo journalctl -k | grep -i apparmor

The User= / Group= unit syntax is the same; only the access-control layer differs.

Confirm the service user can write state data:

bash
sudo -u golinuxcloud test -w /var/lib/golinuxcloud && echo "writable: yes"

Sample output:

output
writable: yes

If access fails under enforcing SELinux on RHEL, Rocky Linux, AlmaLinux or Fedora, inspect denials instead of disabling SELinux:

bash
ausearch -m AVC,USER_AVC -ts recent
bash
journalctl -t setroubleshoot

Mandatory access control decisions are independent of UID and GID ownership, so correct chown settings alone do not guarantee access.

Optional hardening directives can be added incrementally:

ini
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yes

StateDirectory= and related directory directives are preferable to manually opening broad filesystem paths. When combined with filesystem protection directives, systemd makes the managed service directories available according to the unit's execution environment. Add ReadWritePaths= only for additional writable paths not already covered by the directory directives.

Test after each addition; an overly strict sandbox can block legitimate application paths.

Evaluate exposure from systemd sandboxing directives:

bash
systemd-analyze security golinuxcloud-worker.service

Sample output:

output
NAME                                                        DESCRIPTION                                                             EXPOSURE
✗ RemoveIPC=                                                  Service user may leave SysV IPC objects around                               0.1
✗ RootDirectory=/RootImage=                                   Service runs within the host's root directory                                0.1
✓ User=/DynamicUser=                                          Service runs under a static non-root user identity
✗ NoNewPrivileges=                                            Service processes may acquire new privileges                                 0.2
...

systemd-analyze security estimates exposure from unit directives; it is not a complete vulnerability assessment.


Troubleshoot services running as the wrong user or group

Failed with status 217/USER or 216/GROUP

Confirm the account exists and NSS can resolve it:

bash
getent passwd golinuxcloud
bash
getent group golinuxcloud

Read the unit failure text:

bash
systemctl status golinuxcloud-worker.service
bash
journalctl -u golinuxcloud-worker.service -b

Likely causes include a misspelled user or group, an account missing from NSS, LDAP or SSSD identity unavailable at startup, an invalid numeric UID or GID, or a unit that starts before the remote identity provider is ready.

Failed with status 203/EXEC

Inspect the executable path, type, and shebang:

bash
ls -l /usr/local/bin/golinuxcloud-worker
bash
file /usr/local/bin/golinuxcloud-worker

Sample output:

output
/usr/local/bin/golinuxcloud-worker: Bourne-Again shell script, ASCII text executable
bash
head -n 1 /usr/local/bin/golinuxcloud-worker

Sample output:

output
#!/bin/bash
bash
namei -l /usr/local/bin/golinuxcloud-worker

Common causes include a wrong ExecStart= path, missing execute permission, an invalid interpreter, a parent directory without search permission, a filesystem mounted with noexec, or a mandatory access control denial.

Permission denied when writing files

Test write access as the service user:

bash
sudo -u golinuxcloud test -w /var/lib/golinuxcloud
bash
ls -ld /var/lib/golinuxcloud

On SELinux-enabled hosts, also check the security context:

bash
ls -ldZ /var/lib/golinuxcloud

Making the executable writable by the service account is not the correct fix. Grant write access only on the data, state, cache, runtime, or log directories the application needs.

Service starts but immediately becomes inactive

Distinguish a long-running Type=simple service from a command that exits normally, from Type=oneshot, and from RemainAfterExit=yes. Do not set RemainAfterExit=yes merely to keep a finished script looking active.

Service works manually but fails under systemd

Check for assumptions about interactive shell profiles, $PATH, current working directory, environment variables, TTY or password prompts, relative paths, or home-directory access. Use Environment=, EnvironmentFile=, WorkingDirectory=, and absolute paths where needed.

Symptom Likely cause Main check
217/USER Missing or unresolved user getent passwd
216/GROUP Missing or unresolved group getent group
203/EXEC Executable path or permission problem namei, file, shebang
Permission denied UNIX mode or MAC policy ls -lZ, ausearch, AppArmor logs
Runs as root Missing or overridden User= systemctl cat, systemctl show
Exits immediately Program is not long-running Review Type= and process behavior

Summary

Use a dedicated non-login service account, keep service executables root-owned, and set runtime credentials with User= and Group=. The core workflow applies on Ubuntu, Debian, Fedora, RHEL, Rocky Linux, AlmaLinux, CentOS Stream, Oracle Linux, openSUSE, Arch Linux and other current systemd-based distributions.

Prefer systemd-managed RuntimeDirectory= and StateDirectory= where they fit your layout. Verify the actual running PID—not only systemctl status. Override packaged units with drop-ins under /etc/systemd/system instead of editing vendor unit files. When access still fails, check UNIX permissions and any mandatory access control layer your distribution enforces.


References


Frequently Asked Questions

1. Does User= let the service account run systemctl start?

No. User= changes the UNIX identity of the service process only. It does not grant that account permission to manage systemd units, edit files under /etc/systemd/system, or start other system services. Use carefully scoped sudoers rules or PolicyKit when a human user must control a system unit.

2. When should I use systemctl --user instead of User=?

Use systemctl --user when the workload belongs to a logged-in user session manager, such as a desktop app or a per-user agent. Use a system service with User= when PID 1 should start the unit at boot and keep it running independently of interactive logins.

3. What is DynamicUser=yes?

DynamicUser=yes tells systemd to allocate a transient user and group for the service at runtime instead of requiring a permanent account in /etc/passwd. It works especially well with systemd-managed paths such as RuntimeDirectory=, StateDirectory=, CacheDirectory=, and LogsDirectory=. Use a permanent service account when external files, administrators, or other services need a stable named owner outside those managed paths.

4. Is the same User= syntax used on Ubuntu and RHEL?

Yes. User=, Group=, RuntimeDirectory=, and StateDirectory= are defined by systemd itself. Custom units belong under /etc/systemd/system on Ubuntu, Debian, Fedora, RHEL, Rocky Linux, AlmaLinux and other systemd-based distributions.
Deepak Prasad

R&D Engineer

Founder of GoLinuxCloud with more than 15 years of expertise in Linux, Python, Go, Laravel, DevOps, Kubernetes, Git, Shell scripting, OpenShift, AWS, Networking, and Security. With extensive …