Hardware Setup Helper
eas-station-hwsetup is a small root-owned daemon that performs the
privileged operations needed to bring a GPS HAT online — apt install
of chrony / gpsd / util-linux-extra, idempotent edits to
/etc/default/gpsd, /etc/chrony/chrony.conf, and
/boot/firmware/config.txt, hwclock -w after replacing an exhausted
RTC coin cell, and systemctl enable --now of the resulting services.
The unprivileged eas-station web service connects to it over a Unix
socket and dispatches commands from a hardcoded allowlist.
This document covers installation, the threat model, and how to extend the command registry. End-user-facing setup actions live in the admin UI under Admin → Hardware Settings → GPS → GPS HAT Setup Status.
Why a separate daemon
The web service (eas-station-web.service) runs as eas-station with
NoNewPrivileges=true. It cannot apt install, write to
/boot/firmware/config.txt, or restart chrony directly. The two
commonly considered alternatives both have problems:
| Approach | Why we rejected it |
|---|---|
| Sudoers wildcard for the service user | Wildcards on apt install … or tee /etc/… are trivially exploitable into arbitrary command execution. |
| Run the web service as root | Drags every transitive Python dependency (Flask, Jinja2, SQLAlchemy, Werkzeug, …) into the root attack surface. |
A separate stdlib-only daemon with an enumerated command registry is
the cheapest way to keep the privileged surface auditable: every
action is a Python function in
bin/eas-station-hwsetup, and a
compromise of the web tier cannot reach root by inventing new actions
— the helper rejects anything not in COMMANDS.
Installation
The helper consists of three files:
| Source path | Install path |
|---|---|
bin/eas-station-hwsetup |
/opt/eas-station/bin/eas-station-hwsetup |
systemd/eas-station-hwsetup.service |
/etc/systemd/system/eas-station-hwsetup.service |
| (socket created at runtime) | /run/eas-station/hwsetup.sock |
After pulling the latest code:
sudo install -m 0755 /opt/eas-station/bin/eas-station-hwsetup \
/opt/eas-station/bin/eas-station-hwsetup
sudo cp /opt/eas-station/systemd/eas-station-hwsetup.service \
/etc/systemd/system/eas-station-hwsetup.service
sudo systemctl daemon-reload
sudo systemctl enable --now eas-station-hwsetup.service
The service unit creates /run/eas-station/ via RuntimeDirectory=
and the helper drops the socket inside it as root:eas-station mode
0660 so only members of the eas-station group can connect.
Verify:
sudo systemctl status eas-station-hwsetup.service
ls -la /run/eas-station/hwsetup.sock
# srw-rw---- 1 root eas-station 0 ... /run/eas-station/hwsetup.sock
# End-to-end ping from the web service user
sudo -u eas-station python3 -c "
import sys; sys.path.insert(0, '/opt/eas-station')
from app_utils.hwsetup_client import call
print(call('ping', {'message': 'hello'}))
"
You should see {'ok': True, 'exit_code': 0, 'stdout': 'hello', ...}.
Wire format
JSON-Lines over a Unix stream socket. One request line in, one or more events out, connection closed.
Request
{"action": "ping", "params": {"message": "hi"}, "request_id": "<uuid-or-omitted>"}
action(str, required) — must be a key inCOMMANDS.params(object, optional) — action-specific arguments. Validated inside the action handler.request_id(str, optional) — generated by the helper if absent; echoed in every event for log correlation.
Hard limits: 64 KiB request line, params nested no more than 4 levels.
Response
Zero or more streaming events:
{"type": "stdout", "data": "Reading package lists...\n", "request_id": "..."}
{"type": "stderr", "data": "W: ...\n", "request_id": "..."}
Exactly one terminal event:
{"type": "result", "ok": true, "exit_code": 0, "request_id": "...",
"stdout": "...", "stderr": "...", "error": null}
The connection closes after the terminal event.
Threat model
Trust boundary
The helper trusts:
- Anything on the local filesystem readable by root.
- Connections to its socket — but only because the socket
permissions (
0660, groupeas-station) restrict who can connect. We log peer credentials viaSO_PEERCREDfor audit, but do not use them for authorization.
The helper does not trust:
- The contents of any request — every parameter is validated inside the action handler before it reaches a privileged syscall or subprocess.
Action allowlist
COMMANDS is a dict of name → function in the helper module. Adding
an action requires editing the helper itself; no remote registration,
no plugin loading, no eval. Each action is responsible for:
- Validating its own params (types, length, allowed values).
- Building any
argvit executes from a fixed shape (noshell=True, no string concatenation of user input). - Returning a result dict with
ok,exit_code, and anerrorstring when applicable.
What is not sandboxed
The systemd unit deliberately does not set ProtectSystem=strict
because legitimate actions need to write to /etc/default,
/etc/chrony, /boot/firmware. Filesystem protection comes from the
per-action allowlist inside the helper, not from the kernel.
What is sandboxed: namespaces, suid/sgid, kernel modules / tunables,
control groups, write+execute memory pages, and the syscall filter
restricts to @system-service, @file-system, and @network-io.
MemoryMax=64M and TasksMax=8 cap a misbehaving handler.
Adding a new action
Implement the function in
bin/eas-station-hwsetup:def _cmd_seed_rtc(params, emit): # validate params ... # build argv from a fixed shape ... # subprocess.run(...), stream output via emit ... return {"ok": True, "exit_code": 0}Register it:
COMMANDS: Dict[str, CommandFn] = { "ping": _cmd_ping, "seed_rtc": _cmd_seed_rtc, }Restart the helper:
sudo systemctl restart eas-station-hwsetup.service.Call it from the web tier:
from app_utils.hwsetup_client import call result = call("seed_rtc", {"confirm": True}, timeout=10.0)
The web side never sees the bytes that produced the result — only the JSON the helper chose to emit. Treat helper output as untrusted from the web tier's perspective: render it as plain text in the UI, never as HTML, and don't pipe it back through a shell.
This document is served from docs/hardware/HWSETUP_HELPER.md in the EAS Station™ installation.