docs(doorman): README — architecture, webhook contract, deploy runbook [#355]

Cross-links: Discourse t/318 (docs SoR), Redmine #345/#355/#356,
PFVCluster, LegacyTechops lineage.

https://community.turnsys.com/t/318
This commit is contained in:
2026-09-02 21:13:07 -05:00
parent 84ea6e931e
commit fbb43e7035
+99
View File
@@ -0,0 +1,99 @@
# doorman — badge access, modernized
Physical access control for the server-room door: badge-reader listener
for the reader host, dispatching scans to Home Assistant, which owns the
whitelist, logging, alerts, and unlock decision.
**Dedicated docs topic: [Discourse t/318](https://community.turnsys.com/t/318)**
· Tracking: [Redmine #345](https://projects.knownelement.com/issues/345)
(umbrella) → [#355](https://projects.knownelement.com/issues/355)
(code & testing) → [#356](https://projects.knownelement.com/issues/356)
(deployment) · Ops status: [Discourse t/297](https://community.turnsys.com/t/297)
· Fleet context: [KNEL/PFVCluster](https://git.knownelement.com/KNEL/PFVCluster)
## Lineage
Successor of the 2018 Perl doorman
([KNEL/LegacyTechops `doorman/`](https://git.knownelement.com/KNEL/LegacyTechops/src/branch/main/doorman/doorman.pl)),
carried verbatim under [`legacy/`](legacy/doorman.pl). Same reader class
(keyboard-emulating USB RFID), same decode semantics — but the defunct
plain-HTTP auth portal (`doors.pfv.turnsys.net`) is NOT replicated: HA is
the single authentication brain.
## Architecture
```
USB RFID reader (HID keyboard)
└─> /dev/input/by-id/*-event-kbd (reader host: pfvsvrpi prod,
| ultix-field dev)
v
bin/doorman.sh od(1) + awk(1) decode — bash and
| coreutils ONLY, no package installs
v
POST {badge_id, reader, ts} -----> Home Assistant webhook
| whitelist decision
| logbook/recorder history
| unknown badge -> instant alert
'- unlock (e-lock or relay)
```
Fail-closed by construction: if HA is unreachable, the scan is logged and
dropped — the door does not open. The optional local relay bridge
(`usbrelay`, 2018 lineage) exists behind `DOORMAN_UNLOCK_ON_2XX` (default
**off**) for the transition period; native HA webhooks always answer 200,
so that flag must stay off until HA returns real accept/deny codes.
### Webhook contract (for the HA side)
`POST <DOORMAN_WEBHOOK_URL>` with JSON:
```json
{"badge_id": "0000000001", "reader": "pfvsvrpi", "ts": "2026-09-02T19:12:44-0500"}
```
- Values are digits + env-provided strings; the JSON cannot carry
user-controlled quotes/backslashes.
- Any 2xx satisfies the listener; HA automations do the real work.
- Configure the HA webhook trigger to this URL; the token lives in the
URL path — treat it as a secret, keep it in `/etc/default/doorman`
(0600) on the reader host, never in git.
## Deploy (reader host)
```bash
sudo mkdir -p /opt/doorman
sudo cp bin/doorman.sh /opt/doorman/bin/ # + this repo, or rsync
sudo cp deploy/doorman.service /etc/systemd/system/
sudo cp .env.example /etc/default/doorman # fill in, chown root:root
sudo chmod 600 /etc/default/doorman
sudo systemctl daemon-reload && sudo systemctl enable --now doorman
```
Record width auto-detects (24 B on 64-bit kernels, 16 B on 32-bit Pis).
Logs go to syslog/journald (`-t doorman`); `DOORMAN_DEBUG=true` mirrors
to stderr. Deployment discipline: **ultix-field (dev) first, verify
end-to-end, then pfvsvrpi (prod)** — never both at once (#356).
## Testing (no hardware needed)
```bash
bash scripts/test.sh # decoder suite: synthetic event fixtures
bash tests/shellcheck.sh # lint gate (docker, zero-warning)
bash scripts/check-rules.sh # full rule audit
```
`--selftest` mode decodes a fixture file and prints one badge ID per
line — that is the entire test surface of the decoder, including the
2018 quirks we intentionally preserve (usage passthrough 0-9, bad-usage
parity-anchor behavior, empty-scan suppression).
## Repo map
| Path | What |
|------|------|
| `bin/doorman.sh` | The listener daemon |
| `deploy/doorman.service` | systemd unit (`/etc/default/doorman` env file) |
| `tests/run-tests.sh` | Offline decoder suite (9 tests) |
| `legacy/` | Verbatim 2018 Perl snapshot (not maintained) |
| `scripts/` | Rule engine + hooks (adopted from TSYSGroupAIOS) |
| `questions-v1.md` | Points to the active round (PFVCluster questions-v4.md) |