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; unitgolinuxcloud-worker.service.
/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:
systemd --versionSample output:
systemd 257 (257-23.el10_2.1.rocky.0.1-ga8848ef)For a specific execution directive, consult the manual on your system:
man systemd.execMinor differences between distributions are environmental, not syntactic:
network-online.targetordering works the same in the unit file, but the wait-online helper that must be enabled differs (systemd-networkd-wait-onlineon some Debian servers,NetworkManager-wait-onlineon 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:
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:
useradd --system --user-group --home-dir /var/lib/golinuxcloud --shell "$nologin_shell" golinuxcloudThe 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:
groupadd --system golinuxclouduseradd --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:
id golinuxcloudSample 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:
getent passwd golinuxcloudSample output:
golinuxcloud:x:993:993::/var/lib/golinuxcloud:/sbin/nologingetent group golinuxcloudSample 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.
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:
vi /usr/local/bin/golinuxcloud-worker#!/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
doneKeep 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:
chown root:root /usr/local/bin/golinuxcloud-workerchmod 0755 /usr/local/bin/golinuxcloud-workerConfirm ownership and mode:
ls -l /usr/local/bin/golinuxcloud-workerSample output:
-rwxr-xr-x. 1 root root 292 Jul 11 16:48 /usr/local/bin/golinuxcloud-workerParent 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:
RuntimeDirectoryMode=0750
StateDirectoryMode=0750Create the systemd service as a specific user and group
Create the unit file under the administrator unit directory:
vi /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.targetUser= 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:
DefaultDependencies=no
TimeoutStartSec=0
RemainAfterExit=yes
After=network.target
WantedBy=default.targetDefaultDependencies=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:
systemd-analyze verify /etc/systemd/system/golinuxcloud-worker.serviceA valid unit produces no output and exits with status 0.
Reload systemd so it picks up the new file:
systemctl daemon-reloadThe reload succeeds silently when the unit directory is readable.
Start the service:
systemctl start golinuxcloud-worker.serviceInspect state and recent log lines:
systemctl status golinuxcloud-worker.serviceSample 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:30The journal shows the worker started as UID 993, matching the golinuxcloud account.
journalctl -u golinuxcloud-worker.service -bSample 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:30Read the identity configured on the unit:
systemctl show golinuxcloud-worker.service -p User -p Group -p MainPIDSample output:
MainPID=5569
User=golinuxcloud
Group=golinuxcloudsystemctl status alone does not prove the running process uses the expected credentials. Confirm the main PID:
pid=$(systemctl show --value -p MainPID golinuxcloud-worker.service)ps -o user:20,group:20,pid,ppid,cmd -p "$pid"Sample output:
USER GROUP PID PPID CMD
golinuxcloud golinuxcloud 5569 1 /bin/bash /usr/local/bin/golinuxcloud-workerInspect supplementary groups when they matter for file or socket access:
grep -E '^(Uid|Gid|Groups):' /proc/"$pid"/statusSample output:
Uid: 993 993 993 993
Gid: 993 993 993 993
Groups: 993The 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:
systemctl enable --now golinuxcloud-worker.serviceSample 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.
systemctl is-enabled golinuxcloud-worker.serviceSample output:
enabledsystemctl is-active golinuxcloud-worker.serviceSample output:
activeAfter a reboot, confirm the service is still active and still running as the configured account:
systemctl status golinuxcloud-worker.servicesystemctl show golinuxcloud-worker.service -p User -p Group -p MainPIDNormal lifecycle commands use the same unit name. After editing the unit, run systemctl daemon-reload before restart:
systemctl restart golinuxcloud-worker.servicesystemctl stop golinuxcloud-worker.servicesystemctl disable --now golinuxcloud-worker.serviceChange the user or group of an existing service safely
Before editing a packaged unit, find where systemd loaded it from:
systemctl show example.service -p FragmentPath -p DropInPathsFragmentPath 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:
systemctl cat example.servicesystemctl 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:
systemctl cat golinuxcloud-worker.serviceSample 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.targetFor a packaged unit named example.service, open an editor drop-in:
systemctl edit example.serviceExample override:
[Service]
User=golinuxcloud
Group=golinuxcloudAfter 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.
systemctl daemon-reloadsystemctl restart example.serviceUser= 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:
- UNIX owner, group, and mode
- systemd execution restrictions
- 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:
namei -l /usr/local/bin/golinuxcloud-workerSample 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-workerOn SELinux-enabled distributions, inspect the security context:
ls -ldZ /usr/local/bin/golinuxcloud-worker /var/lib/golinuxcloud /run/golinuxcloudSample 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/golinuxcloudOn Ubuntu or Debian with AppArmor, profile denials appear in the kernel log instead of SELinux AVC messages. A quick check:
sudo journalctl -k | grep -i apparmorThe User= / Group= unit syntax is the same; only the access-control layer differs.
Confirm the service user can write state data:
sudo -u golinuxcloud test -w /var/lib/golinuxcloud && echo "writable: yes"Sample output:
writable: yesIf access fails under enforcing SELinux on RHEL, Rocky Linux, AlmaLinux or Fedora, inspect denials instead of disabling SELinux:
ausearch -m AVC,USER_AVC -ts recentjournalctl -t setroubleshootMandatory 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:
NoNewPrivileges=yes
PrivateTmp=yes
ProtectSystem=strict
ProtectHome=yesStateDirectory= 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:
systemd-analyze security golinuxcloud-worker.serviceSample 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:
getent passwd golinuxcloudgetent group golinuxcloudRead the unit failure text:
systemctl status golinuxcloud-worker.servicejournalctl -u golinuxcloud-worker.service -bLikely 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:
ls -l /usr/local/bin/golinuxcloud-workerfile /usr/local/bin/golinuxcloud-workerSample output:
/usr/local/bin/golinuxcloud-worker: Bourne-Again shell script, ASCII text executablehead -n 1 /usr/local/bin/golinuxcloud-workerSample output:
#!/bin/bashnamei -l /usr/local/bin/golinuxcloud-workerCommon 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:
sudo -u golinuxcloud test -w /var/lib/golinuxcloudls -ld /var/lib/golinuxcloudOn SELinux-enabled hosts, also check the security context:
ls -ldZ /var/lib/golinuxcloudMaking 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
- systemd.service — unit file structure
- systemd.exec —
User=,Group=,DynamicUser=, and sandboxing directives - systemd.unit — dependency and install section behavior
- Debian systemd documentation — unit locations and administrator overrides on Debian-family systems
- Red Hat Enterprise Linux — working with systemd unit files — custom units, drop-ins, and
daemon-reloadon Enterprise Linux

