mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-10-04 14:25:08 +00:00
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>
157 lines
8.4 KiB
Markdown
157 lines
8.4 KiB
Markdown
# 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` | |
|
|
|
|
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 <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`](../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/`](../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`](../scripts/install/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:
|
|
|
|
```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
|
|
```
|