Small office gateway
Three roles, three postures, three failure modes, one gateway. Engineering gets direct egress for speed; finance is locked behind a single VPN with a static exit IP; HR’s HRIS traffic goes through a bridge that’s invisible to SNI inspection. Every config change is in the audit log; every kill-switch trip pages the admin.
A small startup (~10 employees, two roles formalised) wants a network gateway that:
- Routes engineering traffic direct for speed.
- Routes finance traffic only through a kill-switched VPN tunnel so client-data never touches the open WAN.
- Routes HR traffic through a bridge so sensitive HRIS traffic isn’t even discoverable by IP/SNI inspection.
- Alerts the office admin on any policy violation, leak, or tunnel issue.
- Keeps a complete audit trail of every config change for the auditor.
Hardware & install
Section titled “Hardware & install”Either:
- A small fanless mini-PC (4-core Intel/AMD, 4 GB RAM, 64 GB SSD),
linux/amd64, install profilevps, or - A Raspberry Pi 4 (8 GB) in
primarymode if you want WiFi AP too.
We’ll assume the mini-PC, sitting between the ISP modem and a managed L2 switch with VLAN support.
sudo ./scripts/install-gateway.sh --profile vpsTopology
Section titled “Topology” ISP modem ── eth0 ── [mini-PC] ── eth1 ── managed switch ├── VLAN 10 engineering ├── VLAN 20 finance ├── VLAN 30 HR └── VLAN 40 guestsThe switch tags ports per role. The Gateway sees each VLAN as a separate L3 interface and applies different policies per source interface.
Tunnels and bridges
Section titled “Tunnels and bridges”wg-finance— single tunnel to a privacy-focused provider with verified no-logs policy. Static IP at the exit (the bookkeeping SaaS allowlists it).bridge-hr— bridge node on a VPS the company controls. Domains:hris.vendor.example,*.hris.vendor.example.
Privacy classes
Section titled “Privacy classes”POST each as a JSON body to https://gateway.lan:8080/api/v1/privacy-classes. The field shapes:
{ "name": "engineering", "force_dns": true, "ipv6_policy": "allow", "quic_policy": "allow", "block_webrtc": false, "allow_direct_fallback": true } // direct egress is expected
{ "name": "finance", "force_dns": true, "ipv6_policy": "block", "quic_policy": "block", "block_webrtc": true, "allow_direct_fallback": false } // fail-closed: never direct
{ "name": "hr", "force_dns": true, "ipv6_policy": "block", "quic_policy": "block", "block_webrtc": true, "allow_direct_fallback": false }System-wide under Settings → Privacy, turn on block_doh and set DNS mode to paranoid (drops TCP/853 DoT) so no client can sidestep the integrated resolver.
Policies
Section titled “Policies”Each role is one VLAN-keyed policy. Multiple source_type: vlan policies on the same gateway, distinguished by source_value (the VLAN ID):
# Engineering — direct egress, fail-opencurl -X POST https://gateway.lan:8080/api/v1/routing/policies \ -H "Authorization: Bearer $API_KEY" -d '{ "name":"engineering","priority":100,"enabled":true, "source_type":"vlan","source_value":"10", "hops":[{"type":"interface","target":"eth0"}], "fail_policy":"fail-open" }'
# Finance — strict VPN, fail-closed (kill-switch engaged on tunnel down)curl -X POST https://gateway.lan:8080/api/v1/routing/policies \ -H "Authorization: Bearer $API_KEY" -d '{ "name":"finance","priority":90,"enabled":true, "source_type":"vlan","source_value":"20", "hops":[{"type":"vpn","target":"wg-finance"}], "fail_policy":"fail-closed" }'
# HR's bridge-routed destinations are handled by bridge-hr automatically — the# bridge subsystem auto-creates per-domain routing policies at priority 200# pointing at the bridge's local SOCKS5 port. Just keep `hris.vendor.example`# and `*.hris.vendor.example` in the bridge's `domains` list.# Drop the rest of HR's traffic so it can't reach anything else from VLAN 30:curl -X POST https://gateway.lan:8080/api/v1/routing/policies \ -H "Authorization: Bearer $API_KEY" -d '{ "name":"hr-drop-other","priority":80,"enabled":true, "source_type":"vlan","source_value":"30", "hops":[{"type":"drop"}], "fail_policy":"fail-closed" }'
# Guests — direct egress, fail-open (no fancy posture)curl -X POST https://gateway.lan:8080/api/v1/routing/policies \ -H "Authorization: Bearer $API_KEY" -d '{ "name":"guests","priority":70,"enabled":true, "source_type":"vlan","source_value":"40", "hops":[{"type":"interface","target":"eth0"}], "fail_policy":"fail-open" }'
curl -X POST https://gateway.lan:8080/api/v1/routing/policies/apply \ -H "Authorization: Bearer $API_KEY"Alerts
Section titled “Alerts”The 17 built-in alert rules already cover everything this deployment cares about. Confirm the relevant ones are enabled and wire up the dispatch channels.
# Rules to leave enabled (default) for this deployment:# tunnel-down, killswitch-engaged, dns-bypass, direct-fallback,# drift-detected, backup_failed, leak-detected, cert-expiry-critical
# Dispatch destinations — single webhook + Telegram, applied to every firecurl -X PUT https://gateway.lan:8080/api/v1/settings \ -H "Authorization: Bearer $API_KEY" \ -H "Content-Type: application/json" \ -d '{ "webhook_url": "https://hooks.example.invalid/admin", "telegram_bot_token": "REDACTED", "telegram_chat_id": "@office-bot" }'
# Optional: disable a noisy rule the office doesn't care aboutcurl -X PUT https://gateway.lan:8080/api/v1/alerts/rules/new-device \ -H "Authorization: Bearer $API_KEY" \ -d '{"enabled": false}'Compliance & audit
Section titled “Compliance & audit”- Audit trail captures every config change with who, what, when, before/after, and source IP. Set
log.retention_days: 365in the bootstrap config to keep a year (default is 90). - Daily backup pushed via SCP to a separate server (sanitized plaintext by default — for an encrypted variant with secrets, use
POST /api/v1/backup/export-encryptedad-hoc). Restore is a singlegwctl backup import <file>away for plaintext; encrypted requires external decrypt first. leak-matrixsnapshot is part of the monthly compliance pack — proves what would have happened to traffic under any tunnel failure.
What happens when
Section titled “What happens when”| Scenario | Result |
|---|---|
| Engineer streams Netflix | Direct egress. Fast. No VPN overhead. |
| Finance opens bookkeeping SaaS | Traffic goes through wg-finance — the SaaS sees the tunnel’s static IP, matches its allowlist. |
wg-finance handshake stale |
Finance VLAN goes dark (fail-closed). Webhook fires within seconds. Admin gets paged. |
| HR opens HRIS | Traffic goes through bridge-hr over TLS to a decoy SNI. Observer sees only ordinary HTTPS to the decoy site. |
| New employee joins | Add device to the right VLAN port on the switch; policy applies automatically — no per-device config. |
| Auditor asks “what happens if VPN fails?” | Show them gwctl leak-matrix. The ✗ DROP cell in the finance row is the proof. |
Why this beats a generic firewall
Section titled “Why this beats a generic firewall”- Per-VLAN routing on a generic firewall (pfSense, OPNsense) requires manual nftables / pf rules per tunnel — The Gateway compiles them from policy objects.
- Generic firewalls don’t reference-count their kill-switch — restarting a tunnel can produce a brief window of leakage.
- Generic firewalls don’t have a structured audit log — you get syslog, you write the parser yourself.
- Generic firewalls don’t have a leak checker that proves your posture works.
Next steps
Section titled “Next steps”After the VLAN-keyed setup is live:
- Capture an audit baseline. Export the audit log monthly to a write-once destination so a compromised admin can’t quietly rewrite history. See Operations → Monthly.
- Schedule a quarterly restore test. A backup that doesn’t restore is just a file. See Backup → Restore.
- Tighten on-call. Decide which alerts should page vs notify so finance going dark wakes someone before clients notice. See On-call signals.
- Federate to a second site if you have one — the same VLAN→tunnel mapping applies anywhere with the same switch port topology. See Federation.
Related
Section titled “Related”- Routing policies —
source_interfacematching is what binds VLAN-tagged ports to roles. - Alerts & monitoring — webhook + Telegram setup for the compliance signals shown here.
- Backup — scheduled encrypted exports with remote SFTP push.