tucomunidad SecBox — edge security box service https://tucomunidad.ai
  • TypeScript 79.8%
  • Shell 14.7%
  • Python 4%
  • Dockerfile 1.5%
Find a file
Portal Dev 062fd554be
Some checks failed
Build and publish SecBox images / secbox (push) Has been cancelled
Build and publish SecBox images / face-service (push) Has been cancelled
Build and publish SecBox images / relay-client (push) Has been cancelled
fix(compose-snapshot): ensure secbox.env exists + sanity-check before compose-up
Today's Caseta5 outage was caused by /etc/tucomunidad/secbox.env
being absent on a legacy install (pre-claim-loop pairing). When
compose.snapshot pushed the new YAML and ran 'podman-compose
--env-file /etc/tucomunidad/secbox.env up', podman-compose
silently no-op'd the file load + every ${VAR:-default} substitution
fell through to literal strings in container env. The tunnel then
tried to refresh-jwt against the literal "${CLOUD_BASE_URL:-...}"
URL, failed parse, retried forever. Cloud lost the WSS; bridge SSH
gone too.

Two prevention layers:

1. ENSURE: before compose-up, the handler creates secbox.env from
   /etc/tucomunidad/mgmt-agent.env if absent or empty.
   CLOUD_BASE_URL + DEVICE_ID + BOOTSTRAP copied as-is;
   DEVICE_GATEWAY_URL derived from MGMT_GATEWAY_URL by swapping
   the path suffix. chown to the podman runtime user (tc/tcops)
   so podman-compose can read it.

2. SANITY: hard-check secbox.env contains non-empty
   DEVICE_ID + CLOUD_BASE_URL. If not, abort with
   phase='secbox-env-missing-keys' BEFORE recreating containers.
   Better to fail loudly at the cloud than to wedge the box for
   hours with broken substitutions.

Verified on Caseta5: '[compose-snapshot] secbox.env sanity ok
(2/2 required keys present)' in journal, HTTP 200 from
compose.snapshot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:02:59 -04:00
.forgejo/workflows feat(ci): cosign sign all secbox images on push 2026-05-08 00:39:35 -04:00
.husky chore(ci): pre-commit TypeScript strict-mode check 2026-05-03 11:55:50 -04:00
docs chore(secbox): initial scaffold — TS service skeleton + Docker stack 2026-05-03 08:38:29 -04:00
face-service feat(face-service): accept photoBaseName in /enroll for sha256 dedupe 2026-05-07 12:50:54 -04:00
mgmt-agent fix(compose-snapshot): ensure secbox.env exists + sanity-check before compose-up 2026-05-08 20:02:59 -04:00
prisma feat(secbox): hardening trio — nftables egress, sudo audit shipper, embedding cipher mirror 2026-05-07 12:13:08 -04:00
recovery-agent feat(recovery-agent): tolerate missing creds at startup so install can chain 2026-05-08 18:10:42 -04:00
relay-client feat(relay-client): provider-swappable media relay container 2026-05-05 09:48:06 -04:00
scripts feat(host-setup): TUCOMUNIDAD_SUDO_MODE=allowlist for production 2026-05-07 18:46:46 -04:00
src fix(sudo-audit): use @<unix-seconds> for journalctl --since 2026-05-07 12:20:23 -04:00
.env.example chore(secbox): initial scaffold — TS service skeleton + Docker stack 2026-05-03 08:38:29 -04:00
.gitignore chore(secbox): initial scaffold — TS service skeleton + Docker stack 2026-05-03 08:38:29 -04:00
docker-compose.yml feat(compose): bind /var/log/secbox-lan.log + set LAN_ACCESS_LOG_PATH 2026-05-08 15:19:44 -04:00
Dockerfile fix: hardcode FRIGATE_RESTART_CMD + ship curl in image 2026-05-05 23:42:56 -04:00
frigate-config-sample.yml feat: bring secbox repo to functional parity with caja/edge/pair.mjs 2026-05-04 07:20:13 -04:00
install.sh feat(install): fetch bundle as a single tarball + cloud proxy URL 2026-05-05 17:24:42 -04:00
package-lock.json feat(handlers): frigate.config.read + frigate.config.update 2026-05-05 15:21:42 -04:00
package.json feat(persistence): pairing survives container recreation 2026-05-05 22:52:32 -04:00
README.md docs(readme): document container-mode + host-mode update paths 2026-05-07 12:49:50 -04:00
tsconfig.json chore(secbox): initial scaffold — TS service skeleton + Docker stack 2026-05-03 08:38:29 -04:00

tucomunidad SecBox

The edge security service that runs in caseta de seguridad. Connects the caseta's cameras + Frigate NVR to tucomunidad cloud, holds a local replica of bans + scheduled guests, keeps working when the internet is down.

This repo is intentionally isolated from the tucomunidad cloud monorepo. A SecBox lives in physically-accessible hardware; a compromise must not leak server-side source.

