Secure Proxy Manager

September 9, 2026 Β· View on GitHub

Release CI License: MIT Images: GHCR Β· cosign-signed Docs WAF: 21 categories Β· FN=0 gated

πŸ“– Documentation: https://fabriziosalmi.github.io/secure-proxy-manager/

A self-hosted Secure Web Gateway (SWG) β€” one controllable egress point for a home, lab, or small-office network. It combines a Squid forward proxy, a custom WAF (request/response inspection over ICAP), a DNS sinkhole, and a modern web UI + API into a single Docker Compose stack: the self-hosted counterpart to cloud SWGs like Zscaler / Cloudflare Gateway β€” with no traffic leaving your network.

Block domains and IPs, enforce a default-deny egress allowlist, inspect requests against WAF rules, sinkhole malware/ad domains at the DNS layer, and see what every client is reaching.

Malicious requests blocked by the proxy + WAF, benign traffic allowed

Malicious requests get 403'd at the proxy by the WAF; benign traffic passes. Every category is verified with an adversarial test suite β€” see Tested like an attacker.

Dashboard

How it compares

Most self-hosted network tools cover one layer. SPM combines forward-proxy egress control, request inspection, and DNS sinkholing in a single stack:

CapabilityPi-hole / AdGuardNginx Proxy ManagerCloud SWG (Zscaler…)Secure Proxy Manager
DNS sinkhole (block at resolve time)βœ…β€”βœ…βœ…
Forward proxy (outbound egress control)β€”β€”βœ…βœ…
HTTP request/body inspection (WAF)β€”β€”βœ…βœ…
Default-deny egress allowlistβ€”β€”βœ…βœ…
Reverse proxy / ingressβ€”βœ…β€”β€”
Self-hosted β€” traffic stays on your networkβœ…βœ…β€”βœ…
Free / no per-seat costβœ…βœ…β€”βœ…

Pi-hole/AdGuard block only at the DNS layer; Nginx Proxy Manager is reverse-proxy ingress; cloud SWGs do all of this but as a paid SaaS your traffic flows through. SPM is the self-hosted outbound counterpart β€” one controllable egress point with WAF inspection and DNS sinkholing combined, on your own metal.

What it is

The stack is five containers (plus an optional sixth):

ServiceTechRole
webReact 19 + Vite, served by nginxThe management UI and HTTPS entry point. Terminates TLS, proxies the API and WebSocket.
backendGo (chi, modernc/sqlite, JWT)REST API, log ingestion, settings, auth, background workers. ~20 MB RAM.
wafGo ICAP serviceInspects proxied requests/responses Squid hands off over ICAP.
proxySquid (Ubuntu 22.04, squid-openssl)The actual forward proxy on port 3128, with ICAP wired to the WAF.
dnsdnsmasqDNS resolver that sinkholes blacklisted domains at the network layer.
tailscaleTailscale (optional)Sidecar for remote access over a private network. Enabled with a compose profile.

The backend applies blacklist and config changes by writing to the shared ./config volume: it exports the lists as flat files and touches a trigger (.reload-squid, .reload-dns, .clear-cache). Each container's own watchdog notices the change and reloads in place β€” no Docker socket is mounted into the backend, and nothing calls the Docker Engine API.

Quick start

Requirements: Docker 20.10+ with Compose v2, ~512 MB RAM, ~2 GB disk. Runs on x86_64 and ARM64.

One command (fresh VPS or server):

curl -fsSL https://raw.githubusercontent.com/fabriziosalmi/secure-proxy-manager/main/deploy/install.sh | sudo bash

The installer checks for Docker, generates random admin credentials, pulls the pre-built signed images from GHCR, and starts the stack. It prints the credentials at the end (they are also saved to .env).

Manual:

git clone https://github.com/fabriziosalmi/secure-proxy-manager.git
cd secure-proxy-manager
cp .env.example .env
# Set a strong BASIC_AUTH_PASSWORD in .env (the backend refuses to start with an
# empty, common, or <8-char password). Leave SECRET_KEY empty to auto-generate.
docker compose up -d --build

