Documentation
Getting Started
Documentation Index Installation Details Quickstart
Architecture
Alert Geometry Coverage Data Flow Sequences Design Standards Display System Architecture Eas Decoding Summary Eas Monitor V3 Architecture Sdr Service Architecture System Architecture Theory Of Operation
Development
Agents Certification Reliability Plan Component Library Contributing Fcc Part11 Compliance Matrix Javascript Api Releasing Sdr Frequency Validation User Interface Guide
Hardware
Alpha Led Guide Argon40 Zigbee Setup Gpio Guide Gps Hat Setup Hwsetup Helper Neopixel Led Control Sdr Setup Serial To Ethernet Adapters Vfd Display Setup Waveshare Rs232 Wifi Setup
Guides
Alert Signals Analytics And Reporting Api Key Management Application Settings Audio Monitoring Audit Log Review Database Backups Disk Space Cleanup Eas Config Tool Eas Test Signal Pipeline Hardware Quickstart Health Monitoring Help Https Setup Icecast Streaming Setup Ipaws Feed Integration Local Authorities Local Mail Server Manual Eas Events Mfa Totp Setup Notifications One Button Upgrade Setup Instructions Smart Setup Ssl Web Ui Guide Tailscale Setup Tts Normalization
Troubleshooting
Audio Sdr Fix Tool Firewall Requirements Polling Not Working Sdr Master Troubleshooting Guide Troubleshooting 504 Timeout Tts Troubleshooting
Security
Audit Log Integrity Security
Reference
About Alpha M Protocol Changelog Dependency Attribution Diagrams Ecig Cap To Eas Implementation Guide V1 0 Fips Data Sources Mdc1200 Nrsc4B Same Standard Nws Alert Parameters Nws Zone Catalog Ohio Eas Documentation Overview Protocols Overview Rbds Standard Repository Statistics Sage Endec Same Vtec Event Linking
Policies
Privacy Policy Sms Messaging Terms Of Use Trademark Policy

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 in COMMANDS.
  • 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, group eas-station) restrict who can connect. We log peer credentials via SO_PEERCRED for 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 argv it executes from a fixed shape (no shell=True, no string concatenation of user input).
  • Returning a result dict with ok, exit_code, and an error string 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

  1. 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}
    
  2. Register it:

    COMMANDS: Dict[str, CommandFn] = {
        "ping": _cmd_ping,
        "seed_rtc": _cmd_seed_rtc,
    }
    
  3. Restart the helper: sudo systemctl restart eas-station-hwsetup.service.

  4. 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.