A privacy-first, full-tunnel architecture combining WireGuard, encrypted DNS, network IDS, and SIEM — deployed on a client/server model.
Full-tunnel VPN exit node with layered defense — IDS, SIEM, encrypted DNS, and blocklists.
Each layer has a specific responsibility. Defense in depth — no single point of failure.
Full-tunnel client/server. All traffic — IPv4 and IPv6 — encrypted and routed through the VPS. Client real IP never reaches the destination.
DNS-over-HTTPS server running on the VPS, served only to VPN clients. All DNS queries are encrypted inside the tunnel before leaving the VPS.
Passive packet inspection on WAN, VPN, and container interfaces. Custom rules detect SSH brute force, web shells, SQLi, path traversal, and recon.
Centralized log and event collection. Agent on client ships FIM alerts, vulnerability data, and auth events to the manager for correlation and alerting.
Host-based IDS running alongside Wazuh. Monitors system files, auth logs, and processes. Active response can ban IPs or kill processes on detection.
NAT masquerade for VPN forwarding. ipset blocklists updated daily from Feodo, Emerging Threats, and Tor exit feeds. Zero-downtime atomic swaps.
SSH brute-force protection. Monitors auth logs and inserts iptables DROP rules after N failed attempts. Automatically expires bans.
Exposes internal web services without opening any inbound ports. Traffic arrives encrypted from Cloudflare's edge — no public IP exposure of origin.
How traffic moves through each path in the stack.
CLIENT
│ AllowedIPs = 0.0.0.0/0, ::/0
│ ALL traffic enters tunnel
│
│──► UDP encrypted ──────────► VPS:51820
│ ChaCha20-Poly1305 │
│ PersistentKeepalive=25s │
│ ▼
│ iptables FORWARD (pos 1)
│ NAT POSTROUTING MASQUERADE
│ │
│◄── return traffic ──────────│
│ ▼
│ INTERNET
│
└── Your visible IP = VPS IP ✓
Real IP = hidden ✓Client app queries DNS
└─► OS stub resolver
└─► VPN_SERVER:53
(inside encrypted tunnel)
└─► dnscrypt-proxy
│
├─► 1.1.1.1 over HTTPS
└─► 9.9.9.9 over HTTPS
DNS never leaves tunnel unencrypted.
DNS server only reachable via VPN.
Verify:
dig google.com | grep SERVER
→ should show VPN server IPBrowser
└─► Cloudflare Edge
│ TLS termination
│ DDoS / bot filtering
│
│ Cloudflare Tunnel (QUIC)
│ No inbound ports open
▼
cloudflared daemon (client)
│
▼
Security Proxy (localhost)
│ Blocks: SQLi, SSRF,
│ path traversal, scanners
▼
Backend (localhost only)Network traffic
└─► Snort IDS (passive tap)
│ WAN + VPN + docker
│ Custom rules SID 9000001–8
└─► snort.alert.fast
└─► Wazuh / OSSEC
└─► Correlation
└─► Active response
iptables blocklists (daily update):
├─► Feodo C2 IPs
├─► ET compromised hosts
└─► Tor exit nodes
fail2ban: auth.log → >5 fails → DROPNon-obvious choices and the reasoning behind them.
Docker adds a REJECT rule to the iptables FORWARD chain. Using -A (append) places your WireGuard ACCEPT rule after Docker's REJECT — VPN traffic is silently dropped.
# Wrong — sits after Docker's REJECT iptables -A FORWARD -i wg0 -j ACCEPT # Correct — inserted at position 1, before Docker iptables -I FORWARD 1 -i wg0 -j ACCEPT
A full tunnel with only AllowedIPs = 0.0.0.0/0 still leaves IPv6 exposed. Assign a ULA prefix (fd00::/8) to the VPN interface and include ::/0 in AllowedIPs.
When two WireGuard tunnels claim overlapping subnets, the secondary tunnel's route blackholes traffic to the VPN server's internal IP.
IFACE=$(route -n get 8.8.8.8 | awk '/interface:/{print $2}')
route add -host VPN_INTERNAL_IP -interface "$IFACE"nginx ships a global ssl_protocols directive that includes TLS 1.0/1.1. Remove it at the global level.
ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers "ECDHE-ECDSA-AES128-GCM-SHA256:..."; ssl_prefer_server_ciphers off;
Cloud firewall rules may block port 22 from unknown source IPs. The VPN server's internal IP is only reachable through the tunnel — bypassing external firewall rules entirely.
Real issues encountered in production and how they were resolved.
AllowedIPs 0.0.0.0/0 routes all traffic into the tunnel. If the VPN server goes down, all traffic disappears. Fix: bring down the tunnel to restore the default gateway.
sudo wg-quick down /path/to/client.conf
Secondary tunnel claims the VPN subnet. SSH and DNS to the VPN server stop working. Fix: add a specific /32 host route through the primary tunnel interface.
Same routing bug causes DNS queries to VPN server to be blackholed. OS falls back to secondary public DNS — queries are unencrypted and outside the tunnel.
Snort detected an SSLv2 Client_Hello overflow attempt. Removed legacy protocols from global nginx config, enforced TLS 1.2/1.3 only, reloaded zero-downtime.
This architecture is implementation-agnostic. Swap components to suit your environment.
| Layer | This Stack | Alternatives |
|---|---|---|
| VPN | WireGuard | OpenVPN, IPSec/IKEv2, Tailscale, Nebula |
| Network IDS | Snort | Suricata, Zeek, Arkime |
| SIEM | Wazuh | Elastic SIEM, Splunk, Graylog |
| Host IDS | OSSEC | Tripwire, AIDE, Falco, Samhain |
| DNS | dnscrypt-proxy | Pi-hole + DoH, Unbound, AdGuard Home |
| Firewall | iptables + ipset | nftables, pf (BSD/macOS), ufw + ipset |
| Brute-force | fail2ban | CrowdSec, DenyHosts, SSHGuard |
| Reverse proxy | nginx | Caddy, HAProxy, Traefik, Envoy |
| Tunnel / Ingress | Cloudflare Tunnel | ngrok, frp, Tailscale Funnel, bore |
| Threat Intel | Feodo + ET + Tor | AbuseIPDB, Spamhaus, AlienVault OTX |