An ultra-lightweight, zero-stress edge gateway for IP security cameras.
Have you ever tried displaying snapshots from your IP cameras on a Home Assistant wall tablet, MagicMirror, or web dashboard—only to have the camera freeze, disconnect, or crash under the connection load? Or spent hours wrestling with complex ONVIF SOAP protocols and RTSP credentials just to pan a camera or receive a motion alert?
cam-proxy solves this. It acts as an intelligent, featherweight (< 20MB RAM) buffer between your cameras and your smart home or custom applications. Instead of running heavy 24/7 video decoders or exposing fragile camera hardware to multiple concurrent clients, cam-proxy gives you simple HTTP snapshot URLs, clean REST PTZ controls, and automatic MQTT motion alerts—without burning CPU or overloading your cameras.
- Camera Freezes & RTSP Socket Flooding: Budget IP cameras (Tapo, Reolink, Hikvision, Dahua, Amcrest) have limited microcontrollers. When a dashboard refreshes, multiple tablets load simultaneously, or an automation requests snapshots, the camera's RTSP connection crashes.
cam-proxyuses single-flight request coalescing and an in-memory TTL frame cache: 20 simultaneous snapshot requests hit the camera as a single connection. - Resource Exhaustion on Low-Power Edge Hardware: Running a full NVR (like Frigate or Blue Iris) requires gigabytes of RAM and constant CPU/GPU decoding just to grab periodic pictures.
cam-proxyidles at < 15MB RAM and 0% CPU, connecting to the camera only when a snapshot or PTZ action is requested. - Complex Protocols (ONVIF SOAP & Digest Auth): Controlling a PTZ camera or pulling an ONVIF snapshot normally requires crafting complex XML SOAP envelopes with WS-Security timestamps and HTTP Digest handshakes.
cam-proxytranslates all of this into standard, browser-friendly JSON REST endpoints:GET /snapshotandPOST /ptz. - Cloud-Free Camera Alerts in Home Assistant: Subscribes directly to local camera event queues (motion detection, tampering, line crossing) and publishes them to MQTT with Home Assistant MQTT Auto-Discovery—no cloud subscriptions, vendor apps, or flaky webhooks required.
- Web Dashboard & Browser Security: Browsers cannot connect directly to camera RTSP streams or digest-authenticated HTTP endpoints due to CORS restrictions.
cam-proxyprovides configurable CORS middleware and standard security headers (X-Content-Type-Options: nosniff) out of the box.
If you already use tools like Frigate, go2rtc, or ffmpeg scripts, where does cam-proxy fit?
| Feature / Capability | cam-proxy |
Full NVRs (Frigate, Blue Iris, Shinobi) | Streaming Servers (go2rtc, MediaMTX) | DIY Scripts (ffmpeg / curl cron) |
|---|---|---|---|---|
| Primary Focus | Lightweight Snapshots, PTZ & MQTT Events | 24/7 Video Recording & AI Detection | Low-Latency Live Video Streaming | Ad-hoc Frame Grabbing |
| Idle Memory (RAM) | < 15–20 MB | 500 MB – 4+ GB | 50 MB – 200 MB | Transient (spikes per execution) |
| Idle CPU Usage | ~0% (no continuous decoding) | High (constant decoding/AI) | Low–Moderate | 0% until script triggers |
| Hardware Strain Protection | Yes (Single-flight coalescing + cache) | None (pulls continuous 24/7 stream) | None (passes connections through) | No (concurrent runs crash camera) |
| ONVIF PTZ REST API |
Yes (Simple JSON POST /ptz) |
Rare / Complex integration | Limited / None | Manual SOAP XML scripting |
| Camera Event Bridge |
Yes (ONVIF PullPoint |
Via internal events | RTSP metadata only | Not supported |
| Home Assistant Discovery | Zero-Config (MQTT Auto-Discovery) | Via custom integration | Via go2rtc integration | Manual YAML configuration |
| CORS & Web Browser Ready | Yes (Built-in CORS middleware) | Usually behind reverse proxy | WebRTC / WS focused | Requires reverse proxy |
| Dependency Footprint | Zero (Single static binary, CGO-free) | Heavy (Python, OpenCV, TensorRT) | Moderate | Requires ffmpeg, bash, curl
|
- You want snapshots on dashboards & smart displays: Wall tablets, Home Assistant Lovelace cards, MagicMirrors, e-ink picture frames, or smart watch notifications.
- You want camera events without cloud lock-in: Instant motion, tamper, and line-crossing alerts pushed to Home Assistant or Node-RED via MQTT.
- You run on low-power hardware: Raspberry Pi Zero/3/4/5, thin clients, NAS devices, or edge routers where every megabyte of RAM matters.
- You want to avoid camera crashes: Your camera drops offline when multiple devices request snapshots simultaneously.
- Continuous 24/7 video recording: If you need to store weeks of continuous video footage on hard drives, use a dedicated NVR like Frigate, Scrypted, or Blue Iris (you can still run
cam-proxyalongside them for lightweight dashboard snapshots!). - Low-latency live video streaming: If you want to stream 30fps live video in a browser over WebRTC, tools like
go2rtcorMediaMTXare purpose-built for video re-streaming.
Choose your preferred installation method:
Install directly from the official smford/homebrew-tap:
brew tap smford/tap
brew install cam-proxyInstall cam-proxy directly inside Home Assistant:
- In Home Assistant, navigate to Settings ➔ Add-ons ➔ Add-on Store.
- Click the top-right menu (⋮) ➔ Repositories ➔ Add repository:
https://github.com/smford/cam-proxy - Locate cam-proxy in the add-on store and click Install.
- Configure your cameras in the Configuration tab and click Start. (If the official Mosquitto broker add-on is installed, MQTT event forwarding and Home Assistant auto-discovery configure automatically!)
cam-proxy is optimized for edge hardware (Raspberry Pi 3/4/5 and Zero 2 W) running Raspberry Pi OS (64-bit arm64 or 32-bit armv7l), idling at < 15MB RAM and 0% CPU.
Download and install the latest linux/arm64 release directly onto your Raspberry Pi:
# 1. Fetch latest release archive for ARM64
LATEST=$(curl -s https://api.github.com/repos/smford/cam-proxy/releases/latest | grep "tag_name" | cut -d '"' -f 4)
curl -sSL "https://github.com/smford/cam-proxy/releases/download/${LATEST}/cam-proxy_${LATEST#v}_linux_arm64.tar.gz" | sudo tar -xz -C /usr/local/bin cam-proxy
# 2. Set up configuration directory and template
sudo mkdir -p /etc/cam-proxy
sudo curl -sSL https://raw.githubusercontent.com/smford/cam-proxy/main/config.example.yaml -o /etc/cam-proxy/cam-proxy.yaml
# 3. Discover local cameras on your network
cam-proxy --scan --generate-config | sudo tee -a /etc/cam-proxy/cam-proxy.yaml
# 4. Install and enable the hardened production systemd service
sudo curl -sSL https://raw.githubusercontent.com/smford/cam-proxy/main/systemd/cam-proxy.service -o /etc/systemd/system/cam-proxy.service
sudo systemctl daemon-reload
sudo systemctl enable --now cam-proxy
# 5. Verify service status
sudo systemctl status cam-proxyRun the official multi-arch container (linux/arm64):
docker run -d \
--name cam-proxy \
--restart unless-stopped \
--network host \
-v /etc/cam-proxy/cam-proxy.yaml:/etc/cam-proxy/cam-proxy.yaml:ro \
ghcr.io/smford/cam-proxy:latestIf developing on macOS or Linux x86_64, build the static ARM64 binary locally and deploy over SSH:
# Build static ARM64 binary locally
make build-linux-arm64
# Copy binary to Raspberry Pi
scp cam-proxy-linux-arm64 pi@raspberrypi.local:/tmp/cam-proxy
ssh pi@raspberrypi.local "sudo install -m 755 /tmp/cam-proxy /usr/local/bin/cam-proxy && rm /tmp/cam-proxy"Official multi-architecture images (linux/amd64, linux/arm64) with ffmpeg and ca-certificates pre-installed:
docker run -d \
--name cam-proxy \
--restart unless-stopped \
--network host \
-v $(pwd)/cam-proxy.yaml:/etc/cam-proxy/cam-proxy.yaml:ro \
ghcr.io/smford/cam-proxy:latestDownload pre-compiled, zero-dependency static binaries (linux/amd64, linux/arm64, darwin/amd64, darwin/arm64) with SHA256 checksums from the GitHub Releases page.
Requires Go 1.22+:
git clone https://github.com/smford/cam-proxy.git
cd cam-proxy
make build- Discover Cameras on Local Network:
./cam-proxy --scan --generate-config >> cam-proxy.yaml - Configure Credentials & Settings:
Edit
cam-proxy.yamlwith your camera usernames/passwords, or launch the interactive terminal manager:./cam-proxy --tui
- Start the Edge Daemon:
Live snapshots are immediately available at
./cam-proxy -config cam-proxy.yaml
http://localhost:8080/api/v1/cameras/<id>/snapshot!
cam-proxy is engineered as a zero-CGO edge gateway running on minimal resources (~10–15 MB RAM, ~0% idle CPU) while protecting camera hardware from socket flooding via single-flight request coalescing and an in-memory frame cache.
For the complete architectural breakdown, data flow diagrams, hardware protection lifecycle, and protocol engine internals, see docs/architecture.md.
cam-proxy can automatically scan your local network interfaces for ONVIF cameras (via WS-Discovery multicast 239.255.255.250:3702) and active RTSP video streams (port 554/8554 sweep):
# Scan local network
./cam-proxy --scan
# Scan with custom timeout
./cam-proxy --scan --scan-timeout 2s
# Scan and generate ready-to-use YAML configuration block
./cam-proxy --scan --generate-config >> cam-proxy.yamlExample Output:
Scanning local network for ONVIF and RTSP cameras (timeout: 3s)...
IP ADDRESS TYPE MANUFACTURER MODEL / NAME RTSP / ONVIF ENDPOINT
------------------------------------------------------------------------------------------------
192.168.1.12 ONVIF+RTSP - C216 rtsp://192.168.1.12:554/live
192.168.1.113 ONVIF - C720 http://192.168.1.113:2020/onvif...
------------------------------------------------------------------------------------------------
Discovered 2 camera(s) on local network.
Launch a full terminal user interface to browse detected cameras, inspect live details, configure credentials, test connections, and save changes:
# Launch interactive TUI
./cam-proxy --tui
# Or via make
make tui CAM-PROXY CAMERA MANAGER Config: cam-proxy.yaml
[1. Configured (2)] 2. Discovered (2) [Tab] switch view
CAMERA ID NAME ONVIF ADDRESS SNAPSHOT
────────────────────────────────────────────────────────────────────────────────
▶ front_door Front Porch Tapo C216 192.168.1.12:2020 auto
driveway Driveway PTZ 192.168.1.113:2020 auto
● Press 's' to scan network, 'a' to add, 'e' to edit, 'w' to save, 'q' to quit
[s] Scan LAN [a] Add [e/Enter] Edit [d] Delete [t] Test [w] Write File [q] Quit
[Tab]: Switch between Configured and Discovered cameras.[s]: Broadcast network WS-Discovery & RTSP sweep to find nearby cameras.[a]: Add camera (pre-populates with highlighted camera details when on Discovered tab).[e]/[Enter]: Edit highlighted camera credentials, endpoints, and event options.[d]: Delete selected camera from configuration.[t]: Test live camera connectivity and snapshot capture.[w]: Save/write updated configuration directly tocam-proxy.yaml.[q]/[Ctrl+C]: Quit TUI.
make buildCross-compile for edge gateways (e.g., Raspberry Pi 4/5):
make build-linux-arm64Auto-generate configuration from discovered cameras:
./cam-proxy --scan --generate-config > cam-proxy.yamlOr copy config.example.yaml:
cp config.example.yaml cam-proxy.yaml./cam-proxy -config cam-proxy.yaml -log-level debugAll endpoints are available over HTTP on port 8080 (or your configured port).
Inspect gateway health, process uptime, total camera count, and per-camera operational metrics (connection state online, last_seen timestamp, last_snapshot_latency_ms, and last_error).
- curl:
curl -s http://localhost:8080/healthz | jq # Or via REST status endpoint: curl -s http://localhost:8080/api/v1/status | jq
- wget:
wget -q -O- http://localhost:8080/healthz
- Response:
{ "status": "ok", "uptime": "15m42s", "camera_count": 2, "cameras": { "front_door": { "id": "front_door", "online": true, "last_seen": "2026-10-03T17:15:30Z", "last_snapshot_latency_ms": 45, "last_error": "" }, "backyard": { "id": "backyard", "online": false, "last_seen": null, "last_snapshot_latency_ms": 0, "last_error": "dial tcp 192.168.1.102:80: connect: connection refused" } } }
Retrieve metadata, ONVIF addresses, and configuration flags for all cameras.
- curl:
curl -s http://localhost:8080/api/v1/cameras | jq - wget:
wget -q -O- http://localhost:8080/api/v1/cameras
- Response:
[ { "id": "camera1", "name": "Front Porch C216", "address": "192.168.1.160:2020", "snapshot_method": "auto", "events_enabled": false }, { "id": "tort1", "name": "Tortoise Terrarium", "address": "192.168.1.12:2020", "snapshot_method": "auto", "events_enabled": false } ]
Grabs a fresh keyframe on demand and serves it directly as a standard binary JPEG (image/jpeg).
-
curl:
# Download to snapshot.jpg curl -s -o snapshot.jpg http://localhost:8080/api/v1/cameras/camera1/snapshot # Save with timestamp in filename curl -s -o "camera1_$(date +%Y%m%d_%H%M%S).jpg" http://localhost:8080/api/v1/cameras/camera1/snapshot
-
wget:
# Download to snapshot.jpg wget -O snapshot.jpg http://localhost:8080/api/v1/cameras/camera1/snapshot # Quiet download with timestamp in filename wget -q -O "camera1_$(date +%Y%m%d_%H%M%S).jpg" http://localhost:8080/api/v1/cameras/camera1/snapshot
Streams an ongoing sequence of JPEG snapshots using standard HTTP multipart streaming (multipart/x-mixed-replace). Works natively in web browsers via simple <img> tags and integrates effortlessly with Home Assistant generic camera entities:
- HTML
<img>Element:<img src="http://localhost:8080/api/v1/cameras/driveway/mjpeg?fps=5" alt="Live Driveway Stream" />
- Custom Framerate via
?fps=(default: 2 fps, max: 30 fps):curl -N http://localhost:8080/api/v1/cameras/driveway/mjpeg?fps=5
Send continuous velocity move vectors, halts, or preset recalls to motorized ONVIF Profile S cameras.
Velocity coordinates range from -1.0 to 1.0:
pan:-1.0(max speed left) to1.0(max speed right)tilt:-1.0(max speed down) to1.0(max speed up)zoom:-1.0(zoom out) to1.0(zoom in)
Move Left and Up:
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"move","pan":-0.5,"tilt":0.5}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"move","pan":-0.5,"tilt":0.5}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Pan Right:
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"move","pan":0.5,"tilt":0.0}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"move","pan":0.5,"tilt":0.0}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Tilt Down:
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"move","pan":0.0,"tilt":-0.5}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"move","pan":0.0,"tilt":-0.5}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Zoom In:
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"move","zoom":0.5}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"move","zoom":0.5}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Halts ongoing pan, tilt, or zoom motors immediately.
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"stop"}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"stop"}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Moves the camera to a saved ONVIF viewpoint / preset token.
- curl:
curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"preset","preset_token":"1"}'
- wget:
wget --header="Content-Type: application/json" \ --post-data='{"action":"preset","preset_token":"1"}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
Expose production metrics for Prometheus scraping (snapshot counts by source and status, latency histogram, camera online status gauge, and MQTT event counters):
- curl:
curl -s http://localhost:8080/metrics
- wget:
wget -q -O- http://localhost:8080/metrics
- Key Metrics Exposed:
cam_proxy_snapshots_total{camera_id, status, source}: Counters of snapshot requests served fromcachevs fetched fromhardware, withsuccessorerrorstatus.cam_proxy_snapshot_duration_seconds: Histogram measuring snapshot fetch latency from camera hardware.cam_proxy_camera_online{camera_id}: Gauge tracking camera reachability (1 = online, 0 = offline).cam_proxy_mqtt_events_total{camera_id, status}: Counters of ONVIF PullPoint events processed for MQTT (publishedorerror).
Retrieve the complete machine-readable OpenAPI specification for client generation, API testing, or importing into Swagger UI, Postman, or OWASP ZAP:
- curl:
curl -s http://localhost:8080/openapi.yaml
- wget:
wget -q -O- http://localhost:8080/openapi.yaml
- Spec file is also accessible directly in the repository at
api/openapi.yaml.
The HTTP API includes built-in security and CORS middleware out of the box:
- Configurable CORS Support: Enables web frontends, Home Assistant Lovelace cards, and browser extensions to fetch dynamic snapshots (
GET) and issue PTZ control commands (POST) directly from browser contexts.- Automatically handles preflight
OPTIONSrequests with204 No Content. - Configurable allowed origins (
*by default or custom origin list), methods, allowed headers, credentials, and preflight max age.
- Automatically handles preflight
- MIME Sniffing Protection: Sets
X-Content-Type-Options: nosniffacross all API responses. - Dynamic Image Cache Prevention: Enforces
Cache-Control: no-cache, no-store, must-revalidate,Pragma: no-cache, andExpires: 0headers on all camera snapshot endpoints to prevent stale image caching in client browsers and intermediate forward proxies.
-
With curl:
# Start moving right curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"move","pan":0.5}' # Wait 1 second sleep 1 # Stop moving curl -s -X POST http://localhost:8080/api/v1/cameras/camera1/ptz \ -H "Content-Type: application/json" \ -d '{"action":"stop"}'
-
With wget:
# Start moving right wget --header="Content-Type: application/json" \ --post-data='{"action":"move","pan":0.5}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz # Wait 1 second sleep 1 # Stop moving wget --header="Content-Type: application/json" \ --post-data='{"action":"stop"}' \ -q -O- http://localhost:8080/api/v1/cameras/camera1/ptz
while true; do
curl -s -o "frame_$(date +%Y%m%d_%H%M%S).jpg" http://localhost:8080/api/v1/cameras/camera1/snapshot
sleep 10
doneWhen pull_events: true is enabled on a camera, cam-proxy subscribes to the camera's ONVIF PullPoint notification queue. Detected alerts are normalized and published to:
<topic_prefix>/<camera_id>/<event_type> (e.g. cam-proxy/events/front_door/motion)
Payload format:
{
"camera_id": "front_door",
"type": "motion",
"state": true,
"raw_topic": "tns1:RuleEngine/CellMotionDetector/Motion",
"timestamp": "2026-10-01T22:45:00.123Z"
}When MQTT and discovery are enabled (discovery: true, enabled by default), cam-proxy automatically publishes Home Assistant MQTT discovery payloads on startup for each camera with pull_events: true:
- Discovery Topics:
<discovery_prefix>/binary_sensor/cam_proxy_<camera_id>_<event_type>/config(e.g.,homeassistant/binary_sensor/cam_proxy_front_door_motion/config) - Automatic Binary Sensors:
- Motion Detection (
motion):device_class: "motion" - Tamper Alert (
tamper):device_class: "tamper" - Line Crossing (
line_cross):device_class: "motion",icon: "mdi:vector-line"
- Motion Detection (
- Device Linking & Topology: Entities are grouped under their respective camera device cards with hardware metadata (
manufacturer,model) and linked tocam-proxyviavia_device: "cam-proxy". - Zero Configuration: Discovery messages are published with
retained: trueand QoS 1, ensuring Home Assistant automatically discovers or restores sensor states upon startup or broker reconnection.
cam-proxy is engineered as a primary companion gateway for Home Assistant. It offloads snapshot polling, request coalescing, and ONVIF event stream parsing so your Lovelace dashboards remain instant (<100ms) without locking up camera SOCs.
For full architectural details, automations, and advanced dashboard cards, see docs/home-assistant.md.
If you use Home Assistant OS or Supervised:
- Go to Settings ➔ Add-ons ➔ Add-on Store.
- Click Repositories (top-right menu ⋮) and add:
https://github.com/smford/cam-proxy - Click cam-proxy ➔ Install ➔ configure your cameras in the add-on UI ➔ Start. (If the Mosquitto broker add-on is installed, MQTT event forwarding and sensor discovery configure automatically!)
Add each camera via Settings ➔ Devices & Services ➔ Add Integration ➔ Generic Camera:
- Still Image URL:
http://<cam-proxy-host>:8080/api/v1/cameras/<id>/snapshot - Stream Source:
rtsp://<camera-ip>:554/live/ch0(orhttp://<cam-proxy-host>:8080/api/v1/cameras/<id>/stream.mjpg) - Framerate:
2(recommended for responsive previews without tablet lag)
Control pan, tilt, zoom, and presets from Lovelace cards by adding REST commands to configuration.yaml:
rest_command:
cam_ptz_move:
url: "http://<cam-proxy-host>:8080/api/v1/cameras/{{ camera_id }}/ptz"
method: POST
headers:
Content-Type: "application/json"
payload: >-
{
"action": "move",
"pan": {{ pan | default(0.0) }},
"tilt": {{ tilt | default(0.0) }},
"zoom": {{ zoom | default(0.0) }}
}
cam_ptz_stop:
url: "http://<cam-proxy-host>:8080/api/v1/cameras/{{ camera_id }}/ptz"
method: POST
headers:
Content-Type: "application/json"
payload: '{"action": "stop"}'
cam_ptz_preset:
url: "http://<cam-proxy-host>:8080/api/v1/cameras/{{ camera_id }}/ptz"
method: POST
headers:
Content-Type: "application/json"
payload: >-
{
"action": "preset",
"preset_token": "{{ preset_token }}"
}type: picture-glance
title: Front Door
camera_image: camera.front_door
entities:
- binary_sensor.front_door_motion
- binary_sensor.front_door_tamperFor production Linux installations, cam-proxy provides a hardened Systemd unit template at systemd/cam-proxy.service equipped with strict security sandboxing:
- Security Isolation:
DynamicUser=yesdynamically provisions an unprivileged, ephemeral service user and group. - Read-Only Filesystem:
ProtectSystem=strictandProtectHome=yeslock the filesystem down read-only, preventing unauthorized modifications. - Attack Surface Minimization:
CapabilityBoundingSet=drops all Linux root capabilities, whileMemoryDenyWriteExecute=yes,PrivateTmp=yes,PrivateDevices=yes, andProtectKernelTunables=yesblock privilege escalation vectors. - Automatic Recovery:
Restart=alwaysandRestartSec=5sguarantee immediate daemon recovery across camera or network faults.
Installation Steps:
# 1. Copy the compiled binary into standard path
sudo cp cam-proxy /usr/local/bin/
# 2. Set up the configuration directory
sudo mkdir -p /etc/cam-proxy
sudo cp config.example.yaml /etc/cam-proxy/cam-proxy.yaml
# 3. Install and activate the systemd unit
sudo cp systemd/cam-proxy.service /etc/systemd/system/
sudo systemctl daemon-reload
sudo systemctl enable --now cam-proxy
# 4. Check service status and logs
sudo systemctl status cam-proxy
journalctl -u cam-proxy -fA ready-to-use docker-compose.yml is included in the repository:
services:
cam-proxy:
image: ghcr.io/smford/cam-proxy:latest
container_name: cam-proxy
restart: unless-stopped
network_mode: host
volumes:
- ./cam-proxy.yaml:/etc/cam-proxy/cam-proxy.yaml:ro
environment:
- TZ=UTCStart the service:
docker compose up -dNote
Why network_mode: host?
Local camera discovery (--scan) relies on ONVIF WS-Discovery (multicast UDP 239.255.255.250:3702). Docker bridge networks do not forward UDP multicast broadcasts across the host's physical network adapter. If you do not need auto-discovery and configure cameras directly by IP, you can use standard port mapping instead (-p 8080:8080).
# Using Make
make docker-build
# Or using Docker directly
docker build -t cam-proxy:latest .Releases follow standard Semantic Versioning (vMAJOR.MINOR.PATCH).
Use the built-in Makefile targets to automatically tag and initiate GitHub releases:
# Automatically bump patch and publish (e.g. v0.3.0 -> v0.3.1)
make release-patch
# Or bump minor and publish (e.g. v0.3.0 -> v0.4.0)
make release-minor
# Or release an explicit version tag
make release TAG=v0.4.0cam-proxy incorporates a dual-layer Static Application Security Testing (SAST) strategy automated via .github/workflows/sast.yml running on every push to main, every pull request, and a weekly scheduled scan (Mondays at 07:00 UTC):
| SAST Tool | Role | Strengths | Trigger / Output |
|---|---|---|---|
| GitHub CodeQL | Semantic Taint Analysis | Deep data-flow and inter-procedural taint analysis across Go packages (CWEs, untrusted inputs reaching network/exec sinks) | Analyzes Go source with security-extended query suite; outputs SARIF to GitHub Code Scanning |
| Semgrep OSS | Rapid Pattern Checks & Linting | Fast AST-based pattern matching and rule enforcement across Go source, Dockerfile, and GitHub Actions workflows | Runs semgrep scan --config auto; generates SARIF and uploads alerts to GitHub Code Scanning |
Run Semgrep OSS locally using the Makefile:
make sastOr directly using uvx:
uvx semgrep scan --config autocam-proxy runs automated Dynamic Application Security Testing (DAST) via .github/workflows/dast.yml on push to main, pull requests, weekly scheduled runs (Mondays at 08:00 UTC), or manual workflow_dispatch:
-
Service Container Spin-Up:
- Compiles and packages
cam-proxyinto a test container image (cam-proxy:dast). - Launches
cam-proxyas a background service container mounted with a lightweight test configuration (.zap/dast-config.yaml). - Polls
GET /healthzuntil the HTTP daemon is fully initialized and reporting healthy status.
- Compiles and packages
-
OWASP ZAP Baseline Scan:
- Executes the official
zaproxy/action-baselinecontainer targeting the runningcam-proxydaemon athttp://localhost:8080. - Actively spiders API endpoints (
/healthz,/api/v1/cameras,/api/v1/cameras/{id}/snapshot,/api/v1/cameras/{id}/ptz) and passively evaluates HTTP responses against OWASP Top 10 vulnerabilities. - Applies API-tailored rule overrides defined in
.zap/rules.tsv. - Generates comprehensive HTML and Markdown security audit reports stored as GitHub Actions artifacts (
zap-baseline-report).
- Executes the official
Spin up cam-proxy and run the OWASP ZAP Baseline scan locally:
make dast