Doorman — badge access modernization (KNEL/doorman)

Doorman — badge access modernization (KNEL/doorman)

Dedicated repo for the physical-access badge work, spun out of the
2026-09-02 founder session (“badge access via Home Assistant”):

Repo: Sign In - Gitea (public, AGPLv3)

Lineage

The 2018 system was Perl (doorman.pl, keyboard-emulating USB RFID
reader → HTTP auth portal → usbrelay door strike). It lived unlisted in
KNEL/LegacyTechops doorman/;
the repo carries it verbatim under legacy/ for reference. The defunct
doors.pfv.turnsys.net auth portal is NOT replicated — plain-HTTP auth
is gone for good.

Modern shape (non-HA side shipped; HA side owned by the pfv-bms session)

  • bin/doorman.sh — bash + coreutils ONLY (no CPAN, no package installs
    on the reader Pi). Reads /dev/input/by-id/*-event-kbd with od(1),
    decodes MSC_SCAN HID usages with the 2018 state machine bit-for-bit
    (incl. its quirks: usages 0-9 passthrough, bad-usage parity anchor
    behavior), 24-byte (64-bit) and 16-byte (32-bit Pi) record widths.
  • Each completed scan → POST {badge_id, reader, ts} to the HA webhook
    (contract in the repo README). Fail-closed: HA down ⇒ scan logged,
    door does NOT open.
  • Auth whitelist, logging, unknown-badge alerts, unlock actuation: all
    Home Assistant side (#345 sketch, 2026-09-02 note).
  • Optional legacy relay bridge (usbrelay) behind
    DOORMAN_UNLOCK_ON_2XX, default off, pending the actuator decision.
  • deploy/doorman.service + /etc/default/doorman env refs (token in
    URL path = secret; material stays out of git).
  • Targets: pfvsvrpi (prod reader) / ultix-field (dev reader,
    hands-on in San Antonio).

Verification (2026-09-02, #355 scope — laptop, offline)

  • Decoder suite: 9/9 green (synthetic event fixtures incl. repeated
    digits, garbage usages, truncated tails, both record widths).
  • shellcheck: 7/7 scripts clean, zero warnings incl. info level.
  • Full rule audit: PASS (enforcement scripts adopted from
    TSYSGroupAIOS/PFVCluster).

Tickets

  • #345 — umbrella: physical access control, server room door badge
  • #355 — code & testing (this repo’s active work)
  • #356 — deployment & onsite testing (after #355; serial, health-gated:
    ultix-field first, verify, then pfvsvrpi)

Open items

Reader model/format, actuator path (usbrelay vs new e-lock), whitelist
home — tracked in PFVCluster questions-v4.md (founder answers gate
#356, not the listener code).

Update 2026-09-02 — two founder rulings recorded:

1. Scope containment: badge access is fully contained to the
server-room door. No other smart locks in the house are in scope —
any such work is a separate ticket for founder approval. Now written
into KNEL/doorman AGENTS.md (c4f1015) and Redmine #345.

2. How HA manages valid badges — git YAML roster (recommended and
recorded):
the roster lives as a git-tracked YAML mapping in the
KNEL/pfv-bms HA config (deployed by the existing git_pull pipeline),
not a UI-only helper:

  • webhook-triggered automation receives {badge_id, reader, ts}
  • automation-level variables: carries the roster:
    badge_id → identity, seeded with the two 2018 badges
  • membership condition → log + unlock path; default → unknown-badge
    alert; night schedule = alert-only
  • every grant/revoke = commit (+ ticket ref) = audit trail, matching
    the IaC mandate (#454) and the “user makes policy, agent implements”
    rule
  • input_text UI helper rejected as primary store (drift, no audit
    trail); fine later as a read-only dashboard mirror

The listener stays dumb on purpose: the reader host holds no
whitelist, so a compromised Pi cannot open anything by itself.

Update 2026-09-02 #2 — PR gate live + hardware confirmed:

Two-approver PR process is ENFORCED on
KNEL/doorman: main is
PR-only, merges require 2 human approvals (server-side verified —
direct pushes are rejected), rejected reviews block, stale approvals
dismiss. A PR sign-off template (.gitea/pull_request_template.md)
carries the ticket ref, access-control checklist, verification box,
and named Approver 1/2 lines. Agents author; only humans approve and
merge. Named approvers: pending founder (questions-v5 Q4).

Reader hardware probed (founder cleared): both hosts run the exact
2018 reader — 13ba:0018 PCPlay Barcode PCP-BCG4209 keyboard-emulating
HID. pfvsvrpi (prod): aarch64, reader on event0, 2018 usbrelay dongle
(16c0:05df) present and accounted for. ultix-field (dev): x86_64,
reader on event15, scan-only. Both 64-bit → 24-byte event records,
matching the shipped decoder defaults. usbrelay userspace tool not yet
installed on pfvsvrpi — ticket-gated deploy item for #356.

Update 2026-09-02 #3 — deployed on both hosts + roster repo born:

  • Stage-1 listener is LIVE on pfvsvrpi
    (prod, /dev/input/event0) and ultix-field (dev, event15): services
    active, reader streams open, decoder suite 9/9 on-host on both
    (incl. aarch64). Log-only until the HA webhook flips on (env line
    commented — no redeploy needed). Founder scan UAT: journalctl -u doorman -f on ultix-field.
  • Gate re-scope (founder): code + host work = no gates, fast
    movement; the two-human gate belongs to badge IDs only.
  • KNEL/access-roster (private) is that gate, materialized:
    PR-only + two human approvals enforced; roster/badges.yaml is the
    single home of badge IDs (Charles NW seeded enabled, Josef C
    disabled pending re-enrollment PR). Fail-safe: no second human =
    no roster merges, by design.
  • HA consumption wiring (submodule of pfv-bms via git_pull vs sync
    step): pfv-bms session’s call; contract unchanged.