Then open https://localhost:8443 and accept the self-signed certificate. Log in with the credentials from your .env (or the ones the installer printed). On first login a short wizard helps you pick a starting configuration.

To send traffic through the proxy, point a client at http://<host>:3128 (Settings > Client Setup generates per-OS instructions and a PAC file).

Ports

PortServiceNotes
443, 8443Web UI (HTTPS)Main interface. 8443 is the same UI on an alternate port.
80, 8011HTTPRedirects to HTTPS; also serves ACME challenges for Let's Encrypt.
3128ProxyPoint clients here. Only RFC1918/localhost sources are allowed by default.
5001Backend APIBound to 127.0.0.1 on the host; the UI proxies it internally.

These have to be free on the host before you start. A web server already on port 80 is the usual clash, and it is the one case that fails quietly: the container cannot bind, and the stack looks like it is merely slow to come up. install.sh checks every published port first and names the process holding it. Free it (systemctl stop nginx), or change the host side of the mapping in docker-compose.yml, or set SKIP_PORT_CHECK=1 if you know the conflict is harmless.

Configuration

All configuration is environment-driven via .env. .env.example documents the variables an operator normally sets; the complete set the backend reads is in backend-go/internal/config/config.go. The essentials:

VariableDefaultPurpose
BASIC_AUTH_USERNAME / BASIC_AUTH_PASSWORDrequiredAdmin login. The backend refuses to start if the password is empty, a common default, or shorter than 8 characters.
SECRET_KEYauto-generatedJWT signing secret. Leave empty to generate and persist one. If set, it must be unique, random, and 32+ chars; known/example values are rejected at startup.
LETSENCRYPT_DOMAIN / LETSENCRYPT_EMAILemptySet both to obtain a real certificate via certbot instead of the self-signed one.
CORS_ALLOWED_ORIGINShttps://localhost:8443Allowed UI origin(s).
DNS_UPSTREAM_1..3malware-blocking resolversUpstream DNS for dnsmasq. Override to use your own (e.g. Pi-hole).
PROXY_IPemptyYour LAN IP, to enable WPAD auto-discovery (wpad.dat) for browsers.
WAF_BLOCK_THRESHOLD10Anomaly score at which a request is blocked (lower is stricter).
WAF_FAIL_OPEN0If the WAF handler errors, block (0, fail-closed) or allow (1) the request.

Most runtime behaviour (WAF categories, blocklists, filtering toggles, notifications) is managed from the UI and stored in the database, not in .env.

Capacity

Single node by design: the login-attempt counters and the JWT revocation list are in-process, the state lives on bind mounts, and each container generates its own configuration β€” so a second instance would not share what matters. Running more of them is not a supported way to grow. These are the ceilings the design imposes and what happens at each:

CeilingValueAt the limit
Write throughputone SQLite writer, pool of 4Writes queue (busy_timeout 5s), so it shows as latency, not errors
WAF behavioural tracking10,000 client IPsBeyond it an entry is evicted, so beaconing/sharding history resets for some clients
Live log stream256 queued messages, 64 per viewerLines are dropped for a slow viewer rather than stalling the stream
WAF security alerts64 queued, 8 delivery workersAlerts are dropped and counted in notifyDropped on /metrics. Blocks are still enforced and logged β€” only the notification is lost, and the queue fills exactly during an attack
API rate limit20 req/s per IP, burst 60429
Concurrent DNS scans2429
Request body55 MB; blacklist import 32 MB413 / the import is refused
Log ingest5,000 lines per 500 ms tickA backlog catches up over successive ticks
Backend resources0.25 CPU, 128 MBRaise deploy.resources.limits in the compose file

proxy_logs grows with traffic and is pruned only by log_retention_days (default 30). If that setting is not a positive number the pruning stops and spm_log_retention_enabled goes to 0.

Features

