# 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](docker-compose.yml) — openvpn, duo-authproxy, technitium (all `network_mode: host`) - [openvpn/](openvpn/) — server.conf, PAM→RADIUS glue, offline-CA workflow in [pki/README.md](openvpn/pki/README.md) - [duo-authproxy/](duo-authproxy/) — built from Duo's source tarball; cfg rendered from `.env` - [technitium/](technitium/) — Technitium DNS: AD conditional-forwarder zones created by an idempotent API init job ([configure.sh](technitium/configure.sh)), everything else public recursion; web console on :5380 (VPN/tunnel only) - [host/](host/) — `setup.sh` (sysctl, iptables, wg keygen), `wg0.conf.example`, firewall template - [client/](client/) — `.ovpn` template for end users - [ansible/](ansible/) — one idempotent playbook for setup **and** updates, deploys to `/opt/` (see [ansible/README.md](ansible/README.md)) - [docs/](docs/) — [SITE-SIDE.md](docs/SITE-SIDE.md) (DC/firewall work at the sites), [TESTPLAN.md](docs/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](ansible/README.md). Site-side work ([docs/SITE-SIDE.md](docs/SITE-SIDE.md)) and the offline CA stay manual either way. ## Deploy: manual 1. **Host**: `sudo host/setup.sh` — installs wireguard + iptables rules, generates the WG key, prints the public key for the sites. 2. **Sites**: do [docs/SITE-SIDE.md](docs/SITE-SIDE.md) (WG peers, return routes, DC firewall, Duo app, LDAPS CA export). Paste site pubkeys into `/etc/wireguard/wg0.conf`, then `systemctl enable --now wg-quick@wg0`. 3. **Secrets**: `cp .env.example .env && chmod 600 .env`, fill in DC IPs, Duo keys, service account, `RADIUS_SECRET`. 4. **PKI**: offline CA per [openvpn/pki/README.md](openvpn/pki/README.md); copy server cert/key + `ta.key` into `openvpn/pki/`. Drop the AD CA chain in `duo-authproxy/certs/ad-ldaps-ca.pem`. 5. **Stack**: `docker compose up -d --build`. 6. **Verify**: [docs/TESTPLAN.md](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.x` client IPs. Requires the site-side return routes in SITE-SIDE.md. Fallback masquerade snippet is commented at the bottom of [host/rules.v4.template](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 1360` keeps 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` (persists `down`/`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 rerun `docker 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.conf` is repointed at resolved's upstream file. - **Duo flow**: password → push; `password,passcode` → offline fallback (`allow_concat=true`). `reneg-sec 0` + `auth-gen-token` prevent surprise re-prompts mid-session. `failmode=safe` fails *open* if Duo cloud is down — flip to `secure` in 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`, separate `dev tun1` + firewall rule). Not enabled by default. - **Firewall reloads**: `netfilter-persistent reload` flushes Docker's chains — restart docker afterwards (setup.sh/Ansible handler do this). - **IPv6 is locked down too**: [host/rules.v6.template](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.