๐ท CAMERA STEP 2 โ NVR behind a cloudflared tunnel, then CLOSE the eero forwards
Written 2026-07-01, reframed same day per Sam (docs-only session โ NOTHING installed or changed yet). LIVING doc.
Where we are: cameras are ALREADY PC-FREE โ ops-apinvrFetch()hits the NVR directly (Architecture B, live, cams verified 200+JPEG). Nothing about the camera pipeline needs fixing.
The ONE remaining task is security:nvrFetchcurrently reaches the NVR (192.168.4.47) through eero WAN port-forwards 8500 + 8501 (verified open 2026-07-01; DDNSd6468120.eero.onlineโ74.101.240.91). Raw internet-facing Hikvision is actively exploited. Step 2 = swap that path for a cloudflared tunnel, then close the forwards.
Context:docs/CAMERA_SYSTEM.mdยท skillscamera-bridge-runbook+cloudflare-deploy-safe.
1 ยท THE HOST โ buy/repurpose a small always-on LAN box (~$50). That's the whole decision.
A cloudflared tunnel terminates where cloudflared runs, so the tunnel host MUST sit on the home LAN with a private route to 192.168.4.47. Everything else is ruled out:
- The NVR itself โ Hikvision NVRs can't run cloudflared. โ
- The eero โ can't run third-party software. โ
- The DigitalOcean VM (104.248.227.149 / Tailscale 100.79.23.85) โ NOT on the home LAN; no private route to the NVR; can never be the tunnel origin. (A Tailscale subnet router would itself require a home box, so it adds nothing.) โ
- A dedicated small always-on device on the LAN โ โ
the answer. Runs only cloudflared; idle load is negligible; survives reboots as a service.
Hardware shortlist (~$50, any of these):
| Box | Notes |
|---|---|
| Used mini-PC (Dell/Lenovo/HP micro, eBay ~$40-60) | x86, most headroom; also unlocks the go2rtc-off-PC durability end state later (CAMERA_SYSTEM.md roadmap) |
| Raspberry Pi 4/5 (or a Pi 3 already in the house) | cloudflared ships official ARM builds; prefer wired Ethernet |
| Any retired laptop/NUC already owned | $0 if one exists; lid closed, power settings = never sleep |
Box setup once: wired Ethernet if possible ยท DHCP reservation in the eero app ยท BIOS/OS set to auto-boot after power loss.
2 ยท TUNNEL SETUP (on the box)
Canonical path โ fresh tunnel nvr (Linux/Pi)
# 1. Install cloudflared (.deb/.rpm from pkg.cloudflare.com โ ARM + x86 builds)
cloudflared tunnel login # browser auth to the hookstreetservices.com zone
cloudflared tunnel create nvr # note the tunnel UUID
Config /etc/cloudflared/config.yml:
tunnel: <UUID>
credentials-file: /root/.cloudflared/<UUID>.json
ingress:
- hostname: nvr.hookstreetservices.com
service: http://192.168.4.47:8500 # NVR LAN IP, HTTP/ISAPI port
- service: http_status:404
cloudflared tunnel route dns nvr nvr.hookstreetservices.com
sudo cloudflared service install
sudo systemctl enable --now cloudflared
systemctl status cloudflared # active (running); survives reboot
(Windows variant โ only if the box ends up being a Windows mini-PC: winget install Cloudflare.cloudflared, same login/create, config at C:\Users\<user>\.cloudflared\config.yml, same route dns, then cloudflared service install from an admin prompt.)
Shortcut IF re-homeable โ reuse the existing treitel-cameras managed tunnel
Tunnel treitel-cameras (id 16a75a7b-5ee7-4715-8bf5-b6ae746eb8da) is remotely managed โ ingress editable via CF API. If its connector credentials can be re-homed onto the new box (connector runs there instead), the NVR is ONE added ingress rule:
{ "hostname": "nvr.hookstreetservices.com", "service": "http://192.168.4.47:8500" }
via PUT /accounts/<acct>/cfd_tunnel/16a75a7b.../configurations (insert BEFORE the catch-all 404; keep the cam.hookstreetservices.com โ http://localhost:1984 rule intact โ its localhost only resolves on whatever box runs go2rtc) + proxied CNAME nvr.hookstreetservices.com โ 16a75a7b....cfargotunnel.com. If re-homing is fiddly, don't force it โ the fresh tunnel above is the canonical path.
Gate it with CF Access (service token โ same pattern as the go2rtc host)
The tunnel hostname is otherwise publicly reachable. Copy the cam.hookstreetservices.com pattern exactly:
1. CF Zero Trust โ Access โ Applications โ new self-hosted app "Treitel NVR" on nvr.hookstreetservices.com.
2. Policies: (a) Sam's Google login (allow), (b) non-identity Service Auth policy for a service token.
3. Service token: reuse ops-api-camera-proxy (the token behind CAM_CF_ID/CAM_CF_SECRET) by adding it to the new app's policy โ zero new secrets โ or mint a fresh ops-api-nvr-proxy token (then two new Worker secrets, e.g. NVR_CF_ID/NVR_CF_SECRET).
4. โ ๏ธ Code reality check: nvrFetch() (ops-api/src/index.ts ~L480) does NVR digest auth only โ it does not send CF-Access-Client-Id/Secret headers today. The Access gate therefore needs a ~4-line addition to nvrFetch (attach the two headers, same as the go2rtc fetch does). Small, but it IS an ops-api code change โ Brain/#042 lane, path-scoped build only.
3 ยท THE ops-api SWITCH (Brain/#042 lane โ read cloudflare-deploy-safe FIRST)
Secrets involved (names only, values never in git): NVR_HOST ยท NVR_USER ยท NVR_PASS.
- NVR_USER/NVR_PASS unchanged โ digest auth passes straight through the tunnel to the NVR.
- NVR_HOST changes: d6468120.eero.online:8500 โ nvr.hookstreetservices.com (no port; edge terminates TLS).
- โ ๏ธ One-line code change required: index.ts:481 hardcodes const base = `http://${env.NVR_HOST}` โ through the tunnel it must be https (make the scheme part of NVR_HOST or default to https). Ship together with the Access-header addition from ยง2.
- Applying it: wrangler secret put NVR_HOST + deploy is the textbook move, but ops-api's 30 secrets are VERSION-EMBEDDED โ a bare wrangler secret put/wrangler deploy strands all 30 (broke prod twice 6/24-25). Until the wrangler secret bulk ops-api/.secrets.json durable fix is confirmed done, the safe path is the CF-API secret op on the live version (inherits the other 29) + the path-scoped Workers Build for the code change โ exactly per cloudflare-deploy-safe. Verify after with a live /camera/snapshot, never wrangler secret list (it lies โ shows 0).
4 ยท CLOSE THE FORWARDS (Sam, eero app) โ ONLY after ยง5 passes through the tunnel
- eero app โ Settings โ Network settings โ Reservations & port forwards โ
192.168.4.47("Hangzhou Hikvision Digital") โ delete/disable the 8500 AND 8501 forwards. (Keep the IP reservation itself.) - Optional hardening: turn OFF Dynamic DNS if nothing else uses
d6468120.eero.online. - Rollback (if cameras break and it's urgent): re-add the 8500 forward to
192.168.4.47in the eero app + setNVR_HOSTback tod6468120.eero.online:8500โ restores the current direct path in minutes. Treat as temporary; the hole reopens.
5 ยท VERIFICATION CHECKLIST (not "done" until ALL pass)
| # | Check | Pass |
|---|---|---|
| 1 | curl "https://ops-api.sam-0f0.workers.dev/camera/snapshot?cam=5" -H "referer: https://ops.hookstreetservices.com/" โ 200 + JPEG (ffd8โฆ) with NVR_HOST = the tunnel hostname |
โฌ |
| 2 | Test 2โ3 cams (5/7/8 โ never 4, it's permanently dead) โ all JPEG | โฌ |
| 3 | Direct hit nvr.hookstreetservices.com in a plain browser โ CF Access login wall, not the Hikvision UI |
โฌ |
| 4 | After closing the forwards: curl -m 10 http://d6468120.eero.online:8500/ from WAN โ timeout/refused (the hole is closed) |
โฌ |
| 5 | /health shows nvr_host_set: true |
โฌ |
| 6 | Portal cameras page loads on Sam's phone off Wi-Fi (cellular) | โฌ |
Source trail: built from docs/CAMERA_SYSTEM.md (2026-07-01 corrected state) ยท hookstreet-skills/camera-bridge-runbook/SKILL.md ยท hookstreet-skills/cloudflare-deploy-safe/SKILL.md ยท ops-api/src/index.ts L83-86/480-500/1028-1076 (read-only). No infra touched. Reframed 2026-07-01 per Sam: cameras are already PC-free; tunnel host = dedicated always-on LAN box ONLY (the work PC is not an option).