- tools/make_deploy_bundles.py: PMS7003·Zero2W 디스플레이 노드 번들 생성 반영(폐쇄망 동봉) - tools/harden_pi.sh: 하드닝 스크립트 갱신 - docs/superpowers: 노드 센서 통합·스모크 파일럿·대시보드 다크 설계/계획 문서 - docs/claude-config: IoT 납품·검증 스킬/룰 참조본 - review/: 프로젝트 리뷰·UI v2 목업
5.8 KiB
5.8 KiB
| name | description |
|---|---|
| iot-airgap-delivery | Use when working on IoT sensor systems with air-gapped deploy bundles (Pi nodes + shared-hosting PHP server) — covers derived-key auth, bundle regeneration workflow, 403/WAF debugging, display render verification, and pre-delivery audit. Trigger on deploy bundle work, HMAC key mismatch/403 errors, Pi display rendering, or "납품" (delivery) prep. |
IoT Air-gap Delivery Workflow
Battle-tested workflow for IoT systems shaped like: sensor nodes (Raspberry Pi / STM32) → shared-hosting PHP server (cafe24-style) → optional mini display node, deployed as self-contained offline bundles generated by a bundle-generator script.
1. Key architecture (get this right first — most repeated confusion)
- API_KEY = master secret. Lives ONLY in: server
config.local.php+ build machinedeploy/.secrets/api_key.txt. Never on leaf devices. - Derived keys =
HMAC-SHA256(message=<label>, key=API_KEY):STATUS_KEY = hash_hmac('sha256', 'status', API_KEY)→ display node onlyCRON_SECRET = hash_hmac('sha256', 'cron', API_KEY)→ cron HTTP access
- The server stores NO derived keys — it recomputes them per request and compares with
hash_equals. So "server-side status key" does not exist as a stored value. - To print the expected derived key on the server:
php -r 'require "config.php"; echo hash_hmac("sha256","status",API_KEY),"\n";' - NEVER put the same value in both API_KEY and a derived-key slot. They are different values.
- Regenerating
api_key.txtbreaks EVERY deployed node. The generator must reuse an existing secret file; verify the printed key prefix is unchanged after regeneration.
2. Bundle workflow (source is truth)
- Edit source files only (never hand-edit files inside
deploy/). - Re-run the generator (e.g.
python tools/make_deploy_bundles.py). - Confirm output reuses the existing API_KEY (prefix printed unchanged).
- Deploy: server bundle → hosting upload; node bundle → copy to Pi →
sudo systemctl restart <service>. deploy/must be gitignored (contains live secrets); remind to back updeploy/.secrets/offline — losing it orphans every deployed node.
3. 403 / auth-failure debugging ladder (in order)
- Distinguish WHO rejected:
curl -sS -i -H "X-<Key-Header>: <key>" <url>- JSON error body (
{"error":"forbidden"}) → app rejected → key mismatch. - HTML 403 → Apache/WAF rejected → NOT a key problem.
- JSON error body (
- Key mismatch: print server-side derived key (see §1), diff against device config byte-for-byte (watch for pasted whitespace/newlines).
- curl works but device fails with SAME key →
a. Service not restarted after config edit (config imported once at process start).
b. Service loads a DIFFERENT config file (check systemd
WorkingDirectory/ExecStart, grep the actual file the service imports). c. WAF blocks defaultpython-requests/x.yUser-Agent (very common on shared hosting; curl's UA passes). Fix: always send a custom UA ("<AppName>/2.0") in everyrequestscall. Reproduce withcurl -A "python-requests/2.31.0". - 503
server_not_configured→ server API_KEY still default → upload real config.local.php. - Placeholder junk keys (e.g.
wnegfjk...) in a config = never-configured file; find which file the process actually loads before editing anything.
4. Display node changes (small TFT, Pillow rendering)
- Any render-code change: verify OFF-DEVICE first. Write a scratchpad script that stubs
config+ driver modules viasys.modules, feeds synthetic data (including worst cases: all-sections node, missing history, single data point, extreme values), saves PNGs at the panel's exact resolution. - Read the PNGs back (vision) to check: no overlapping text, no clipping at panel edges, no dead zones; then send previews to the user via SendUserFile before deploying.
- Layout: slot-based adaptive layout (only sections the node type needs, evenly divided) beats fixed y-coordinates. Guard: clamp page index when sensor count shrinks; fall back to a smaller font when a formatted value exceeds panel width.
- SPI display init fail
No such file or directory→/dev/spidev*missing → enable SPI (sudo raspi-config nonint do_spi 0) + reboot; then check user inspi,gpiogroups.
5. Server API endpoints (shared hosting)
- Poll-heavy endpoints (device polls every ~30s): every time-window query MUST ride an
index. Composite
(sensor_id, measured_at)needs asensor_idpredicate — loop per-sensor with a prepared statement instead of one global range scan. - Cap unbounded result sets (
LIMIT) — a flapping sensor must not inflate the JSON payload. - Wrap the whole handler in try/catch → generic
{"error":"internal"}+error_log; setini_set('display_errors','0')centrally (shared hosting often ships On). - Rate-limit per IP (file-based fixed window, LOCK_EX, same pattern as login throttle); fail-open on storage errors so monitoring devices never die from the throttle itself.
- Guard optional tables with a
table_existshelper before querying. - Additive JSON only: add keys, never rename/remove — old deployed clients keep working.
6. Pre-delivery audit (납품 전)
- Launch TWO parallel review agents: security-reviewer + code-reviewer, scoped to the changed surfaces. Verify each finding against actual code before acting.
- Always check FIRST: real secret files (
config_local.py,config.local.php) covered by.gitignoreAND never committed (git log --all --full-history -- <path>); rotate if leaked. - Fix by severity: CRITICAL (secrets/injection) → HIGH (perf/full scans) → MEDIUM (error disclosure, unbounded payloads) → LOW (render edge cases).
php -levery touched PHP file; re-run render verification; regenerate bundles; then commit.