Setting Up a UPS with NUT on openSUSE Tumbleweed — Including SELinux
Plugging a UPS into your computer via USB and making sure it shuts down in time during a power outage: sounds like a quarter-hour job. 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 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 has no percentage threshold
The most common mistake 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 in /etc/ups/, not in /etc/nut/ as with Debian-based distributions. Almost all online guides mention 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
Here, 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 crucial lines:
ignorelbinstructs the driver to ignore the UPS’s hardwareLBflag and make its own decision instead.override.battery.charge.low = 25setsLBas soon as the charge level drops below 25%.override.battery.runtime.low = 120also setsLBas 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 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 check 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 what you want 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—you can choose any password 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 quotes; 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 notifications, it makes sense to add a NOTIFYCMD that sends an email.
Step 6: File Permissions
Three files contain access data. On openSUSE, the relevant group is upsd—not 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 lie that take up the most time. Both result in a Permission denied error, even though the 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, its primary group is upsd, not daemon. Result: A process in the upsd group encounters 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 7a, the driver successfully opens the UPS 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 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 appear 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 revert 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 enable the services:
sudo systemctl enable --now nut-driver@usv nut-server nut-monitor
The Third Stumbling Block: nut.target
And now for the step that almost every guide omits—you’ll only notice it during the first reboot:
sudo systemctl enable --now nut.target
The reason: nut-server.service and nut-monitor.service both contain
WantedBy=nut.target
So they depend exclusively on nut.target and not on multi-user.target. However, the package preset does not activate this target. Therefore, systemctl enable nut-server nut-monitor only creates symlinks under a target that is never pulled during boot.
The tricky part is this: With --now, the services start immediately; everything runs, and the setup appears to be complete. Only after the next reboot are all three units inactive (dead) — not failed. There is no error in the log because no startup attempt was made at all. A client such as the KDE utility “UPS Monitor” then only reports Connection failure: Connection refused.
Verification:
systemctl is-enabled nut.target
systemctl list-dependencies nut.target
In the dependency list, the driver, server, and monitor must appear with a dot. The fact that local-fs.target also appears there is intentional—this ensures that /var/lib/ups is available before the driver starts.
Notes on Working with the Units
Do not start the driver with upsdrvctl—on systems with systemd, this leads to conflicts with the unit instances. If manual startup is ever necessary, use upsdrvsvcctl.
Use the canonical names for status queries: nut-driver@…, nut-server, nut-monitor. Although the aliases upsd.service and upsmon.service exist and work for start/stop, systemctl status sometimes returns misleading output when using them.
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 points are driver.flag.ignorelb: enabled and the actual thresholds that have been applied. ups.status: OL means it is running on AC power.
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 only reads upsd.users 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 will later trigger the shutdown, while the actual monitoring runs as an unprivileged user under upsd.
Step 10: Functionality 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 the shutdown, temporarily set override.battery.charge.low to 95, run the test, 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 is left. After a few days of operation, it’s worth checking battery.runtime under realistic load: If the remaining runtime at the selected charge threshold is less than about two minutes, 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 is beyond this range, the reported remaining runtime should be taken with a grain of salt—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 is 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 |
| After reboot, all services are inactive (dead), no error in the journal | nut.target is not enabled |
systemctl enable --now nut.target |
upsd.conf: invalid directive [...] |
User definitions are in upsd.conf instead of upsd.users |
Move the blocks |