-
Notifications
You must be signed in to change notification settings - Fork 0
Hardening
What has actually been done to reduce what this application can be made to do, what was found when it was reviewed, and what has deliberately not been done.
The short version is in SECURITY.md.
This page is the reasoning and the measurements.
Nexus Manager is not a network service. It has no listener on any port, no update checker and no telemetry. What makes it worth reviewing at all is a narrower set of capabilities:
| Capability | Why it exists | What it means |
|---|---|---|
Writes to a hidraw device |
It draws on the panel | Access is an ACL for the logged-in user, not root |
Creates a virtual keyboard on /dev/uinput
|
A touch button presses a real key; Wayland has no XTEST | While running, it can type into whatever has focus |
Runs a configured command through /bin/sh
|
A touch button launches things | Whoever can write your config runs commands as you |
Spawns parec, pactl, wpctl, gdbus
|
Audio capture and media control | Fixed argument arrays, never a shell |
| Binds a Unix socket | A second launch raises the first one's window | Accepts exactly one word, show
|
Everything after the first two rows is ordinary desktop behaviour. The first two are the ones worth understanding before you install it.
There is no setuid binary, no capability, no polkit action and no system service. The bundled systemd unit is a user unit. After installation nothing belonging to this project runs as root.
Device access comes from a udev rule that tags the panel with uaccess, which
grants an ACL to the active local seat's user and revokes it at logout — rather
than MODE="0666", which would hand the device to every process on the machine
permanently. See Device Access on Linux.
systemd-analyze security rates the shipped unit 1.3 OK. It rated 7.0
MEDIUM before this pass. Check it yourself:
systemd-analyze security --user nexus-manager.serviceWhat was added: SystemCallFilter=@system-service, SystemCallArchitectures=native,
PrivateNetwork=yes and IPAddressDeny=any, RestrictAddressFamilies narrowed
to what the app actually uses, DevicePolicy=closed with only the panel and the
virtual keyboard allowed through, an empty capability bounding set,
LockPersonality, RestrictSUIDSGID, ProtectHostname, ProtectClock,
ProtectProc=invisible and UMask=0077.
A hardening score measures the policy. It says nothing about whether the program still runs under it. Each of these improved the score and had to be reverted because the service would not start:
| Directive | What happens |
|---|---|
MemoryDenyWriteExecute=true |
The .NET runtime JITs and needs pages that are writable and then executable. The service does not start. |
SystemCallFilter=~@resources |
Failed to create CoreCLR, HRESULT: 0x8007054F. The runtime needs syscalls in that set. |
RestrictAddressFamilies=AF_UNIX alone |
Device enumeration goes through udev, which needs AF_NETLINK. Without it the panel is simply never found. |
They are written down here and in the unit file so that the next person to look at the score does not re-add them.
How to test a directive rather than guess:
systemd-run --user -p ProtectHome=read-only --wait --pipe nexus-manager daemonOne property per run names the culprit in minutes. Two things to watch for:
systemctl is-active reports activating during a restart loop, so it is not
proof of anything; and a test unit with Restart=always will grab the panel
lock between probes and quietly invalidate every result after it.
Five faults, all in 0.0.3, all fixed in 0.0.4. They are listed because the interesting part is the pattern rather than the individual bugs.
-
The bundled systemd unit could never start the daemon.
ProtectHome=covers/run/useras well as/homeand/root, so the directory holding the single-instance lock was read-only. The daemon then reported "the panel is already being driven by another instance" — naming a process that did not exist. Fixed withReadWritePaths=%t. - A corrupt configuration crashed the application with a core dump. One interrupted autosave left it unstartable. It now reports the parse error, moves the unreadable file aside rather than letting the next save overwrite it, and starts from a discovered configuration.
-
The
/tmpfallback directory was created with default permissions. That path is predictable and/tmpis world-writable, so another local user could create it first and own the directory the lock, owner record and socket are made in — andFile.WriteAllTextfollows symlinks. Now created 0700, with the mode checked afterwards, becauseCreateDirectorydoes not alter a directory that already exists. - Lock failures were reported as contention. Three unrelated causes — a permissions fault, a sandbox denial and genuine contention — all produced the same message. A failure path is a diagnostic; giving one sentinel three meanings makes the program confidently wrong.
- A launched program could deadlock or pin a thread. The launcher read one pipe to the end before the other, so a child filling stderr hung both, and since the read only returns when the child exits, launching anything long-lived held a worker for its lifetime.
⛔ Every one of those passed every other check. The package inventory, the
dependency sweep, the launch tests and the hardening score were all green while
the service could not boot. That is why packaging/test-deb.sh now starts the
packaged unit and requires it to reach its ready state, verified against the
broken unit as a negative control.
- Release packages bundle the .NET runtime, so they depend only on system libraries.
- Dependency lists in the
.deband the PKGBUILD are derived from a sweep of the published binaries —objdump -pforNEEDEDentries plus a strings sweep for anything loaded bydlopen— rather than written from memory. -
dotnet list package --vulnerable --include-transitivereports nothing across all seven projects, and nothing deprecated. -
packaging/test-deb.shasserts the package contents against the tree it was built from and then launches the binaries out of the extracted package withDOTNET_ROOTscrubbed, so a bundle that silently fell back to a developer toolchain would be caught.
Stated plainly rather than left for someone to discover:
- Releases are not signed. No GPG signature on tags or artifacts, and no build provenance attestation. The AUR package pins a SHA-256 of the release tarball, which protects against a corrupted download but not against a compromised release.
- There is no CI. Everything is built and tested on one machine. No automated dependency scanning, no static analysis on push.
- Builds are not reproducible. A .NET publish embeds timestamps, so two builds of the same commit differ. Nobody can independently confirm that a published binary came from the published source.
-
No portal or Flatpak confinement. The application needs raw
hidrawanduinputaccess, which sandboxed packaging formats do not grant well. This is a real limitation rather than an oversight. -
A
Launchaction is not confirmed before it runs. The configuration is trusted the way a shell profile is trusted. If configuration ever becomes importable —.cuescreensimport is not implemented — that assumption has to be revisited before it ships, because a file from someone else could then carry an action.