Make LAN access installable, with a token

The status page could not be reached at all from another machine:
the unit runs --serve with no --host, and the app defaults to
127.0.0.1, so nothing listens on the Pi's LAN address. Confirmed by
binding both ways - from the LAN, loopback-bound gives connection
refused. Even once bound, a non-loopback caller needs a token and
install.sh had no way to set one, so the page would load with every
panel empty and every button doing nothing (401).

Adds --host, --allow-remote and --token. The token is written to
bridge.env at mode 0600 and referenced via EnvironmentFile rather
than sitting on the ExecStart line, where 'systemctl cat' would
reveal it and the process list would show it.

Exposing the bridge stays opt-in and deliberate: the installer
refuses a non-loopback --host without --allow-remote, and warns when
no token is given, because off loopback anyone reaching the port can
read taps and switch the LEDs.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-10-05 22:44:14 +03:00
parent c2b3eda649
commit 74c9e0f8fa
2 changed files with 114 additions and 5 deletions
+54
View File
@@ -570,6 +570,9 @@ what `--list-ports` reports.
| Option | Default | Meaning |
| --- | --- | --- |
| `--origin URL` | – | Web app origin allowed to call the API (repeatable) |
| `--host ADDR` | `127.0.0.1` | Bind address. Loopback only by default |
| `--allow-remote` | off | Permit a non-loopback `--host` |
| `--token SECRET` | – | Require this token from non-loopback callers (see below) |
| `--device PATH` | `/dev/serial0` | Serial device the service opens (UART; use `/dev/rdm-reader` for USB) |
| `--led-pin N` | `23` | BCM GPIO number of the status LED (physical pin 16) |
| `--led-mode M` | `presence` | `presence`: lit while a card is on the reader; `error`: lit whenever running |
@@ -605,6 +608,52 @@ what `--list-ports` reports.
added. `--serve` is what makes the API available; without it the app only
prints events, which a service sends to the journal.
### Reaching the page from another machine
The bridge binds **loopback only** by default, so `http://192.168.x.x:8765/`
from your laptop gets a *connection refused* — nothing is listening on that
address. Opening the port to the network is opt-in, and needs a token:
```bash
sudo ./deploy/install.sh --device /dev/serial0 \
--host 0.0.0.0 --allow-remote --token 'pick-a-long-secret'
```
The installer refuses a non-loopback `--host` without `--allow-remote`, and
warns if you skip the token. The app enforces the same rule, so this cannot be
switched on by accident.
> **What you are exposing.** Off loopback, anyone who can reach the port can
> read card taps *and* switch the status LEDs. A tap is not a password, but it
> is a username, and the endpoint reports who has touched the reader. Keep it on
> a trusted network, and set a token.
Loopback callers (the Pi itself) never need the token, so local tools and
`curl` keep working unchanged.
The token is written to `/opt/rdm-card-reader/bridge.env` (mode `0600`) and
referenced by the unit as `EnvironmentFile=`, rather than being placed on the
`ExecStart` line — a value there is readable by anything that can run
`systemctl cat` and appears in the process list.
Once exposed, open the page and paste the token into the box at the top right;
it is kept in `localStorage` and sent as `X-Auth-Token`. Until a token is
supplied the panels stay empty and the LED buttons do nothing, because the API
answers `401`. For the LED buttons specifically:
```bash
# from your laptop, if you invoked install.sh with --host 0.0.0.0 --token SECRET
curl -sS -H "X-Auth-Token: SECRET" http://192.168.0.228:8765/api/led
```
If you would rather not expose the port at all, an SSH tunnel gives you the
same page without opening anything:
```bash
ssh -N -L 8765:127.0.0.1:8765 pi@192.168.0.228
# then browse http://127.0.0.1:8765/ on your laptop
```
### Checking the reader from the command line
```bash
@@ -816,6 +865,11 @@ curl -sS -X POST -H 'Content-Type: application/json' \
- **HTTP 409 `that LED is not available`** → the LED has no usable pin; look at
`reason` in the response, which is the same text as the journal line.
**HTTP 401 `missing or invalid token`** → you are reaching the bridge from
another machine and it was installed with a token. Paste the token into the box
at the top of the status page, or send `X-Auth-Token`. See **Reaching the page
from another machine** above.
Press *hand back to card events* (or POST `{"release": true}`) when finished, or
the LEDs stay held where you left them.