CCTV segment (dCCTV 10.0.30.0/24) on a dedicated pfSense leg for the garage camera
All checks were successful
ci/woodpecker/push/default Pipeline was successful

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 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-07-02 20:01:45 +00:00
parent 3a5194c9d4
commit 248e186dce
4 changed files with 85 additions and 5 deletions

View file

@ -118,6 +118,14 @@ _Avoid_: "external", "outside".
`viktorbarzin.lan`, served by Technitium DNS. Resolves only inside the homelab network. `viktorbarzin.lan`, served by Technitium DNS. Resolves only inside the homelab network.
_Avoid_: bare "lan", "private", "intranet". _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**: **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). 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. _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.

View file

@ -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.

View file

@ -1,10 +1,10 @@
# Networking Architecture # 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 ## 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 ## Architecture Diagram
@ -24,9 +24,14 @@ graph TB
CSdrop[CrowdSec drop<br/>nftables / CF edge<br/>out-of-band, pre-Traefik] CSdrop[CrowdSec drop<br/>nftables / CF edge<br/>out-of-band, pre-Traefik]
subgraph "Proxmox Host (eno1)" subgraph "Proxmox Host (eno1, eno2)"
vmbr0[vmbr0 Bridge<br/>192.168.1.127/24] vmbr0[vmbr0 Bridge<br/>192.168.1.127/24]
vmbr1[vmbr1 Internal<br/>VLAN-aware] vmbr1[vmbr1 Internal<br/>VLAN-aware]
vmbr2[vmbr2 Bridge<br/>eno2 → TL-SG105PE]
subgraph "dCCTV - 10.0.30.0/24<br/>ADR-0017"
Camera[vermont-garage<br/>10.0.30.70]
end
subgraph "VLAN 10 - Management<br/>10.0.10.0/24" subgraph "VLAN 10 - Management<br/>10.0.10.0/24"
Proxmox[Proxmox Host<br/>10.0.10.1] Proxmox[Proxmox Host<br/>10.0.10.1]
@ -71,6 +76,9 @@ graph TB
vmbr1 -.VLAN 20.- Tech vmbr1 -.VLAN 20.- Tech
vmbr1 -.VLAN 20.- Master vmbr1 -.VLAN 20.- Master
vmbr1 -.VLAN 20.- Node1 vmbr1 -.VLAN 20.- Node1
vmbr2 -.physical link.- eno2
vmbr2 -.untagged.- Camera
vmbr2 -.pfSense net3 = dCCTV 10.0.30.1.- pfSense
``` ```
## Components ## Components
@ -81,6 +89,7 @@ graph TB
| phpIPAM | v1.7.0 | phpipam.viktorbarzin.me | IP address management, device inventory, DNS sync | | 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 | | 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 | | 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 | | 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 | | Cloudflare DNS | SaaS | External | ~50 public domains under viktorbarzin.me |
| Cloudflared | Container | K8s (3 replicas) | Tunnel ingress, replaces port forwarding | | 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 | | 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 | | 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 ## IPAM & DNS Auto-Registration
Devices are automatically discovered, named, and registered in DNS without manual intervention. Devices are automatically discovered, named, and registered in DNS without manual intervention.

View file

@ -117,8 +117,9 @@ resource "kubernetes_deployment" "frigate" {
limits = { limits = {
memory = "10Gi" memory = "10Gi"
"nvidia.com/gpu" = "1" "nvidia.com/gpu" = "1"
# GPU VRAM budget (ADR-0016): detector + ffmpeg decode (~1.9 GiB). # GPU VRAM budget (ADR-0016): detector + ffmpeg decode (~1.9 GiB),
"viktorbarzin.me/gpumem" = "2000" # +~250 MiB NVDEC headroom for the vermont-garage camera (ADR-0017).
"viktorbarzin.me/gpumem" = "2300"
} }
} }
env { env {