What's shipped

  • Cloud tunnel (src/tunnel/) — outbound WSS to device-gateway.tucomunidad.ai with auto-reconnect + backpressure.
  • Local replica (prisma/schema.prisma) — Postgres subset of cloud schema (Community, BannedVisitor, GuestAuthorization, OutboundEvent).
  • Sync down (src/sync/down.ts) — applies snapshot.* and delta.* from the cloud to the local DB.
  • Sync up (src/sync/up.ts) — durable OutboundEvent queue with drain-on-reconnect; outage-tolerant, idempotent (clientId).
  • Pairing UI (src/pairing/) — port 8080 first-boot HTTP form; pastes the QR JSON, validates, persists env, restarts secbox.
  • LAN HTTPS (src/lan/) — port 8443 self-signed; placeholder PWA fallback endpoints (/api/lan/ban-list, /api/lan/schedule).
  • Frigate proxy (src/handlers/frigate.ts) — single-frame snapshots + continuous MJPEG stream over the tunnel.
  • Health metrics (src/health/) — periodic health.report to cloud (containers, disk, memory, CPU, queue depth).
  • CI (.forgejo/workflows/build.yml) — multi-arch image publish on push to main.

Architecture

The cloud spec, threat model, and protocol live in the cloud's plan file. In short, the SecBox:

  • Dials the cloud over WSS — outbound only, no inbound ports.
  • Authenticates with a device-JWT rotated nightly.
  • Holds a local Postgres replica (Community, Unit, User, GuestAuthorization, BannedVisitor — all single-community).
  • Runs Frigate (NVR + camera ingest) and proxies to it.
  • Hosts the guard PWA on caseta WiFi for offline operation.
  • Pushes events (visitor entries, motion clips, face matches, health metrics) up the tunnel.

Install

On a Debian 12 host (≥ 16 GB RAM, ≥ 1 TB SSD):

curl -fsSL https://tucomunidad.ai/install/secbox | sh

(For the dev environment use https://dev.tucomunidad.ai/install/secbox.)

The installer pulls images, sets up systemd user services, and exposes a local pairing UI on port 8080. Open http://<box-LAN-IP>:8080 and paste the QR JSON from the super-admin's /admin/devices page to claim the SecBox.

Develop

git clone https://git.intino.ai/tucomunidad.ai/secbox.git
cd secbox
npm install            # installs husky pre-commit hook automatically
npx prisma generate
# Copy .env.example to .env and fill in cloud + frigate URLs.
npm run dev

Pre-commit checks

npm install registers a husky pre-commit hook that runs tsc --noEmit (the same strict-mode TypeScript check CI runs). Local commits fail fast on type errors instead of round-tripping through CI. To run the check manually:

npm run lint:ts        # type check only
npm run check          # type check + eslint

Bypass the hook only for emergencies: git commit --no-verify.

Media relay (provider-swappable)

Recordings playback (HLS) and snapshot strips reach the browser via a media relay — a tunnel that runs alongside the secbox process and exposes the box's local HTTPS server (port 8443) at a public URL the browser hits directly. Our cloud handles signaling + auth; bytes never flow through it.

The relay-client container bakes both cloudflared and rathole so switching providers is a config change in /etc/tucomunidad/secbox.env, not a rebuild or reinstall:

# Phase 1 (PoC, free): Cloudflare Tunnel
TUNNEL_PROVIDER=cloudflare
TUNNEL_TOKEN=eyJhI...

# Phase 2 (production, self-hosted): rathole on a Hetzner VPS
TUNNEL_PROVIDER=rathole
TUNNEL_SERVER=relay.humket.com:6000
TUNNEL_TOKEN=<per-device-secret>
TUNNEL_REMOTE_NAME=<device-id>

# Disabled — no media tunnel, recordings fall back to cloud-relay
TUNNEL_PROVIDER=none

After editing the env file, podman-compose restart relay-client picks up the new provider. No code change, no compose change, no secbox-process restart needed. Architecture rationale + migration triggers in the cloud repo's docs/MEDIA-RELAY-PLAN.md.

Wire format

Documented in docs/wire-format.md. The cloud's mirror is at src/lib/services/device-gateway/messages.ts in the tucomunidad repo. Both sides version messages by their type discriminator; older boxes silently ignore unknown types. CI runs a contract test against a frozen golden message set to catch drift.

Updating a deployed box

Two install modes, two update paths:

Container-mode (Caseta5-style podman-compose):

  1. Push commits to main. CI (.forgejo/workflows/build.yml) auto- builds + publishes git.intino.ai/tucomunidad-images/secbox:stable to the Forgejo registry on every push.
  2. Watchtower on each box polls the registry every WATCHTOWER_POLL_INTERVAL (24h default) and pulls the new tag. Container is restarted automatically.
  3. To force-pull immediately: podman-compose pull secbox && podman-compose up -d secbox
  4. Verify on the box: podman logs --tail 30 secbox should show the new boot banner with whatever's freshest in dist/index.js.

Host-mode (Baldwin-style raw node dist/index.js):

  1. Build locally (or on this Debian sandbox per the build-where-it's- fastest convention): cd /home/c/claude/secbox && npm run build.
  2. tar+scp the artifacts:
    tar cf /tmp/secbox-update.tar dist src
    scp /tmp/secbox-update.tar tc@<box>:/tmp/
    ssh tc@<box> 'cd ~/secbox.new && rm -rf dist src && \
      tar xf /tmp/secbox-update.tar && \
      kill -9 $(pgrep -f "node dist/index.js")'
    
    The wrapper (run.sh, started at boot via cron @reboot) auto- respawns within 5s.
  3. Verify: tail /home/tc/secbox-data/secbox.log for the boot banner.

The Bearer-aware refresh-jwt path (cloud hardening backlog #1.1) requires the box to run code from 2026-05-07 or later. Until every box runs that, keep PORTAL_BOOTSTRAP_ENFORCE_CONSUMED unset on the cloud — device.bootstrap_reused audit entries are the canary that tells you which boxes are still on legacy code.

License

Private. All rights reserved by tucomunidad.ai.