# 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`](../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`](../scripts/install/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](IPC_CONTROL_SOCKET.md#security-model) | | `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` | | | `/usr/local/sbin/ledmatrix-refresh-units` | `root:root` | `755` | Copy of `scripts/install/ledmatrix_refresh_units.py`, installed by `install_service.sh`. Outside the project so the web user cannot edit what sudo runs | | `/var/lib/ledmatrix/unit-backup/` | `root` | `700` | The units the last refresh replaced, for the automatic update's rollback | | `/etc/systemd/system/ledmatrix*.service`, `.path` | `root:root` | `644` | Readable so the web interface can compare them with the templates after an update | What keeps it that way at runtime: - **Config files.** Saves go through [`src/config_manager_atomic.py`](../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`](../src/common/permission_utils.py)). - **Cache files.** [`src/cache/disk_cache.py`](../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`](../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`](../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`](../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`](../scripts/install/lib_sudoers.sh), the only place these rules are defined. Installed by the installer and by [`configure_web_sudo.sh`](../scripts/install/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 /scripts/fix_perms/safe_plugin_rm.sh *` — removes a directory only if it resolves to a child of `plugin-repos/` or `plugins/` - `bash /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 - `/usr/local/sbin/ledmatrix-refresh-units ""` and `/usr/local/sbin/ledmatrix-refresh-units --restore` — exactly these two command lines (`""` means "no arguments"). After an update the first installs the systemd units whose templates changed and runs `systemctl daemon-reload`; the automatic update's rollback runs the second to put the previous units back. The helper takes nothing from the caller: the project folder and the web user come from the installed, root-owned `ledmatrix.service` and `ledmatrix-web.service`. It only replaces the four units `install_service.sh` installs, only if they are already installed, and refuses a template that would change a unit's `User=` (root for the display, the web user for the rest) or `WorkingDirectory=`, or that is a symlink, not a regular file, or over 64 KB. It grants nothing new: the templates are files the web user can edit, but so is `run.py`, which the display service already runs as root. ### `/etc/sudoers.d/ledmatrix_wifi` Written by [`scripts/install/configure_wifi_permissions.sh`](../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=` runs `` 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/`](../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:`, 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 `:`, mode `777` recursively | Works, but looser than the installer's `755`/`644` | | `fix_cache_permissions.sh` | `sudo` | Runs [`setup_cache.sh`](../scripts/install/setup_cache.sh) for `/var/cache/ledmatrix` (`root:ledmatrix`, `2775`, files `660`), then makes `~/.ledmatrix_cache` (the fallback cache) `:` 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; the `ledmatrix-refresh-units` rules also need the helper itself, which `sudo ./scripts/install/install_service.sh` installs) 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: ```bash sudo systemctl restart ledmatrix.service ledmatrix-web.service ``` ## Checking ```bash 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 ```