911b1f8c5d1f55b009a8cb101c1dd6e4f04f3a7f
RackNerd VPN Hub
Client VPN entry point + DNS forwarder for the AtlasHorizon AD environment. Roaming clients terminate here (OpenVPN + Duo MFA) and are routed over WireGuard to the two sites. Inter-site traffic stays on the existing direct Oswald↔Trochalakis tunnel — this box never carries it.
┌────────────── RackNerd VPS ──────────────┐
roaming client ══ovpn══▶ │ tun0 172.31.8.1 │
(172.31.8.x) │ openvpn ─▶ duo-authproxy ─▶ Duo cloud │
DNS/routes pushed │ (127.0.0.1:1812) └──LDAPS 636──┐ │
│ unbound 127.0.0.1 / .8.1 / .255.1 │ │
│ wg0 172.31.255.1 ─────────────────────┼──┤
└──────────┬──────────────────┬─────────┘ │
wg .255.2 wg .255.3 (via wg0)
Trochalakis 192.168.20/24 Oswald 192.168.7/24
AHDC01, AHDC03 ◀━━ direct s2s ━━▶ AH-OSWLDC
Address plan
| Net | Subnet | Notes |
|---|---|---|
| Trochalakis LAN | 192.168.20.0/24 |
AHDC01, AHDC03 |
| Oswald LAN | 192.168.7.0/24 |
AH-OSWLDC |
| WG transfer | 172.31.255.0/24 |
hub .1, Troch .2, Oswald .3 |
| OpenVPN clients | 172.31.8.0/24 |
routed (not NATed) to both sites |
Public endpoint: vpn.atlashorizon.net — only 22/tcp (SSH), 51820/udp
(WireGuard) and 1194/udp (OpenVPN) are open; DNS/RADIUS/mgmt bind to
loopback and tunnel IPs only.
Layout
- docker-compose.yml — openvpn, duo-authproxy, unbound (all
network_mode: host) - openvpn/ — server.conf, PAM→RADIUS glue, offline-CA workflow in pki/README.md
- duo-authproxy/ — built from Duo's source tarball; cfg rendered from
.env - unbound/ — conditional forwarder: AD zones → DCs over wg0, everything else public recursion
- host/ —
setup.sh(sysctl, iptables, wg keygen),wg0.conf.example, firewall template - client/ —
.ovpntemplate for end users - docs/ — SITE-SIDE.md (DC/firewall work at the sites), TESTPLAN.md
Deploy order
- Host:
sudo host/setup.sh— installs wireguard + iptables rules, generates the WG key, prints the public key for the sites. - Sites: do docs/SITE-SIDE.md (WG peers, return
routes, DC firewall, Duo app, LDAPS CA export). Paste site pubkeys into
/etc/wireguard/wg0.conf, thensystemctl enable --now wg-quick@wg0. - Secrets:
cp .env.example .env && chmod 600 .env, fill in DC IPs, Duo keys, service account,RADIUS_SECRET. - PKI: offline CA per openvpn/pki/README.md;
copy server cert/key +
ta.keyintoopenvpn/pki/. Drop the AD CA chain induo-authproxy/certs/ad-ldaps-ca.pem. - Stack:
docker compose up -d --build. - Verify: docs/TESTPLAN.md, stages 0→6.
Design notes / gotchas
- WireGuard is host-level (
wg-quick@wg0), not containerized — tun0 and wg0 share the host netns so the kernel routes between them with no Docker bridge/NAT tricks, and the tunnel survives Docker restarts. - Routed, not NATed: DCs see real
172.31.8.xclient IPs. Requires the site-side return routes in SITE-SIDE.md. Fallback masquerade snippet is commented at the bottom of host/rules.v4.template. - No hairpin: RackNerd's peer AllowedIPs only carry each site's own
LAN; the sites' RackNerd peer only carries
172.31.8.0/24+ the hub IP. - MTU: OpenVPN
tun-mtu 1400/mssfix 1360keeps client packets inside wg0's 1420 so nothing fragments on the middle leg. - Duo flow: password → push;
password,passcode→ offline fallback (allow_concat=true).reneg-sec 0+auth-gen-tokenprevent surprise re-prompts mid-session.failmode=safefails open if Duo cloud is down — flip tosecurein the template if you'd rather fail closed. - TCP/443 fallback: if client networks block 1194/udp, add a second
server instance (
proto tcp4,port 443, separatedev tun1+ firewall rule). Not enabled by default. - Firewall reloads:
netfilter-persistent reloadflushes Docker's chains — restart docker afterwards (setup.sh does this).
Languages
Shell
76.7%
Dockerfile
16.6%
Jinja
6.7%