Hero Image

upsmon unter OpenSUSE Tumbleweed mit altivem SELinux

Setting Up a UPS with NUT on openSUSE Tumbleweed — Including SELinux

Plugging a UPS into the computer via USB and making sure it shuts down in time in the event of a power outage: sounds like a quarter of an hour’s work. On openSUSE Tumbleweed with SELinux enabled, it took me more like three hours because certain details aren’t documented anywhere and the guide provided by openSUSE is getting a bit outdated.

I’d like to spare you this ordeal. I’m therefore sharing the following guide with you and hope that it works for you as well.

This guide describes the complete process for standalone operation: one UPS, one computer, no network access from other systems. Tested with NUT 2.8.5 and an APC Back-UPS RS 900G, but applies unchanged to any other UPS with usbhid-ups.

First things first: upsmon does not recognize a percentage threshold

The most common misconception when getting started. upsmon responds exclusively to two flags provided by the UPS or the driver: OB (mains power failed) and LB (battery nearly empty). There is no setting in upsmon.conf that says “shut down at 25%.”

The threshold is therefore configured where the LB flag is generated—in the driver, i.e., in ups.conf.

Step 1: Installation

sudo zypper install nut

On openSUSE, the configuration is located under /etc/ups/, not under /etc/nut/ as with Debian-based distributions. Almost all online guides list the Debian path—that’s the first pitfall.

Next, check whether the UPS is recognized at all:

lsusb
sudo nut-scanner -U

nut-scanner suggests the appropriate driver. For USB UPSs, this is almost always usbhid-ups.

Step 2: /etc/ups/ups.conf

This is where you configure the driver and set the shutdown threshold:

maxretry = 3

[usv]
    driver = usbhid-ups
    port = auto
    desc = “APC Back-UPS RS 900G”
    offdelay = 30

ondelay = 30
    ignorelb
    override.battery.charge.low = 25
    override.battery.runtime.low = 120

The name in square brackets can be chosen freely, but will be reused in several places later—keep it short and free of special characters.

The three key lines:

  • ignorelb instructs the driver to ignore the UPS’s hardware LB flag and make its own decision instead.
  • override.battery.charge.low = 25 sets LB as soon as the charge level drops below 25%.
  • override.battery.runtime.low = 120 also sets LB as soon as less than 120 seconds of remaining runtime are reported.

Both conditions are linked by an OR operator. The runtime threshold is an important safety net: Under heavy load, 25% remaining charge can mean significantly less time than when the system is idle.

A warning regarding ignorelb: If the UPS reports neither battery.charge nor battery.runtime, LB will never be triggered and the system will not shut down at all. After Step 8, be sure to verify that both values are present.

Step 3: /etc/ups/nut.conf

MODE=standalone

standalone means: upsd is running but only listens on localhost. This is exactly right if no other system is supposed to access this UPS.

Step 4: /etc/ups/upsd.users

NUT has its own internal user management. These users have nothing to do with Linux system users—the password can be chosen freely here:

[upsmon]
    password = ASecurePassword
    upsmon primary

Two pitfalls:

  • The separator is an equal sign, not a colon.
  • If the password contains a #, the parser will truncate everything after that point. Use a purely alphanumeric password for the first run.

primary is also correct in standalone mode. It simply means that this system has the UPS connected directly to the port.

Step 5: /etc/ups/upsmon.conf

MONITOR usv@localhost 1 upsmon ASecurePassword primary
MINSUPPLIES 1
SHUTDOWNCMD “/usr/sbin/shutdown -h +0”
POWERDOWNFLAG /etc/killpower
NOTIFYFLAG ONBATT SYSLOG+EXEC
NOTIFYFLAG LOWBATT SYSLOG+EXEC

The MONITOR line has six fields, and the order is mandatory:

MONITOR <name>@<host> <powervalue> <username> <password> primary

If a field is missing, NUT 2.8 interprets the line as an obsolete format and refuses to start with the message Unable to use old-style MONITOR line without a username. The username and password must match exactly what is listed in upsd.users.

SHUTDOWNCMD must be enclosed in quotation marks; otherwise, the parser will split the command at the spaces.

The default configuration uses WALL for notifications. On a desktop with a Wayland session, this won’t reach anyone and will only generate errors in the journal—hence the SYSLOG+EXEC above. If you have set up local email delivery, it makes sense to add a NOTIFYCMD that sends an email.

Step 6: File Permissions

Three files contain access data. On openSUSE, the responsible group is upsdnot nut, as stated in many guides:

sudo chown root:upsd /etc/ups/upsd.conf /etc/ups/upsd.users /etc/ups/upsmon.conf
sudo chmod 640 /etc/ups/upsd.conf /etc/ups/upsd.users /etc/ups/upsmon.conf

Step 7: The Two openSUSE-Specific Pitfalls

This is where the problems that take up most of the time are hidden. Both result in a Permission denied error, even though all permissions appear correct at first glance.

7a: Group Membership for USB Access

The NUT package includes udev rules that assign USB devices to the daemon group:

ATTR{idVendor}==“051d”, ATTR{idProduct}==“0002”, MODE="664", GROUP="daemon"

When it starts, the driver drops root privileges and switches to the user upsd. However, the user’s primary group is upsd, not daemon. Result: A process in the upsd group attempts to access a device in the daemon group—no access.

The error message reads:

libusb1: Could not open any HID devices: insufficient permissions on everything
No matching HID UPS found

