Skip to main content

Starting Systemd Services Without Root

Google and StackExchange do not give a straight forward and properly explained answer on how to do this, so I used ChatGPT to figure out how and have summarized my findings for you.

Assumptions

  • You need this so that a program running without user interaction can call systemctl without sudo
  • The program on the server that needs to call the service is run under the same $USER that you ssh into the server as
  • The service is called my_service and the path to the service file is /etc/systemd/system/my_service.service

Sudoers

To allow a non-root user, say maste, to run the service without root, we need to edit the sudoers. What is sudoers? /etc/sudoers is a rule list with permissions for regular users to be able to run commands as another user (like the root user). There also exists a directory /etc/sudoers.d where each file is treated like a rule list. We will need to create a new file in this directory with the the following rule (replace {{ your_user }}).

{{ your_user }} ALL=(ALL) NOPASSWD: /usr/bin/systemctl start my_service, /usr/bin/systemctl stop my_service, /usr/bin/systemctl restart my_service, /usr/bin/systemctl reload my_service, /usr/bin/systemctl status my_service

After this, we will still need to use sudo, however, a password will not need to be entered.

It is a bit too much work having to add this rule manually (using visudo) on every additional server or every time we need to allow a new service to be run. So here is a bash function (python incoming in the future) to do so with safety to avoid polluting the file with duplicates.

The following two script are from my devops utilities repository which I will slowly add utility scripts to.

Modifying sudoers via Bash

Bash Function
#!/bin/bash

allow_services_without_root() {
    # usage `allow_services_without_root monerod monero-wallet-rpc-prod monero-wallet-rpc-dev lenerva.com dev.lenerva.com`
    user=$(logname)
    for service in "$@"; do
        # allow user to start/stop/restart/reload the service
        sudoer_rule="$user ALL=(ALL) NOPASSWD: /usr/bin/systemctl start $service, /usr/bin/systemctl stop $service, /usr/bin/systemctl restart $service, /usr/bin/systemctl reload $service"

        # Check if the rule already exists in the sudoers file
        if ! grep -q "$sudoer_rule" /etc/sudoers.d/$user; then
            # Append the rule to the sudoers file
            echo "$sudoer_rule" | sudo tee -a /etc/sudoers.d/$user > /dev/null
            echo "SUCCESS: sudoers file modified to allow $user to start/stop/restart/reload $service"
        else
            echo "INFO: rule for $service already exists in the sudoers file"
        fi
    done
}

Modifying sudoers via Python

Python Function
#!/usr/bin/python3

import platform
import os

def systemd_services_without_root(*services):
    if platform.system() == 'Windows':
        print('ERROR: allow_services_without_root is not currently supported on Windows')
        return 1
    user = os.getlogin()
    new_rules = {}
    for service in services:
        commands = ', '.join((f'/usr/bin/systemctl {unit_cmd} {service}' for unit_cmd in ('start', 'stop', 'restart', 'reload')))
        new_rules[service] = f'{user} ALL=(ALL) NOPASSWD: {commands}\n'
    sudoers_file = f'/etc/sudoers.d/{user}'
    with open(sudoers_file, 'a+', encoding='utf-8') as f:
        existing_rules = set(f.readlines())
        rules_to_add = {}
        for service, new_rule in new_rules.items():
            if new_rule in existing_rules:
                print(f'INFO: rule for {service} already exists in /etc/sudoers')
            else:
                rules_to_add[service] = new_rule
        for service, rule in rules_to_add.items():
            f.write(rule)
            print(f'SUCCESS: {sudoers_file} modified to allow {user} to start/stop/restart/reload {service}')
    return 0

Application

  • Restarting gunicorn workers

Python Example

import subprocess
subprocess.Popen(["systemctl", "restart", "my_service"]).wait()