Files
moonix/modules/services.nix
T
nevaforget f4b8b5dd50 feat(desktop): nwg-displays writes niri outputs, kanshi dropped
The port swapped moonarch's wdisplays-persistent for the nixpkgs wdisplays,
which cannot save -- so the kanshi config stayed empty and its unit was
skipped on every boot. nwg-displays supports niri and writes the layout to
~/.config/niri/monitor.kdl, which niri applies itself.

niri prefers a home config and does not merge, so config.kdl is seeded with
an include of /etc/niri/config.kdl, like moonarch does for stasis.rune.
2026-09-08 16:47:21 +02:00

261 lines
9.9 KiB
Nix

# ABOUTME: systemd system and user services for the desktop session.
# ABOUTME: Rebuilt from moonarch/defaults/etc/systemd/, whose units use FHS paths.
{ config, pkgs, lib, moonarchPkgs, ... }:
let
# Shared by the two root oneshots below. Both read one state file and write a
# single sysfs attribute, so everything else is taken away. Scored with
# `systemd-analyze security`: 7.5 EXPOSED before, 0.9 SAFE after.
#
# Deliberately absent:
# ProtectKernelTunables - remounts /sys read-only, which is exactly the
# write both units exist for. ReadWritePaths is no fix, /sys/class/… is a
# symlink into /sys/devices/….
# PrivateUsers - capabilities inside a user namespace are namespace-local,
# while the kernel's sysfs store handlers test against the initial one.
#
# ProtectSystem=strict is safe here: it leaves /dev, /proc and /sys writable,
# and the state file is only ever read.
sysfsWriterHardening = {
CapabilityBoundingSet = [ "" ];
AmbientCapabilities = [ "" ];
NoNewPrivileges = true;
ProtectSystem = "strict";
ProtectHome = true;
PrivateTmp = true;
PrivateDevices = true;
PrivateNetwork = true;
IPAddressDeny = "any";
RestrictAddressFamilies = [ "AF_UNIX" ];
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
KeyringMode = "private";
UMask = "0077";
};
# Shared by the Wayland session clients below: socket to the compositor,
# nothing else. 9.4 UNSAFE before, 0.9 SAFE after, each one started under the
# full set with `systemd-run --user` first.
#
# Deliberately absent:
# ProtectHome - hides /home, /root *and /run/user*, where the Wayland
# socket lives. Read-only fails too: connect() needs write on the inode.
# PrivateUsers - maps the session uid to nobody, the socket is owned by the
# real one.
waylandClientHardening = {
CapabilityBoundingSet = [ "" ];
AmbientCapabilities = [ "" ];
NoNewPrivileges = true;
ProtectSystem = "strict";
ReadWritePaths = [ "%t" ];
PrivateTmp = true;
PrivateDevices = true;
PrivateNetwork = true;
IPAddressDeny = "any";
RestrictAddressFamilies = [ "AF_UNIX" ];
RemoveIPC = true;
ProtectClock = true;
ProtectHostname = true;
ProtectKernelLogs = true;
ProtectKernelModules = true;
ProtectKernelTunables = true;
ProtectControlGroups = true;
ProtectProc = "invisible";
ProcSubset = "pid";
RestrictNamespaces = true;
RestrictRealtime = true;
RestrictSUIDSGID = true;
LockPersonality = true;
MemoryDenyWriteExecute = true;
SystemCallArchitectures = "native";
SystemCallFilter = [ "@system-service" "~@privileged" "~@resources" ];
SystemCallErrorNumber = "EPERM";
KeyringMode = "private";
};
in
{
# --- Audio ---
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
jack.enable = true;
};
# --- Hardware ---
hardware.bluetooth.enable = true;
services.blueman.enable = true;
services.udisks2.enable = true;
services.upower.enable = true;
services.gvfs.enable = true;
services.fwupd.enable = true;
# power-profiles-daemon over auto-cpufreq: on amd-pstate in active mode the
# platform decides the frequency from the min/max and EPP registers alone, so
# the governor is the wrong knob. ppd writes EPP and the ACPI platform_profile;
# where neither exists it falls back to a driver that touches no hardware.
services.power-profiles-daemon.enable = true;
# --- Firewall, replaces the ufw calls in post-install.sh ---
networking.firewall.enable = true;
# --- System services from defaults/etc/systemd/system/ ---
#
# Rebuilt rather than copied, same reason as the user units: the originals
# reference /usr/bin.
systemd.services.moonarch-batsaver = {
description = "Restore battery conservation mode threshold";
after = [ "sysinit.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExists = [
"/sys/class/power_supply/BAT0/charge_control_end_threshold"
"/var/lib/moonarch/batsaver-threshold"
];
serviceConfig = {
Type = "oneshot";
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-batsaver-restore";
} // sysfsWriterHardening;
};
systemd.services.moonarch-camera = {
description = "Switch off all cameras on boot";
after = [ "sysinit.target" ];
wantedBy = [ "multi-user.target" ];
unitConfig.ConditionPathExistsGlob = "/sys/class/video4linux/video*";
serviceConfig = {
Type = "oneshot";
ExecStart = "${moonarchPkgs.moonarch-scripts}/bin/moonarch-camera-boot";
} // sysfsWriterHardening;
};
# --- Session services ---
#
# Rebuilt rather than copied: the upstream units reference /usr/bin/,
# which does not exist under Nix.
# The bar, launcher and every popout. A unit rather than a spawn-at-startup so
# a crash does not leave the session without a bar until the next login.
systemd.user.services.quickshell = {
description = "Quickshell bar and shell";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
# The widgets shell out to niri, nmcli, powerprofilesctl, the moonarch
# scripts and a dozen more, and which ones is a property of the QML, not of
# this file. As a spawn-at-startup it inherited the session PATH; the unit
# gets the same set rather than a list that goes stale on the next widget.
path = [ config.system.path ];
serviceConfig = {
Type = "simple";
ExecStart = "${lib.getExe pkgs.quickshell} -c moonarch";
Restart = "on-failure";
RestartSec = 3;
};
};
# nixpkgs ships the package but no module, and the package carries no unit of
# its own, so it is declared here. Mirrors /usr/lib/systemd/user/stasis.service.
systemd.user.services.stasis = {
description = "Stasis Wayland Idle Manager";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
# Every binary a configured action names has to be listed here. stasis does
# not run the actions through a login shell, so /etc/profile never applies
# and nothing resolves via /run/current-system/sw/bin. A missing binary is
# silent: the spawn fails, stasis logs the action as started, sees the
# immediate exit, treats the step as finished and stops there — which is how
# swaylock was absent for the whole idle chain while `niri msg` worked.
# pactl is why pulseaudio appears at all — it stays off systemPackages, see
# modules/desktop.nix. brightnessctl is for the laptop's brightness step.
path = with pkgs; [
bashNonInteractive
niri
pulseaudio
libnotify
swaylock
brightnessctl
];
# No waylandClientHardening: swaylock is a child of this unit and
# authenticates through the setuid-root unix_chkpwd. `systemd --user` builds
# mount namespaces through an unprivileged user namespace, whose mounts are
# nosuid — the wrapper loses setuid and the session is no longer unlockable.
# Measured per directive with `echo wrong | unix_chkpwd $USER nonull`: exit
# 7 means shadow was read, 9 means no access. Only these five return 7, so
# only they are set. 9.4 → 9.0; there is no more to get here.
serviceConfig = {
Type = "simple";
ExecStart = "${lib.getExe pkgs.stasis}";
ExecReload = "${lib.getExe pkgs.stasis} reload";
Restart = "on-failure";
RestartSec = 2;
IPAddressDeny = "any";
ProtectProc = "invisible";
ProcSubset = "pid";
RemoveIPC = true;
KeyringMode = "private";
};
};
systemd.user.services.cliphist-text = {
description = "Clipboard history manager (text)";
partOf = [ "graphical-session.target" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStartPre = "${pkgs.runtimeShell} -c 'mkdir -p $XDG_RUNTIME_DIR/cliphist && ${pkgs.cliphist}/bin/cliphist wipe'";
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
Restart = "on-failure";
RestartSec = 3;
} // waylandClientHardening;
};
systemd.user.services.cliphist-image = {
description = "Clipboard history manager (image)";
partOf = [ "graphical-session.target" ];
after = [ "cliphist-text.service" ];
wantedBy = [ "graphical-session.target" ];
serviceConfig = {
Type = "simple";
ExecStart = "${pkgs.wl-clipboard}/bin/wl-paste --type image --watch ${pkgs.cliphist}/bin/cliphist -db-path %t/cliphist/db store";
Restart = "on-failure";
RestartSec = 3;
} // waylandClientHardening;
};
# Night light stays disabled by default: it is a user toggle. Enabling it
# here would create a system-scope symlink that overrides any
# `systemctl --user disable`.
systemd.user.services.wlsunset = {
description = "Wlsunset night light (blue light filter)";
partOf = [ "graphical-session.target" ];
after = [ "graphical-session.target" ];
wantedBy = [ ];
serviceConfig = {
Type = "simple";
ExecStartPre = "${pkgs.coreutils}/bin/sleep 2";
ExecStart = "${lib.getExe pkgs.wlsunset} -T 6500 -t 5000 -S 00:00 -s 00:01";
Restart = "on-failure";
RestartSec = 3;
} // waylandClientHardening;
};
# The polkit agent is not a service here: niri spawns it via
# spawn-at-startup in defaults/xdg/niri/config.kdl. Only the FHS path it
# references has to exist — see modules/desktop.nix.
}