Files
LEDMatrix/docs/PERMISSIONS.md
T
ChuckandClaude Opus 5.5 695ff92009 feat(ipc): display control socket, stage 1 - on-demand with acks (#706)
The display serves a control socket (/run/ledmatrix/control.sock) carrying versioned JSON commands, one per line, each answered. Stage 1 covers on-demand start, stop and status; commands are queued on the socket thread and applied on the render thread through the mailbox's own handler, and the web interface falls back to the file mailbox when the socket is unavailable. Protocol and security model: docs/IPC_CONTROL_SOCKET.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-01 10:33:02 -04:00

8.4 KiB

Permissions

Who owns what on an installed system, which privileged commands the web interface may run, and how to repair ownership when it goes wrong. The installer, first_time_install.sh, sets all of this up; this page describes the result.

Users and groups

Account Used by Why
root ledmatrix.service (the display) The LED matrix library needs direct GPIO access
The installing user (e.g. ledpi) ledmatrix-web.service, ledmatrix-update-verify.service A web server should not run as root
ledmatrix group shared files Members: the installing user, root, and daemon if it exists. Created by setup_cache.sh and the installer

The installer also adds the web user to systemd-journal and adm so the Logs tab can read the journal. Group changes apply after the user logs in again (services pick them up on restart).

Files and directories

Path Owner Mode Notes
Project directory web user dirs 755, files 644, *.sh 755 Set in the installer's "Normalize project file permissions" step
config/ web user 2775
config/config.json web user 644 Written by the web interface
config/config_secrets.json web user : ledmatrix 640 Owned by the web user because the web interface writes it; root reads it regardless of mode
plugin-repos/, plugins/ web user dirs 2775, files 664 The web interface installs and removes plugins
assets/ web user dirs 755, files 644 Root writes downloaded logos regardless
/var/cache/ledmatrix/ root:ledmatrix 2775 (setgid) Shared cache: see below
Cache files creator : ledmatrix 660
/run/ledmatrix/ root 755 tmpfs; RuntimeDirectory= in ledmatrix.service, removed when the display stops
/run/ledmatrix/control.sock root : cache directory's group (ledmatrix) 660 The display's control socket; only root and that group can connect. See IPC_CONTROL_SOCKET.md
scripts/fix_perms/safe_plugin_rm.sh, safe_pip_install.sh root:root 755 Run as root through sudo, so the web user must not be able to edit them
/etc/sudoers.d/ledmatrix_web, ledmatrix_wifi root 440

What keeps it that way at runtime:

  • Config files. Saves go through src/config_manager_atomic.py, which applies get_config_file_mode() (640 for secrets, 644 otherwise) and, when running as root, moves the file's group to the project directory's group (ensure_shared_group_ownership() in src/common/permission_utils.py).
  • Cache files. src/cache/disk_cache.py sets every file it writes to 0660 and gives it the cache directory's group, without relying on the setgid bit. So a file root writes stays readable by the web user.
  • Plugin directories. run.py sets sys.dont_write_bytecode, because root-owned __pycache__ directories inside a plugin stop the web user updating or removing it.

ledmatrix-web.service deliberately has no CacheDirectory=: systemd would re-own /var/cache/ledmatrix to the web user and its primary group, and the web interface could no longer read what the display writes (see the comment in systemd/ledmatrix-web.service).

If /var/cache/ledmatrix is not usable, CacheManager falls back to ~/.ledmatrix_cache, /opt/ledmatrix/cache or a temp directory (src/cache_manager.py). The two services then may not share a cache, and the web UI shows stale or empty display status, on-demand state and plugin health. Fix the directory rather than living with the fallback.

sudo rules

/etc/sudoers.d/ledmatrix_web

Generated by web_sudoers_rules() in scripts/install/lib_sudoers.sh, the only place these rules are defined. Installed by the installer and by configure_web_sudo.sh, both of which check them with visudo -c first. The web user may run, without a password:

  • reboot, poweroff
  • systemctl start|stop|restart|enable|disable|status ledmatrix.service, systemctl is-active ledmatrix[.service]
  • systemctl start|stop|restart ledmatrix-web.service
  • bash <project>/scripts/fix_perms/safe_plugin_rm.sh * — removes a directory only if it resolves to a child of plugin-repos/ or plugins/
  • bash <project>/scripts/fix_perms/safe_pip_install.sh * — installs a requirements.txt only if it is the project's own or one under plugin-repos/ or plugins/, so the root display service can import the packages
  • journalctl -u ledmatrix.service *, -u ledmatrix *, -t ledmatrix *, tagged NOEXEC: journalctl opens a pager on a terminal, and a shell escape from that pager would be a root shell

/etc/sudoers.d/ledmatrix_wifi

Written by scripts/install/configure_wifi_permissions.sh (run as the web user; the installer calls it). It refuses to grant a binary that is not root-owned or is group/world-writable. The rules cover:

  • nmcli device wifi connect|disconnect *, nmcli device connect|disconnect *, nmcli radio wifi on|off
  • systemctl start|stop|restart hostapd, ... dnsmasq, systemctl restart NetworkManager
  • sysctl -w net.ipv4.ip_forward=0|1
  • nft add|delete table ip ledmatrix
  • rfkill unblock wifi
  • mkdir -p /etc/NetworkManager/dnsmasq-shared.d
  • cp of /tmp/hostapd.conf and /tmp/dnsmasq.conf to their fixed destinations, and rm -f /etc/dnsmasq.d/ledmatrix-captive.conf
  • cp /tmp/ledmatrix-nm-dnsmasq.conf to /etc/NetworkManager/dnsmasq-shared.d/ledmatrix-captive.conf, and rm -f of that file

iptables is deliberately not granted. The captive portal's rules are built from the interface name and port, so a rule covering them would need a trailing wildcard, and iptables --modprobe=<path> runs <path> as root: a wildcard grant is a root shell for the web user. Doing it safely needs a wrapper script that builds the rules itself, like safe_plugin_rm.sh. On a stock Raspberry Pi OS image the default user's blanket NOPASSWD rule (/etc/sudoers.d/010_pi-nopasswd) hides this gap.

polkit

The same script installs /etc/polkit-1/rules.d/10-ledmatrix-wifi.rules, which lets the web user perform any org.freedesktop.NetworkManager.* action without authentication.

Repair scripts

In scripts/fix_perms/. Run them from the project directory.

Script Run as What it does Notes
fix_plugin_permissions.sh sudo plugins/ and plugin-repos/ to root:<user>, dirs 2775, files 664; makes a 700 home directory 755 so root can traverse it Safe. Group-writable, so the web user keeps write access
fix_assets_permissions.sh sudo assets/ to <user>:<group>, mode 777 recursively Works, but looser than the installer's 755/644
fix_cache_permissions.sh sudo Runs setup_cache.sh for /var/cache/ledmatrix (root:ledmatrix, 2775, files 660), then makes ~/.ledmatrix_cache (the fallback cache) <user>:<group> mode 777 Safe. The ~/.ledmatrix_cache mode is still 777
fix_web_permissions.sh the web user, without sudo Resets project file ownership for the web user (it calls sudo itself), then makes safe_plugin_rm.sh and safe_pip_install.sh root:root 755 again and restores config_secrets.json to its owner, group ledmatrix, mode 640 Refuses to run as root. It does not write sudoers rules
safe_plugin_rm.sh, safe_pip_install.sh — Called by the web interface through sudo Not for manual use

To reinstall the sudoers rules, run ./scripts/install/configure_web_sudo.sh (web rules) or ./scripts/install/configure_wifi_permissions.sh (WiFi rules and polkit) as the web user, not with sudo.

After any of these, restart both services:

sudo systemctl restart ledmatrix.service ledmatrix-web.service

Checking

ls -ld /var/cache/ledmatrix          # drwxrwsr-x root ledmatrix
stat -c '%U:%G %a %n' config/config.json config/config_secrets.json
id                                   # web user should list ledmatrix
sudo -l                              # lists the NOPASSWD rules