---
title: 'upsmon in OpenSUSE Tumbleweed with active SELinux'
url: 'https://www.mmorath.de/en/wiki/upsmon-unter-opensuse-tumbleweed-mit-altivem-selinux'
markdown: 'https://www.mmorath.de/en/wiki/upsmon-unter-opensuse-tumbleweed-mit-altivem-selinux.md'
lang: en
date: '2026-09-12'
description: '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 d…'
taxonomy:
  category:
    - Wiki
  tag:
    - upsmon
    - usv
    - ups
    - OpenSUSE
    - SELinux
---

# 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](https://en.opensuse.org/SDB:Network_UPS_Tools) 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

```bash
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:

```bash
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:

```ini
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:

- **`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 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`

```ini
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:

```ini
[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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```bash
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:

```yaml
- 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:

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

Then enable the services:

```bash
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:

```bash
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:

```bash
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

```bash
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:

```bash
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:

```bash
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 |

---

## Navigation

- Parent: [Wiki](https://www.mmorath.de/en/wiki.md)