Filtering

  • Domain and IP blacklists and whitelists, managed in the UI or via the API, importable from URLs or pasted content (with CIDR support for IPs).
  • DNS sinkholing of blacklisted domains via dnsmasq, so blocked lookups never reach the proxy at all.
  • Direct-IP-access blocking and a method whitelist in Squid.
  • Optional default-deny egress (off by default): instead of the deny-list blacklists, allow outbound traffic only to an explicit CIDR/domain allowlist and refuse everything else. Toggled in Settings; destinations managed on the Egress Allowlist page.

WAF

  • 170 regex rules across 21 toggleable categories (SQLi, XSS, traversal, C2, data-loss, etc.), each enableable/disableable at runtime.
  • 7 behavioural heuristics (entropy, beaconing, PII, sharding, morphing, ghosting, sequence) plus DGA detection (bigram + entropy), typosquatting detection (edit distance + homoglyphs), and a safe-URL fast-path cache.
  • Anti-evasion input normalization and anomaly scoring; fails closed on internal errors by default.

HTTPS inspection (optional)

  • HTTPS is tunnelled via CONNECT by default - the WAF sees connection metadata (host, port), not request contents.
  • Enabling SSL-bump in Settings makes Squid intercept TLS with a locally generated CA so the WAF can inspect decrypted requests. This requires installing the generated CA (Settings > download CA) on every client that should trust it. The CA is generated per deployment at first boot.

Operations

  • Live dashboard, per-client drill-down, searchable access logs, and an audit log of configuration changes.
  • Prometheus metrics on the internal network: the backend at :5000/metrics (RED metrics per route, DB connection-pool gauges, worker heartbeats, Go runtime) and the WAF at :8080/metrics (counters plus a REQMOD latency histogram). An opt-in observability Compose profile ships Prometheus + Grafana: docker compose --profile observability up -d.
  • Health probes: /livez (process up) and /readyz (database reachable β€” the container healthcheck uses this so a wedged DB surfaces as unhealthy). Structured per-request access logs on the backend.
  • Notifications to a custom webhook, Gotify, Telegram, or Microsoft Teams, with retry and backoff.
  • WebSocket log streaming, JWT auth with a persistent revocation list, per-IP rate limiting, and AES-256-GCM encryption of sensitive settings at rest.
  • Optional Let's Encrypt, WPAD/PAC client auto-config, and a Tailscale sidecar.

Using it

Point a client at the proxy. Set the HTTP/HTTPS proxy to http://<host>:3128 on the device, or use the PAC file / WPAD auto-discovery from Settings > Client Setup. Only RFC1918 and localhost sources are permitted by default.

Block a domain. Blacklists > Domains > add example.com. The change is exported to Squid and dnsmasq within a couple of seconds. Whitelists take precedence.

Inspect HTTPS. Settings > enable SSL inspection, download the generated CA, and install it as trusted on your clients. Without this, HTTPS requests are only filtered by host/IP and DNS, not by WAF body rules.

Lock egress to an allowlist. Settings > enable default-deny egress, then add the approved IPs/CIDRs and domains on the Egress Allowlist page. Local clients can then reach only those destinations; everything else is refused.

Import a blocklist. Blacklists > Import supports popular public lists by URL or pasted content. Imports are size-bounded and fetched with an SSRF-safe client that refuses private/internal targets.

Updating

cd secure-proxy-manager
git pull
docker compose up -d --build

Your .env, database, and blacklists live in bind-mounted volumes and are preserved across updates. The backend checks GitHub for newer releases and shows a badge in the UI when one is available.

Backup and restore

The database (data/), config (config/), and .env hold all state.

docker compose down
cp data/proxy_manager.db proxy_manager.db.bak
tar czf config.bak.tgz config/ .env
docker compose up -d

The UI also offers a config export/import under Settings, and the database can be exported via the API.

Health and testing

# Service health
curl -skI https://localhost:8443/            # UI
curl -I http://127.0.0.1:5001/health         # backend API (localhost only)

# Proxy a request
curl -x http://localhost:3128 -I http://example.com