This is misleading because it sounds like a missing driver or a missing udev rule. The solution:

sudo usermod -aG daemon upsd
id upsd

The output must contain 453(upsd),2(daemon).

Important: Do not write your own udev rule. The included rule is correct and covers all common devices. It applies to every USB port and after every reboot because it matches the vendor and product IDs, not a device path.

7b: SELinux Context for the State Directory

After step 7a, the driver successfully opens the USV but fails at the next step:

Fatal error: unable to create listener socket
bind /var/lib/ups/usbhid-ups-usv failed: Permission denied

The file permissions on /var/lib/ups are correct (upsd:daemon, 770), the process is running as the owner—and yet the system still denies access. The reason is in the audit log:

sudo ausearch -m avc -ts recent
avc: denied { write } for comm="usbhid-ups"
scontext=system_u:system_r:nut_upsdrvctl_t:s0
tcontext=system_u:object_r:var_lib_t:s0 tclass=dir

The SELinux policy recognizes only the path /run/nut with the type nut_var_run_t for NUT runtime data. However, openSUSE builds NUT with /var/lib/ups as the state directory. This path does not appear in the policy, so it is assigned the generic type var_lib_t—and write access is blocked.

The clean solution is a file context rule:

sudo semanage fcontext -a -t nut_var_run_t ‘/var/lib/ups(/.*)?’
sudo restorecon -Rv /var/lib/ups
ls -Zd /var/lib/ups

Afterward, nut_var_run_t should be listed there. If semanage is missing:

sudo zypper install policycoreutils-python-utils

Deliberately not recommended: a policy module via audit2allow. This would grant the driver write access to all var_lib_t directories—significantly more extensive than necessary if the actual problem is merely a mislabeled directory. Likewise, setenforce 0 is not a solution; it’s fine as a quick diagnostic step, but you should immediately reset it to setenforce 1 afterward.

The semanage rule is stored in the local policy database and survives package updates and reboots. It must be set again after a fresh installation—a good candidate for your own Ansible role:

- name: SELinux context for NUT state directory
  community.general.sefcontext:
    target: ‘/var/lib/ups(/.*)?’
    setype: nut_var_run_t
    state: present
  notify: restorecon ups

Step 8: Start services

NUT 2.8 uses the nut-driver-enumerator to generate a separate systemd unit for each section in ups.conf. After making changes to ups.conf, the enumerator must be rerun; otherwise, the unit will not exist or will be out of date:

sudo systemctl restart nut-driver-enumerator.service
systemctl list-units ‘nut-driver@*’

Then, in this order:

sudo systemctl enable --now nut-driver@usv nut-server nut-monitor

Do not start the driver with upsdrvctl—on systems with systemd, this causes conflicts with the unit instances. If manual startup is ever necessary, use upsdrvsvcctl.

Step 9: Verification

upsc usv

The output should include, among other things:

battery.charge: 100
battery.charge.low: 25
battery.runtime: 905
battery.runtime.low: 120
driver.flag.ignorelb: enabled
ups.status: OL

The key settings are driver.flag.ignorelb: enabled and the actual threshold values applied. ups.status: OL indicates mains operation.

And the monitor:

systemctl status nut-monitor

An ERR ACCESS-DENIED error should no longer appear here. If it does appear, the username or password in upsd.users does not match the one in the MONITOR line. upsd reads upsd.users only at startup—so after making a correction, restart nut-server, not just nut-monitor.

The fact that two upsmon processes appear in the process list is intentional: The root process triggers the shutdown later; the actual monitoring runs as an unprivileged upsd process.

Step 10: Functional Test

Unplug the UPS from the power outlet and observe:

watch -n 2 ‘upsc usv ups.status battery.charge’

The status should change to OB within a few seconds. However, a full test through to shutdown takes a long time, depending on the battery capacity. If you want to verify the entire process, including shutdown, temporarily set override.battery.charge.low to 95, test it, and then reset it to the desired value.

Choosing the Right Threshold

25% is a useful starting point, but not a universal recommendation. The key factor is how much remaining runtime it represents. After a few days of operation, it’s worth checking battery.runtime under realistic load conditions: If less than about two minutes remain at the selected charge threshold, the threshold should be set higher.

On a computer with a powerful graphics card, the difference between idle and full load can be a factor of three or more. This is precisely why the additional runtime.low threshold from Step 2 isn’t just a gimmick—it comes into play in exactly this scenario.

It’s worth taking one last look at battery.mfr.date in the upsc output. Lead-acid batteries in UPSs typically last three to five years. If the date listed there falls outside this range, the reported remaining runtime should be treated with caution—regardless of what the software displays.

Summary of Pitfalls

Symptom Cause Solution
Configuration files not found openSUSE uses /etc/ups/ instead of /etc/nut/ Adjust the path
chown: invalid group ‘nut’ The group is named upsd on openSUSE chown root:upsd
Unable to use old-style MONITOR line The MONITOR line has fewer than six fields Add the username
insufficient permissions on everything upsd is not in the daemon group usermod -aG daemon upsd
bind ... failed: Permission denied SELinux context var_lib_t instead of nut_var_run_t semanage fcontext + restorecon
ERR ACCESS-DENIED Password is incorrect or upsd has not been restarted Check upsd.users, restart nut-server
wall: Command not found WALL in NOTIFYFLAGS Switch to SYSLOG+EXEC