Remote systemd Service Control¶
Overview¶
When you operate a fleet of Linux servers, logging into each host to run systemctl restart nginx does not scale. systemd supports remote management over SSH via systemctl --host, and PolicyKit (polkit) lets you grant narrowly scoped privileges — for example, allowing a systemd-manager user to restart specific services without full root access.
This tutorial goes beyond surface-level examples: you will create a dedicated management user, configure SSH keys, write polkit rules that authorize only defined actions, and operate services remotely with the same commands you use locally.
This is Module 5, Tutorial 12 in the REBASH Academy Linux series.
Prerequisites¶
- SSH and Remote Administration — key-based auth and
~/.ssh/config - systemd Service Management —
systemctl, units, and journal basics - Two Linux VMs (or one machine simulating remote via
localhostand a second user) with systemd 230+ sudoon the remote server to create users and polkit rules
Learning Objectives¶
By the end of this tutorial, you will be able to:
- Run
systemctlandjournalctlagainst remote hosts with--host user@server - Create a least-privilege
systemd-managerservice account - Write polkit rules granting specific systemd actions to non-root users
- Diagnose "Access denied" and SSH transport failures in remote systemctl
- Compare remote systemctl with configuration management and SSH ad-hoc patterns
- Apply security boundaries so operators cannot reload arbitrary units
Architecture Diagram¶
flowchart TD
subgraph Admin["Workstation"]
A[systemctl --host]
B[journalctl --host]
end
subgraph SSH["Transport"]
C[Encrypted SSH Session]
end
subgraph Remote["Server"]
D[systemd-manager user]
E["polkit / pkaction"]
F[systemd PID 1]
G[Unit Files]
H[Running Services]
end
A --> C
B --> C
C --> D
D --> E
E -->|authorized| F
E -->|denied| I[Access Denied]
F --> G
F --> H```
## Theory
### How `systemctl --host` works
When you run:
```bash
systemctl --host deploy@web01 status nginx the local systemctl binary opens an SSH connection to web01 as user deploy, then executes the equivalent command in a pseudo-terminal or D-Bus proxy context on the remote side. The remote user must have permission to talk to systemd — typically via polkit rules or membership in a privileged group.
Requirements:
- SSH key auth working for
user@host - Remote machine running systemd as PID 1
- Compatible systemd versions (major mismatch can cause protocol errors)
- Polkit authorization for non-root users performing privileged actions
The same --host flag works with journalctl:
Why not give everyone sudo?¶
sudo systemctl restart * is effectively root. Production teams want separation of duties:
| Role | Allowed actions |
|---|---|
| Developer | status, is-active, read logs |
| On-call SRE | restart specific app units |
| Platform admin | full systemctl, daemon-reload |
Polkit expresses these as fine-grained rules evaluated at action time, logged in /var/log/auth.log or journal.
The systemd-manager user pattern¶
Create a dedicated account:
- No interactive login shell required (use
/bin/bashor/sbin/nologindepending on policy) - SSH key in
~/.ssh/authorized_keys— no password - Member of groups only if needed; authorization primarily via polkit
- Home directory for SSH metadata only
This account is not a human user — it is a service identity for automation and jump-box workflows.
Polkit fundamentals¶
PolicyKit mediates privileged actions for unprivileged processes. systemd registers actions like:
org.freedesktop.systemd1.manage-units— start, stop, restart unitsorg.freedesktop.systemd1.reload-or-restart-unitorg.freedesktop.systemd1.manage-unit-files— enable, disable (more sensitive)
Rules live in:
/etc/polkit-1/rules.d/— JavaScript-like.rulesfiles (evaluated top-down, first match wins for allow/deny)/usr/share/polkit-1/actions/— action definitions with defaults
Example rule structure:
polkit.addRule(function(action, subject) {
if (action.id == "org.freedesktop.systemd1.manage-units" &&
subject.user == "systemd-manager" &&
action.lookup("unit") == "nginx.service") {
return polkit.Result.YES;
}
});
After adding rules: no daemon reload needed for polkit — rules are read on each action. Verify with pkaction and test remote commands.
Remote vs local systemctl differences¶
| Aspect | Local | Remote --host |
|---|---|---|
| Transport | D-Bus socket | SSH |
| Auth | Session polkit | Remote user's polkit/sudo |
| Latency | Instant | Network + SSH overhead |
| Tab completion | Full | Limited |
| File paths in units | Local paths | Remote paths |
Unit file edits still happen on the server — remote systemctl does not sync files. Use Ansible, cloud-init, or CI/CD for unit file deployment; use remote systemctl for runtime control.
Security considerations¶
- Restrict
systemd-managerSSH access by source IP insshd_config(Match Address) - Never share the management private key across people — use individual keys mapped to the same polkit-scoped user, or separate users per team
- Audit polkit and SSH logs centrally
- Deny
daemon-reloadandenableunless platform team requires it — those change boot behavior
Hands-on Lab¶
These steps assume a remote server (replace REMOTE with hostname/IP). For single-machine practice, use REMOTE=localhost and a second user on the same box.
Step 1 – Create the systemd-manager user (on remote server)¶
Run on the remote server:
sudo useradd --create-home --comment "Remote systemd operator" "$MGMT_USER"
sudo mkdir -p /home/$MGMT_USER/.ssh
sudo chmod 700 /home/$MGMT_USER/.ssh
sudo chown -R $MGMT_USER:$MGMT_USER /home/$MGMT_USER/.ssh
id "$MGMT_USER"
Expected output: user uid/gid lines; home /home/systemd-manager.
Step 2 – Deploy SSH public key¶
From your workstation (key from SSH tutorial):
ssh-keygen -t ed25519 -f ~/.ssh/systemd_mgr_ed25519 -N "" -C "systemd-manager-key"
sudo bash -c "cat >> /home/$MGMT_USER/.ssh/authorized_keys" < ~/.ssh/systemd_mgr_ed25519.pub
sudo chmod 600 /home/$MGMT_USER/.ssh/authorized_keys
sudo chown $MGMT_USER:$MGMT_USER /home/$MGMT_USER/.ssh/authorized_keys
Test SSH:
Expected output: systemd-manager and the server hostname.
Step 3 – Baseline remote status (expect permission errors)¶
Without polkit rules, privileged actions fail for non-root:
systemctl --host ${MGMT_USER}@${REMOTE} status ssh 2>/dev/null \
|| systemctl --host ${MGMT_USER}@${REMOTE} status sshd
Expected output: service status may work (read-only) on some distros, or polkit authentication prompt / access denied for restricted views.
Check failed units:
Expected output: list of failed units or 0 loaded units listed.
Step 4 – Install polkit rule for nginx management¶
On the remote server, allow systemd-manager to manage only nginx.service (install nginx if needed for the lab):
Create polkit rule:
sudo tee /etc/polkit-1/rules.d/50-systemd-manager-nginx.rules << 'EOF'
polkit.addRule(function(action, subject) {
if (subject.user == "systemd-manager") {
if (action.id == "org.freedesktop.systemd1.manage-units") {
var unit = action.lookup("unit");
if (unit == "nginx.service") {
return polkit.Result.YES;
}
}
if (action.id == "org.freedesktop.systemd1.manage-units" &&
action.lookup("verb") == "reload-or-restart") {
if (action.lookup("unit") == "nginx.service") {
return polkit.Result.YES;
}
}
}
});
EOF
List matching polkit actions:
Expected output: action IDs like org.freedesktop.systemd1.manage-units.
Step 5 – Remote restart nginx¶
systemctl --host ${MGMT_USER}@${REMOTE} restart nginx
systemctl --host ${MGMT_USER}@${REMOTE} is-active nginx
Expected output: active
If nginx is not installed, substitute a running unit you scoped in polkit (e.g., a custom rebash-demo.service).
Step 6 – Verify authorization boundary¶
Attempt to restart sshd (not authorized):
Expected output: Access denied, Interactive authentication required, or polkit rejection — not a successful restart.
Step 7 – Remote journal access¶
Expected output: last five nginx log lines from the remote journal.
Follow logs remotely (Ctrl+C to stop):
Expected output: streaming log lines or timeout exit after 3 seconds.
Step 8 – SSH config shortcut and fleet check script¶
Add to ~/.ssh/config:
Fleet health script:
cat > ~/lab/check_fleet.sh << 'EOF'
#!/bin/bash
set -euo pipefail
HOSTS=(web-fleet) # add more Host aliases
for h in "${HOSTS[@]}"; do
printf '%-20s ' "$h"
if systemctl --host "$h" is-system-running --wait 2>/dev/null; then
failed=$(systemctl --host "$h" --failed --no-legend 2>/dev/null | wc -l)
echo "running (failed units: $failed)"
else
echo "UNREACHABLE or degraded"
fi
done
EOF
chmod +x ~/lab/check_fleet.sh
Run against configured host:
Expected output: host name with running and failed unit count.
Commands¶
| Command | Description |
|---|---|
systemctl --host USER@HOST status UNIT | Remote service status |
systemctl --host USER@HOST start UNIT | Remote start |
systemctl --host USER@HOST stop UNIT | Remote stop |
systemctl --host USER@HOST restart UNIT | Remote restart |
systemctl --host USER@HOST reload UNIT | Remote reload (if unit supports it) |
systemctl --host USER@HOST is-active UNIT | Check if unit is active |
systemctl --host USER@HOST is-enabled UNIT | Check if unit is enabled at boot |
systemctl --host USER@HOST --failed | List failed units on remote |
systemctl --host USER@HOST list-units --type=service | List remote services |
journalctl --host USER@HOST -u UNIT | Remote unit logs |
journalctl --host USER@HOST -p err -n 20 | Recent remote errors |
pkaction \| grep systemd | List polkit actions for systemd |
pkcheck --action-id ... --process $$ | Test polkit authorization |
systemctl --version | Verify local/remote systemd compatibility |
Code¶
Polkit rule — read-only status for developers¶
// /etc/polkit-1/rules.d/40-dev-readonly.rules
polkit.addRule(function(action, subject) {
if (subject.user == "dev-readonly" &&
action.id == "org.freedesktop.systemd1.properties") {
return polkit.Result.YES;
}
});
Polkit rule — multiple allowed units¶
polkit.addRule(function(action, subject) {
if (subject.user != "systemd-manager") return;
var allowed = ["nginx.service", "app-backend.service", "worker.service"];
if (action.id != "org.freedesktop.systemd1.manage-units") return;
var unit = action.lookup("unit");
if (allowed.indexOf(unit) >= 0) {
return polkit.Result.YES;
}
});
Ansible-style loop without Ansible¶
#!/bin/bash
set -euo pipefail
SERVICES=(nginx app-worker)
HOST=systemd-manager@prod-web01
for svc in "${SERVICES[@]}"; do
echo "=== $svc ==="
systemctl --host "$HOST" is-active "$svc"
done
Common Mistakes¶
Granting org.freedesktop.systemd1.manage-units without unit filter
A rule that returns YES for all units is equivalent to passwordless root for service control. Always filter on action.lookup("unit").
Forgetting SSH must work before systemctl --host works
Test ssh user@host true first. systemctl errors about connection are SSH problems, not systemd problems.
Mixing up user contexts
systemctl --host root@host vs systemctl --host systemd-manager@host hit different polkit policies. Use the least-privileged account that can perform the task.
Editing unit files remotely via shell heredocs
Remote systemctl controls runtime state, not configuration management. Deploy unit files through reviewed pipelines, then daemon-reload deliberately.
Best Practices¶
One polkit rule file per role
Name files 50-team-app-restart.rules, 51-platform-full.rules for clarity and auditability.
Log and alert on denied polkit actions
Forward auth logs to SIEM; repeated denials may indicate compromise attempts or misconfigured automation.
Use SSH config Host aliases in scripts
Scripts reference prod-web-01, not raw IPs — easier rotation and bastion jumps.
Test rules with a non-production user first
Clone polkit rules to staging; verify both allowed and denied actions before promoting.
Troubleshooting¶
| Issue | Cause | Solution |
|---|---|---|
Failed to connect to bus | Remote user cannot access systemd D-Bus | Add polkit rule or use privileged user |
Access denied | Polkit rule missing or wrong unit name | Check exact unit name with .service suffix; review rules.d |
Host key verification failed | SSH known_hosts mismatch | Update known_hosts after server rebuild |
Connection timed out | Firewall, wrong IP, sshd down | Verify network path and SSH separately |
Unit not found | Typo or unit not installed on remote | systemctl --host ... list-unit-files \| grep name |
| Works locally, fails remotely | Different polkit defaults per user | Compare pkaction --verbose and auth logs |
systemctl: invalid option --host | Ancient systemd version | Upgrade systemd or use SSH wrapper script |
Summary¶
systemctl --host user@serverandjournalctl --hostmanage remote systemd over SSH — same CLI, different transport.- A dedicated systemd-manager user with key-based SSH separates human admin accounts from automation identities.
- Polkit rules in
/etc/polkit-1/rules.d/grant least-privilege: specify user, action ID, and unit name — never blanket YES for manage-units. - Unauthorized restart attempts should fail loudly; authorized restarts should succeed without sudo passwords.
- Remote control handles runtime; unit file deployment belongs in configuration management with reviewed changes.
Interview Questions¶
- How does
systemctl --hosttransport commands to a remote server? - Why create a dedicated
systemd-manageruser instead of sharing root SSH keys? - What is polkit, and how does it differ from sudo?
- Name two polkit action IDs related to systemd unit management.
- How would you allow a user to restart
nginx.servicebut notsshd.service? - What happens if your polkit rule omits the
.servicesuffix in the unit lookup? - How do you view remote logs for a unit without SSH-ing in manually?
- Why should
daemon-reloadandenablebe more restricted thanrestart? - What would you check first if
systemctl --host user@host status nginxhangs? - Compare remote systemctl with running
ssh user@host 'sudo systemctl restart nginx'.
Related Tutorials¶
- Linux – Category Overview
- SSH and Remote Administration (previous)
- Disk and Filesystem Management (next)
- systemd Service Management
- Learning Paths – DevOps Engineer