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──┐ │
│ technitium :53 / console :5380 │ │
│ 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. RADIUS binds loopback only;
Technitium's DNS (:53) and web console (:5380) are firewalled to the
tunnels and additionally guarded by its recursion ACL.
Layout
- docker-compose.yml — openvpn, duo-authproxy, technitium (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 - technitium/ — Technitium DNS: AD conditional-forwarder zones created by an idempotent API init job (configure.sh), everything else public recursion; web console on :5380 (VPN/tunnel only)
- host/ —
setup.sh(sysctl, iptables, wg keygen),wg0.conf.example, firewall template - client/ —
.ovpntemplate for end users - ansible/ — one idempotent playbook for setup and updates, deploys to
/opt/<service>(see ansible/README.md) - docs/ — SITE-SIDE.md (DC/firewall work at the sites), TESTPLAN.md
Deploy: Ansible (preferred)
ansible/site.yml does everything in the manual list below — Docker
install, sysctl, firewall, WireGuard keygen, and the compose stack under
/opt — and re-running it is the update mechanism. See
ansible/README.md. Site-side work
(docs/SITE-SIDE.md) and the offline CA stay manual
either way.
Deploy: manual
- 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. - Technitium is stateful:
DNS_SERVER_*env vars seed config on the first start only; after that its state lives in/opt/technitium/config(persistsdown/up, backed up like data). The init job only creates missing forwarder zones — console edits are never overwritten. Changed a DC IP in.env? Delete the zone in the console and rerundocker compose up technitium-init. - Port 53 on the host: systemd-resolved's stub listener is disabled
(setup.sh/Ansible) — Technitium binds wildcard :53 and would collide;
/etc/resolv.confis repointed at resolved's upstream file. - 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/Ansible handler do this). - IPv6 is locked down too: host/rules.v6.template default-drops the public v6 address (SSH/WG/OpenVPN only, ICMPv6 allowed) since Technitium and the console bind dual-stack wildcard.