From 248e186dce53dbbeec9696eb5de983be2c41aaac Mon Sep 17 00:00:00 2001 From: Viktor Barzin Date: Thu, 2 Jul 2026 20:01:45 +0000 Subject: [PATCH] CCTV segment (dCCTV 10.0.30.0/24) on a dedicated pfSense leg for the garage camera Viktor and emo are adding the first owned camera at the Sofia site (HiLook IPC-T241H-C watching the garage / server rack). Viktor asked to finalize emo's plan; the grilling session resolved emo's five open decisions and replaced the doc's 802.1Q-trunk idea with the site idiom: a dedicated physical leg (R730 eno2 -> vmbr2 -> pfSense net3 = dCCTV 10.0.30.1/24), port-based VLAN split on the shared TL-SG105PE, camera default-deny with NTP-only egress, Frigate + ha-sofia as the only consumers. The PVE bridge, pfSense interface, Kea subnet and firewall rules were applied live this session (hand-managed hosts, backed up). This commit records the decision (ADR-0017), the glossary terms (Segment / CCTV segment), the as-built architecture doc, and bumps Frigate's ADR-0016 VRAM budget 2000 -> 2300 MiB for the upcoming NVDEC stream. Co-Authored-By: Claude Fable 5 --- CONTEXT.md | 8 ++++ ...0017-cctv-segment-dedicated-pfsense-leg.md | 46 +++++++++++++++++++ docs/architecture/networking.md | 31 +++++++++++-- stacks/frigate/main.tf | 5 +- 4 files changed, 85 insertions(+), 5 deletions(-) create mode 100644 docs/adr/0017-cctv-segment-dedicated-pfsense-leg.md diff --git a/CONTEXT.md b/CONTEXT.md index fa5113d5..5a381475 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -118,6 +118,14 @@ _Avoid_: "external", "outside". `viktorbarzin.lan`, served by Technitium DNS. Resolves only inside the homelab network. _Avoid_: bare "lan", "private", "intranet". +**Segment**: +One isolated L2/L3 network with pfSense as its gateway — realised as one Proxmox bridge feeding one dedicated pfSense interface (dManagementsVms 10.0.10.0/24, dKubernetes 10.0.20.0/24, dCCTV 10.0.30.0/24). pfSense itself never terminates 802.1Q; any tagging happens on the bridge or a switch. +_Avoid_: "VLAN" as the primary name (VLAN 10/20 are informal aliases; dCCTV has no tag on the wire at all). + +**CCTV segment**: +The untrusted camera **Segment** (`dCCTV`) — devices in it may be pulled from (RTSP/ISAPI) but may initiate nothing except NTP to their gateway. Deliberately outside every trusted source-IP allowlist (ADR-0017). +_Avoid_: "camera VLAN", "CCTV LAN". + **Ingress auth**: The `auth = "..."` parameter on `ingress_factory` — a discrete *mode*, not a ranked tier — one of `required` (Authentik forward-auth gates every request), `app` (the backend owns its login), `public` (anonymous Authentik binding for audit only), or `none` (Anubis-fronted content, or native-client API). Default `required` (fail-closed). _Avoid_: "auth tier" / "auth mode" — refer to it by the canonical key, `auth` (e.g. `auth = "required"`). "tier" is reserved for State tier and Namespace tier. diff --git a/docs/adr/0017-cctv-segment-dedicated-pfsense-leg.md b/docs/adr/0017-cctv-segment-dedicated-pfsense-leg.md new file mode 100644 index 00000000..9eeb61c8 --- /dev/null +++ b/docs/adr/0017-cctv-segment-dedicated-pfsense-leg.md @@ -0,0 +1,46 @@ +# CCTV segment on a dedicated pfSense leg, not an 802.1Q trunk + +Status: accepted (2026-07-02) + +The first owned camera at the Sofia/Vermont site (`vermont-garage`, HiLook +IPC-T241H-C at the garage entrance) needs to be network-isolated: its cable is +physically exposed outside the apartment, so anything plugged into that cable +must land in a segment that can reach nothing. The original design doc +(NAS: `Emo shared/Claude shared/garage-camera/`) called for an "802.1Q trunk +to pfSense" — but nothing in this network terminates dot1q on pfSense; the +site idiom is one vlan-aware Proxmox bridge → one tagged VM NIC → one clean +untagged pfSense interface per segment. + +**Decision:** the CCTV segment (`dCCTV`, 10.0.30.1/24) rides a dedicated +physical leg — R730 `eno2` (spare) → new bridge `vmbr2` → pfSense `net3` +(vtnet3), untagged end-to-end. The shared TL-SG105PE PoE switch in the rack +splits via port-based VLANs: {camera port, eno2 uplink} in an internal VLAN, +{home-LAN uplink, 4G router 192.168.1.7, UPS mgmt, switch mgmt 192.168.1.6} +stay in VLAN 1. Cameras are untrusted: default-deny on dCCTV with a single +NTP-to-gateway exception; Frigate (k8s) pulls RTSP in; ha-sofia (192.168.1.8) +may reach ISAPI/RTSP directly; home-LAN clients route in via an AX6000 static +route (10.0.30.0/24 via 192.168.1.2). 10.0.30.0/24 is deliberately NOT in the +10.0.20.0/22 trusted source-IP allowlist. + +## Considered options + +- **802.1Q tag over the existing LAN path (eno1/vmbr0)** — rejected: vmbr0 is + vlan-aware with `bridge-vids 2-4094`, so ANY device on the home LAN could + inject tagged frames straight into the camera segment (defeats the + cable-tap threat model); tag-passing through the unmanaged SW1 is + undefined; and it reconfigures the live bridge carrying the host IP and + pfSense WAN. +- **AX6000 as the camera gateway** — rejected earlier in the design (consumer + router, no inter-VLAN firewall). + +## Consequences + +- eno2 is consumed; eno3/eno4 remain the last spare NICs on the R730. +- The TL-SG105PE is now load-bearing shared infra: it carries pfSense's + backup-WAN path (4G router), UPS mgmt, AND the CCTV segment. Its Easy + Smart mgmt UI answers on every port regardless of VLAN — mitigated by a + strong password; residual L2 risk accepted. +- Adding a future camera = one PoE port in the CCTV VLAN + a Kea + reservation; no pfSense/PVE work. +- Frigate's ADR-0016 VRAM budget was bumped 2000 → 2300 MiB for the extra + NVDEC stream. diff --git a/docs/architecture/networking.md b/docs/architecture/networking.md index 8d383d32..53f7562e 100644 --- a/docs/architecture/networking.md +++ b/docs/architecture/networking.md @@ -1,10 +1,10 @@ # Networking Architecture -Last updated: 2026-04-19 (WS E — Kea DHCP pushes dual DNS per subnet; Kea DDNS TSIG-signed) +Last updated: 2026-07-02 (dCCTV segment added — dedicated pfSense leg for the garage camera, ADR-0017) ## Overview -The homelab network is built on a dual-VLAN architecture with pfSense providing gateway services, Technitium for internal DNS, and Cloudflare for external DNS. Traefik serves as the Kubernetes ingress controller with a middleware chain of anti-AI bot-blocking, Authentik forward-auth, rate limiting, and retry. CrowdSec IP-reputation enforcement is **out-of-band** (not a Traefik hop): banned IPs are dropped in-kernel via nftables on direct hosts and blocked at the Cloudflare edge on proxied hosts (see `docs/architecture/security.md`). All HTTP traffic flows through Cloudflared tunnels, avoiding the need for port forwarding or exposing public IPs. +The homelab network is built on three isolated segments behind pfSense (management VLAN 10, Kubernetes VLAN 20, and the physically-legged dCCTV camera segment — see ADR-0017) with pfSense providing gateway services, Technitium for internal DNS, and Cloudflare for external DNS. Traefik serves as the Kubernetes ingress controller with a middleware chain of anti-AI bot-blocking, Authentik forward-auth, rate limiting, and retry. CrowdSec IP-reputation enforcement is **out-of-band** (not a Traefik hop): banned IPs are dropped in-kernel via nftables on direct hosts and blocked at the Cloudflare edge on proxied hosts (see `docs/architecture/security.md`). All HTTP traffic flows through Cloudflared tunnels, avoiding the need for port forwarding or exposing public IPs. ## Architecture Diagram @@ -24,9 +24,14 @@ graph TB CSdrop[CrowdSec drop
nftables / CF edge
out-of-band, pre-Traefik] - subgraph "Proxmox Host (eno1)" + subgraph "Proxmox Host (eno1, eno2)" vmbr0[vmbr0 Bridge
192.168.1.127/24] vmbr1[vmbr1 Internal
VLAN-aware] + vmbr2[vmbr2 Bridge
eno2 → TL-SG105PE] + + subgraph "dCCTV - 10.0.30.0/24
ADR-0017" + Camera[vermont-garage
10.0.30.70] + end subgraph "VLAN 10 - Management
10.0.10.0/24" Proxmox[Proxmox Host
10.0.10.1] @@ -71,6 +76,9 @@ graph TB vmbr1 -.VLAN 20.- Tech vmbr1 -.VLAN 20.- Master vmbr1 -.VLAN 20.- Node1 + vmbr2 -.physical link.- eno2 + vmbr2 -.untagged.- Camera + vmbr2 -.pfSense net3 = dCCTV 10.0.30.1.- pfSense ``` ## Components @@ -81,6 +89,7 @@ graph TB | phpIPAM | v1.7.0 | phpipam.viktorbarzin.me | IP address management, device inventory, DNS sync | | vmbr0 | Linux bridge | 192.168.1.127/24 | Physical bridge on eno1, uplink to LAN | | vmbr1 | Linux bridge (VLAN-aware) | Internal | VLAN trunk for VM isolation | +| vmbr2 | Linux bridge | Physical (eno2) | dCCTV segment leg: eno2 → TL-SG105PE (rack) → cameras; pfSense net3 is the only L3 exit (ADR-0017) | | Technitium DNS | Container | 10.0.20.201 (LB) / 10.96.0.53 (ClusterIP) | Internal DNS (viktorbarzin.lan) + full recursive resolver | | Cloudflare DNS | SaaS | External | ~50 public domains under viktorbarzin.me | | Cloudflared | Container | K8s (3 replicas) | Tunnel ingress, replaces port forwarding | @@ -90,6 +99,22 @@ graph TB | MetalLB | v0.15.3 Helm chart | K8s | LoadBalancer IPs (10.0.20.200-10.0.20.220), all services on 10.0.20.200 | | Registry Cache | Container | 10.0.20.10 | Pull-through for docker.io:5000, ghcr.io:5010 | +## CCTV Segment (dCCTV) — as-built 2026-07-02 + +Isolated camera segment for owned cameras at the Sofia site (first: `vermont-garage`, HiLook IPC-T241H-C at the garage entrance). Decision + rejected alternatives: `docs/adr/0017-cctv-segment-dedicated-pfsense-leg.md`. + +**Physical path**: camera → TL-SG105PE PoE port (CCTV VLAN, port-based) → R730 `eno2` → `vmbr2` (bridge-ports eno2, not vlan-aware) → pfSense `net3`/vtnet3 = interface **dCCTV `10.0.30.1/24`**. Untagged end-to-end; the only 802.1Q is the internal port-VLAN table on the TL-SG105PE, which also keeps its home-LAN ports (uplink, 4G router `192.168.1.7`, UPS mgmt, switch mgmt `192.168.1.6`) in VLAN 1. + +**Addressing**: Kea DHCP pool `10.0.30.100-199`; devices get MAC reservations (camera `10.0.30.70`). Kea DDNS auto-registers names in Technitium; `phpipam-pfsense-import` picks up leases hourly. + +**Firewall** (all on pfSense): +- dCCTV in: pass `udp OPT4-net → 10.0.30.1:123` (NTP) — everything else hits the interface's default deny. Cameras cannot reach LAN, other segments, or the internet. +- WAN in (home LAN side): pass `192.168.1.8` (ha-sofia) → `10.0.30.70:80` (ISAPI/hikvision_next) and `:554` (RTSP), reply-to disabled on both. +- dKubernetes is allow-all, so cluster Frigate/go2rtc pulls RTSP with no extra rule (pod egress SNATs to node IPs). +- Home-LAN clients need the **AX6000 static route** `10.0.30.0/24 via 192.168.1.2` (camera-day step) to reach the camera UI. + +**Consumers**: cluster Frigate (`/srv/nfs/frigate/config/config.yml` — NOT Terraform) pulls `rtsp://10.0.30.70:554` main+sub as `vermont-garage`; HA integrates via Frigate plus direct hikvision_next for tamper events. + ## IPAM & DNS Auto-Registration Devices are automatically discovered, named, and registered in DNS without manual intervention. diff --git a/stacks/frigate/main.tf b/stacks/frigate/main.tf index 3b6284d9..332aba64 100644 --- a/stacks/frigate/main.tf +++ b/stacks/frigate/main.tf @@ -117,8 +117,9 @@ resource "kubernetes_deployment" "frigate" { limits = { memory = "10Gi" "nvidia.com/gpu" = "1" - # GPU VRAM budget (ADR-0016): detector + ffmpeg decode (~1.9 GiB). - "viktorbarzin.me/gpumem" = "2000" + # GPU VRAM budget (ADR-0016): detector + ffmpeg decode (~1.9 GiB), + # +~250 MiB NVDEC headroom for the vermont-garage camera (ADR-0017). + "viktorbarzin.me/gpumem" = "2300" } } env {