# End-to-end suite (service health, proxy egress, blocking, log pipeline)
bash tests/ci-e2e.sh

Go unit tests: cd backend-go && go test ./... and cd waf-go && go test ./.... UI tests: cd ui && npm test. A local pre-commit check that runs TypeScript, ESLint, Go vet/test, and the UI build is at scripts/pre-commit-validate.sh.

Troubleshooting

  • Backend container won't start - check docker compose logs backend. The most common cause is an empty/weak BASIC_AUTH_PASSWORD or a known/short SECRET_KEY; both are rejected by design.
  • Can't reach the UI - it is HTTPS on 8443 with a self-signed cert; accept the certificate, or use https://localhost:8443 (not http://...:8011).
  • Clients aren't being filtered - confirm the device's proxy is set to :3128 and the source IP is RFC1918/localhost (other sources are denied).
  • HTTPS isn't inspected - that is the default; enable SSL-bump and install the CA to inspect request contents.

API

The backend exposes a REST API (77 routes) used by the UI. A machine-readable listing is available at GET /api/docs. Authenticate with HTTP Basic auth, or exchange credentials at POST /api/auth/login for a JWT and send it as a Bearer token.

Architecture and deployment docs

Tested like an attacker

Most proxies and WAFs ship rules and hope. SPM has an adversarial e2e harness (make adversarial, gated in CI) that drives real attack traffic through the running proxy + WAF in an isolated sandbox and fails the build on any regression. Five planes:

  • block-matrix β€” SQLi / XSS / RCE / SSRF / traversal / secret-exfil / … across the 21 WAF categories: malicious β†’ 403, benign β†’ allowed. Gate: false negatives = 0, false positives = 0.
  • API attacker β€” auth-bypass, forged / tampered JWTs (incl. alg=none), login SQL-injection, rate-limit probing β†’ no bypass (401/403/429 as expected).
  • bench / latency β€” p50/p95 + proxy + ICAP overhead, with a regression gate.
  • config-matrix β€” flips a setting through the real API and proves the data plane changes (disable a rule category β†’ that attack now passes; re-enable β†’ blocked again, selectively).
  • resilience β€” kill the WAF and the proxy fails closed (no fail-open hole); restart it and the stack self-heals.

Run it yourself β€” it brings up the sandbox, attacks it, prints a block-matrix + FP/FN report, and tears down:

make adversarial

See tests/adversarial/.

Manage it from an AI agent (MCP)

SPM ships an optional Model Context Protocol server (mcp/) that exposes the management API as tools, so an assistant or agent (e.g. Claude) can drive your Secure Web Gateway in natural language β€” "what are my top egressing domains?", "is anything reaching AI APIs?", "block evil.example", "disable the SQL_INJECTION category for 5 minutes". Read tools are safe; mutating tools (block/toggle/reload) are explicit and approved per call.

SPM_URL=http://localhost:5001 SPM_USERNAME=admin SPM_PASSWORD=... uvx spm-mcp

See mcp/README.md for the tool list and Claude Desktop config.

Security notes

  • The SSL-bump CA is generated per deployment at first boot and never committed; treat the private key under config/ as sensitive and never share it.
  • Set strong admin credentials. The proxy only accepts RFC1918/localhost clients by default - if you expose port 3128 publicly, put it behind authentication or a private network (e.g. the Tailscale sidecar).
  • Sensitive settings (webhook URLs, tokens) are encrypted at rest; JWT secrets and the encryption key are generated and persisted under data/.
  • Squid is kept patched against upstream CVEs (e.g. Squidbleed / CVE-2026-47729) via a fail-closed version floor in the proxy image and a disabled FTP gateway β€” see Security Advisories.

Contributing

See CONTRIBUTING.md. Issues and pull requests are welcome.

Support

SPM is MIT-licensed and free to self-host. If it's useful to you:

  • ⭐ Star the repo and sponsor to support development.
  • πŸ› οΈ Want it installed, hardened, or managed for your network or organisation? Email fabrizio.salmi@gmail.com.

License

MIT.