claude generated first pass
This commit is contained in:
1 parent
81553590df
commit
911b1f8c5d
24 files changed
+899
-1
No files matched your search
@@ -0,0 +1,29 @@
|
||||
# Copy to .env, fill in, then: chmod 600 .env
|
||||
# Values here are rendered into unbound.conf, authproxy.cfg and pam_radius
|
||||
# config at container start — this is the ONLY place DC IPs live.
|
||||
|
||||
# ── Domain controllers ────────────────────────────────────────────────
|
||||
# AHDC01 (Trochalakis, 192.168.20.0/24)
|
||||
TROCH_DC1_IP=192.168.20.CHANGEME
|
||||
# AHDC03 (Trochalakis, 192.168.20.0/24)
|
||||
TROCH_DC2_IP=192.168.20.CHANGEME
|
||||
# AH-OSWLDC (Oswald, 192.168.7.0/24)
|
||||
OSWALD_DC_IP=192.168.7.CHANGEME
|
||||
|
||||
# ── Duo (create an "OpenVPN" / RADIUS application in the Duo Admin Panel) ──
|
||||
DUO_IKEY=
|
||||
DUO_SKEY=
|
||||
DUO_API_HOST=api-XXXXXXXX.duosecurity.com
|
||||
|
||||
# ── AD service account for the Duo Auth Proxy (read-only domain user) ──
|
||||
# sAMAccountName, not UPN
|
||||
DUO_SVC_USER=svc-duoproxy
|
||||
DUO_SVC_PASSWORD=
|
||||
AD_SEARCH_DN=DC=ad,DC=atlashorizon,DC=net
|
||||
# Optional: restrict VPN to a group. Leave empty to allow any enabled user.
|
||||
# Used only if you uncomment security_group_dn in authproxy.cfg.template.
|
||||
DUO_SECURITY_GROUP_DN=
|
||||
|
||||
# ── OpenVPN ↔ Duo Auth Proxy RADIUS shared secret ─────────────────────
|
||||
# Generate with: openssl rand -hex 24
|
||||
RADIUS_SECRET=
|
||||
@@ -0,0 +1,9 @@
|
||||
# These files execute or get parsed inside Linux containers / on the VPS —
|
||||
# CRLF would break them. Force LF regardless of core.autocrlf.
|
||||
* text=auto
|
||||
*.sh text eol=lf
|
||||
*.conf text eol=lf
|
||||
*.template text eol=lf
|
||||
*.cfg text eol=lf
|
||||
Dockerfile text eol=lf
|
||||
pam-openvpn text eol=lf
|
||||
@@ -0,0 +1,8 @@
|
||||
# Secrets and key material never go in git
|
||||
.env
|
||||
*.key
|
||||
*.pem
|
||||
openvpn/pki/*
|
||||
!openvpn/pki/README.md
|
||||
duo-authproxy/certs/*
|
||||
!duo-authproxy/certs/README.md
|
||||
@@ -1,2 +1,79 @@
|
||||
# racknerd
|
||||
# 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](docker-compose.yml) — openvpn, duo-authproxy, unbound (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`
|
||||
- [unbound/](unbound/) — conditional forwarder: AD zones → DCs over wg0, everything else public recursion
|
||||
- [host/](host/) — `setup.sh` (sysctl, iptables, wg keygen), `wg0.conf.example`, firewall template
|
||||
- [client/](client/) — `.ovpn` template for end users
|
||||
- [docs/](docs/) — [SITE-SIDE.md](docs/SITE-SIDE.md) (DC/firewall work at the sites), [TESTPLAN.md](docs/TESTPLAN.md)
|
||||
|
||||
## Deploy order
|
||||
|
||||
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.
|
||||
- **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 does this).
|
||||
@@ -0,0 +1,44 @@
|
||||
# AtlasHorizon VPN — client profile template.
|
||||
# Per user: paste ca.crt, the user's cert/key, and ta.key (from the offline
|
||||
# CA, see openvpn/pki/README.md) into the inline blocks below, save as
|
||||
# <username>.ovpn, import into OpenVPN Connect.
|
||||
#
|
||||
# Login: AD username; password field takes either
|
||||
# ADpassword -> Duo push
|
||||
# ADpassword,123456 -> appended Duo passcode (offline fallback)
|
||||
#
|
||||
# Connect Before Logon (Windows): install OpenVPN Connect v3 machine-wide
|
||||
# with CBL enabled (MSI: OVPNCONNECTOR_CBL=true or enable in app settings),
|
||||
# import this profile for all users.
|
||||
|
||||
client
|
||||
dev tun
|
||||
proto udp
|
||||
remote vpn.atlashorizon.net 1194
|
||||
resolv-retry infinite
|
||||
nobind
|
||||
persist-key
|
||||
persist-tun
|
||||
|
||||
remote-cert-tls server
|
||||
tls-version-min 1.2
|
||||
data-ciphers AES-256-GCM:CHACHA20-POLY1305
|
||||
auth SHA256
|
||||
|
||||
auth-user-pass
|
||||
auth-retry interact
|
||||
hand-window 120
|
||||
verb 3
|
||||
|
||||
<ca>
|
||||
# ----- paste ca.crt -----
|
||||
</ca>
|
||||
<cert>
|
||||
# ----- paste <username>.crt -----
|
||||
</cert>
|
||||
<key>
|
||||
# ----- paste <username>.key -----
|
||||
</key>
|
||||
<tls-crypt>
|
||||
# ----- paste ta.key -----
|
||||
</tls-crypt>
|
||||
@@ -0,0 +1,57 @@
|
||||
# RackNerd VPN hub — OpenVPN (road warriors) + Duo Auth Proxy + Unbound.
|
||||
#
|
||||
# All services use network_mode: host on purpose:
|
||||
# * openvpn's tun0 must live in the host netns so the kernel can route
|
||||
# tun0 <-> wg0 directly (wg0 is host-level wg-quick).
|
||||
# * unbound/duo bind ONLY loopback + tunnel IPs (enforced in their own
|
||||
# configs), so host networking exposes nothing on the public IP.
|
||||
# Nothing here publishes ports; the host firewall (host/rules.v4.template)
|
||||
# is the single source of truth for what is reachable from where.
|
||||
|
||||
name: racknerd-vpnhub
|
||||
|
||||
services:
|
||||
unbound:
|
||||
build: ./unbound
|
||||
container_name: unbound
|
||||
network_mode: host
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./unbound/unbound.conf.template:/etc/unbound/unbound.conf.template:ro
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD", "dig", "@127.0.0.1", "+time=3", "+tries=1", "cloudflare.com"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
duo-authproxy:
|
||||
build: ./duo-authproxy
|
||||
container_name: duo-authproxy
|
||||
network_mode: host # RADIUS listener bound to 127.0.0.1:1812 only
|
||||
env_file: .env
|
||||
volumes:
|
||||
# AD LDAPS issuing-CA chain, exported from your CA (see certs/README.md)
|
||||
- ./duo-authproxy/certs:/certs:ro
|
||||
restart: unless-stopped
|
||||
|
||||
openvpn:
|
||||
build: ./openvpn
|
||||
container_name: openvpn
|
||||
network_mode: host # creates tun0 in the host netns
|
||||
cap_add:
|
||||
- NET_ADMIN
|
||||
devices:
|
||||
- /dev/net/tun
|
||||
env_file: .env
|
||||
volumes:
|
||||
- ./openvpn/server.conf:/etc/openvpn/server.conf:ro
|
||||
- ./openvpn/pki:/etc/openvpn/pki:ro
|
||||
- openvpn-state:/var/lib/openvpn
|
||||
depends_on:
|
||||
- duo-authproxy
|
||||
- unbound
|
||||
restart: unless-stopped
|
||||
|
||||
volumes:
|
||||
openvpn-state:
|
||||
@@ -0,0 +1,78 @@
|
||||
# Site-side configuration (manual steps at Trochalakis and Oswald)
|
||||
|
||||
Everything the RackNerd box needs from the two sites, the DCs, and the Duo
|
||||
admin panel. Nothing here runs on the VPS.
|
||||
|
||||
## 1. WireGuard peers (both sites)
|
||||
|
||||
Add the RackNerd peer to each site's existing WG config. The sites
|
||||
**initiate** (RackNerd has the static IP) and keep the tunnel alive:
|
||||
|
||||
```ini
|
||||
# Trochalakis WG box — add:
|
||||
[Peer] # RackNerd hub
|
||||
PublicKey = <RACKNERD_WG_PUBLIC_KEY> # printed by host/setup.sh
|
||||
Endpoint = vpn.atlashorizon.net:51820
|
||||
AllowedIPs = 172.31.255.1/32, 172.31.8.0/24
|
||||
PersistentKeepalive = 25
|
||||
```
|
||||
|
||||
Oswald: identical block. Site WG interface addresses on the new mesh links:
|
||||
Trochalakis `172.31.255.2/32`, Oswald `172.31.255.3/32` (add to the site
|
||||
[Interface] Address list if the WG box doesn't already carry an address in
|
||||
this transfer net).
|
||||
|
||||
**Do NOT** add the other site's LAN to the RackNerd peer's AllowedIPs —
|
||||
inter-site traffic stays on your existing direct tunnel. The mesh only adds
|
||||
`172.31.8.0/24` (roaming clients) and `172.31.255.1` (the hub) via RackNerd.
|
||||
|
||||
## 2. Return-path routing (both sites)
|
||||
|
||||
Hosts at each site must route the client subnet back via the site WG box.
|
||||
If the WG box is **not** the LAN default gateway, add static routes on the
|
||||
LAN router (or directly on the DCs):
|
||||
|
||||
| Destination | Via |
|
||||
|------------------|------------------------|
|
||||
| `172.31.8.0/24` | site WireGuard box |
|
||||
| `172.31.255.1/32`| site WireGuard box |
|
||||
|
||||
Verify no asymmetry: `tracert 172.31.8.10` from a DC must leave via the
|
||||
local WG box → RackNerd, never via the other site.
|
||||
|
||||
## 3. Windows Firewall / segmentation on the DCs
|
||||
|
||||
Allow from **`172.31.8.0/24`** (roaming clients, real source IPs): standard
|
||||
AD client ports — DNS 53, Kerberos 88/464, LDAP 389/636, SMB 445, RPC 135 +
|
||||
49152-65535, NTP 123, GC 3268/3269.
|
||||
|
||||
Allow from **`172.31.255.1/32`** (the RackNerd host itself):
|
||||
- TCP/UDP 53 → AHDC01, AHDC03, AH-OSWLDC (Unbound forwarder queries)
|
||||
- TCP 636 → AHDC01, AHDC03 (Duo Auth Proxy LDAPS)
|
||||
|
||||
## 4. Active Directory
|
||||
|
||||
- **Sites and Services**: create subnet `172.31.8.0/24` and assign it to the
|
||||
Trochalakis site (clients will then prefer AHDC01/AHDC03 for DC discovery).
|
||||
- **Duo service account**: plain domain user, read-only, no interactive
|
||||
logon rights; goes in `.env` as `DUO_SVC_USER` / `DUO_SVC_PASSWORD`.
|
||||
- Optional: a `VPN Users` security group if you want `security_group_dn`
|
||||
gating in the auth proxy.
|
||||
|
||||
## 5. LDAPS certificates
|
||||
|
||||
AHDC01 and AHDC03 need valid LDAPS certs (AD CS auto-enrollment or
|
||||
manually issued). Export the issuing CA chain as PEM →
|
||||
`duo-authproxy/certs/ad-ldaps-ca.pem` on the VPS
|
||||
(see that directory's README).
|
||||
|
||||
## 6. Duo Admin Panel
|
||||
|
||||
Applications → Protect an Application → **OpenVPN** (RADIUS). Copy the
|
||||
integration key, secret key, and API hostname into `.env`. Recommended
|
||||
policy: enrolled users only; new-user policy deny.
|
||||
|
||||
## 7. Public DNS
|
||||
|
||||
Create `vpn.atlashorizon.net` → RackNerd static IP (public registrar DNS,
|
||||
not AD). Used by roaming clients and both site WG endpoints.
|
||||
@@ -0,0 +1,79 @@
|
||||
# Test plan
|
||||
|
||||
Run the stages in order — each depends on the previous one.
|
||||
|
||||
## 0. Host plumbing (on the VPS)
|
||||
|
||||
```bash
|
||||
sysctl net.ipv4.ip_forward # = 1
|
||||
wg show wg0 # both peers: recent "latest handshake"
|
||||
ping -c3 172.31.255.2 # Trochalakis WG box
|
||||
ping -c3 172.31.255.3 # Oswald WG box
|
||||
ping -c3 <TROCH_DC1_IP> # AHDC01 across the tunnel
|
||||
ping -c3 <OSWALD_DC_IP> # AH-OSWLDC across the tunnel
|
||||
```
|
||||
|
||||
## 1. Unbound (on the VPS)
|
||||
|
||||
```bash
|
||||
docker compose ps unbound # healthy
|
||||
# AD SRV record via conditional forward (the money query):
|
||||
dig @127.0.0.1 _ldap._tcp.dc._msdcs.ad.atlashorizon.net SRV +short
|
||||
dig @127.0.0.1 ahdc01.ad.atlashorizon.net +short
|
||||
dig @127.0.0.1 ah-oswldc.ad.oswaldtheotter.net +short
|
||||
dig @127.0.0.1 -x <TROCH_DC1_IP> +short # reverse zone
|
||||
dig @127.0.0.1 google.com +short # public recursion
|
||||
```
|
||||
|
||||
## 2. Duo Auth Proxy (on the VPS)
|
||||
|
||||
```bash
|
||||
docker compose logs duo-authproxy # connectivity tool output: LDAPS bind
|
||||
# to both DCs OK, Duo API reachable
|
||||
# End-to-end RADIUS (expect a push to your phone, then Access-Accept):
|
||||
apt-get install -y freeradius-utils
|
||||
radtest '<aduser>' '<adpassword>' 127.0.0.1:1812 10 "$RADIUS_SECRET"
|
||||
# Negative: wrong password -> immediate Access-Reject, no push.
|
||||
```
|
||||
|
||||
This stage proves the **LDAP bind** leg independently of OpenVPN.
|
||||
|
||||
## 3. Full client connect (from outside, e.g. phone hotspot)
|
||||
|
||||
1. Import a built `.ovpn`, connect with AD username + password → approve
|
||||
the Duo push → connected.
|
||||
2. `ipconfig /all`: address in `172.31.8.0/24`, DNS `172.31.8.1`, suffix
|
||||
`ad.atlashorizon.net`.
|
||||
3. DNS through the tunnel:
|
||||
`nslookup -type=SRV _ldap._tcp.dc._msdcs.ad.atlashorizon.net`
|
||||
4. Reachability, one host per site:
|
||||
`ping ahdc01.ad.atlashorizon.net` and `ping ah-oswldc.ad.oswaldtheotter.net`
|
||||
5. AD actually works: `dir \\ad.atlashorizon.net\SYSVOL` then `klist`
|
||||
(Kerberos tickets present, no NTLM fallback).
|
||||
6. Passcode-append fallback: reconnect using `password,<passcode>` from the
|
||||
Duo app — must succeed with no push.
|
||||
|
||||
## 4. Routing correctness
|
||||
|
||||
- On AHDC01, Security log 4624/4768 for the test user shows the **real**
|
||||
client IP (`172.31.8.x`), not a NAT address — confirms routed (no
|
||||
masquerade) design.
|
||||
- From the client: `tracert <TROCH_DC1_IP>` → first hop `172.31.8.1`.
|
||||
- From a DC at each site: `tracert 172.31.8.x` → leaves via the **local**
|
||||
WG box (no path through the other site = no asymmetric routing).
|
||||
- From a Trochalakis host: `tracert <oswald LAN IP>` still uses the direct
|
||||
site-to-site tunnel (RackNerd never appears).
|
||||
|
||||
## 5. Exposure check (from any outside box)
|
||||
|
||||
```bash
|
||||
nmap -sU -p 53,1194,51820 <racknerd_public_ip> # 53 must NOT respond
|
||||
nmap -sT -p 22,53,636,1812,7505 <racknerd_public_ip> # only 22 open
|
||||
```
|
||||
|
||||
## 6. Persistence
|
||||
|
||||
```bash
|
||||
docker compose down && docker compose up -d --build # survives cleanly
|
||||
reboot # then: wg show, docker compose ps, re-run stage 3 quick connect
|
||||
```
|
||||
@@ -0,0 +1,38 @@
|
||||
# Duo publishes no official image; build from their source tarball.
|
||||
# Install flags match Duo's documented non-interactive install.
|
||||
|
||||
FROM ubuntu:24.04 AS build
|
||||
|
||||
ARG DUO_VERSION=latest
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
build-essential libssl-dev libffi-dev perl zlib1g-dev wget ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
RUN wget -q "https://dl.duosecurity.com/duoauthproxy-${DUO_VERSION}-src.tgz" -O /tmp/duo-src.tgz \
|
||||
&& mkdir /tmp/duo \
|
||||
&& tar xzf /tmp/duo-src.tgz -C /tmp/duo --strip-components=1
|
||||
|
||||
RUN groupadd -r -g 35505 duo \
|
||||
&& useradd -r -u 35505 -g duo -d /opt/duoauthproxy -s /usr/sbin/nologin duo \
|
||||
&& cd /tmp/duo && make \
|
||||
&& cd duoauthproxy-build \
|
||||
&& ./install --install-dir /opt/duoauthproxy --service-user duo \
|
||||
--log-group duo --create-init-script no --enable-selinux no
|
||||
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# gettext-base = envsubst for rendering authproxy.cfg from env vars
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends ca-certificates gettext-base \
|
||||
&& rm -rf /var/lib/apt/lists/* \
|
||||
&& groupadd -r -g 35505 duo \
|
||||
&& useradd -r -u 35505 -g duo -d /opt/duoauthproxy -s /usr/sbin/nologin duo
|
||||
|
||||
COPY --from=build --chown=duo:duo /opt/duoauthproxy /opt/duoauthproxy
|
||||
COPY --chown=duo:duo authproxy.cfg.template /opt/duoauthproxy/conf/authproxy.cfg.template
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
USER duo
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,42 @@
|
||||
; Duo Authentication Proxy — rendered from env vars at container start.
|
||||
; Chain: OpenVPN (pam_radius) -> [radius_server_auto] -> [ad_client] LDAPS
|
||||
; to AHDC01/AHDC03 over wg0, factor via Duo cloud (outbound 443).
|
||||
|
||||
[main]
|
||||
log_stdout=true
|
||||
debug=false
|
||||
|
||||
[ad_client]
|
||||
host=${TROCH_DC1_IP}
|
||||
host_2=${TROCH_DC2_IP}
|
||||
service_account_username=${DUO_SVC_USER}
|
||||
service_account_password=${DUO_SVC_PASSWORD}
|
||||
search_dn=${AD_SEARCH_DN}
|
||||
transport=ldaps
|
||||
port=636
|
||||
ssl_ca_certs_file=/certs/ad-ldaps-ca.pem
|
||||
; We target the DCs by IP, so hostname verification can't match the cert
|
||||
; SAN. The chain is still validated against your AD CA. To go strict:
|
||||
; use DC FQDNs in host/host_2, point the VPS resolver at Unbound, and
|
||||
; set this to true.
|
||||
ssl_verify_hostname=false
|
||||
; Restrict VPN access to one AD group (fill DUO_SECURITY_GROUP_DN in .env
|
||||
; and uncomment):
|
||||
;security_group_dn=${DUO_SECURITY_GROUP_DN}
|
||||
|
||||
[radius_server_auto]
|
||||
ikey=${DUO_IKEY}
|
||||
skey=${DUO_SKEY}
|
||||
api_host=${DUO_API_HOST}
|
||||
client=ad_client
|
||||
; Only OpenVPN on this host may talk to us
|
||||
interface=127.0.0.1
|
||||
port=1812
|
||||
radius_ip_1=127.0.0.1
|
||||
radius_secret_1=${RADIUS_SECRET}
|
||||
; Password-only = auto (push); "password,123456" appends a passcode —
|
||||
; the fallback for connect-before-logon when the phone is offline.
|
||||
allow_concat=true
|
||||
; safe = allow password-only auth if Duo cloud is unreachable (Duo default).
|
||||
; Set to "secure" to fail closed instead.
|
||||
failmode=safe
|
||||
@@ -0,0 +1,18 @@
|
||||
# AD LDAPS CA chain
|
||||
|
||||
Place the PEM-encoded CA chain that issued your domain controllers' LDAPS
|
||||
certificates here as **`ad-ldaps-ca.pem`** (root + any intermediates,
|
||||
concatenated).
|
||||
|
||||
If you run AD CS, export it from any domain-joined box:
|
||||
|
||||
```powershell
|
||||
# Grab the issuing chain from AHDC01's LDAPS cert
|
||||
$tcp = New-Object Net.Sockets.TcpClient('ahdc01.ad.atlashorizon.net', 636)
|
||||
# ...or just export from the CA console / certutil:
|
||||
certutil -ca.cert ca.der
|
||||
certutil -encode ca.der ad-ldaps-ca.pem
|
||||
```
|
||||
|
||||
The Duo Auth Proxy refuses to start LDAPS without it (checked in
|
||||
`entrypoint.sh`). Everything except this README is gitignored.
|
||||
@@ -0,0 +1,25 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
for v in TROCH_DC1_IP TROCH_DC2_IP DUO_IKEY DUO_SKEY DUO_API_HOST \
|
||||
DUO_SVC_USER DUO_SVC_PASSWORD AD_SEARCH_DN RADIUS_SECRET; do
|
||||
eval "val=\${$v:-}"
|
||||
[ -n "$val" ] || { echo "ERROR: $v is not set (see .env.example)" >&2; exit 1; }
|
||||
done
|
||||
|
||||
if [ ! -f /certs/ad-ldaps-ca.pem ]; then
|
||||
echo "ERROR: /certs/ad-ldaps-ca.pem missing — export your AD LDAPS issuing" >&2
|
||||
echo "CA chain into duo-authproxy/certs/ (see certs/README.md)" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
envsubst '${TROCH_DC1_IP} ${TROCH_DC2_IP} ${DUO_IKEY} ${DUO_SKEY} ${DUO_API_HOST} ${DUO_SVC_USER} ${DUO_SVC_PASSWORD} ${AD_SEARCH_DN} ${RADIUS_SECRET}' \
|
||||
< /opt/duoauthproxy/conf/authproxy.cfg.template \
|
||||
> /opt/duoauthproxy/conf/authproxy.cfg
|
||||
chmod 600 /opt/duoauthproxy/conf/authproxy.cfg
|
||||
|
||||
# Optional pre-flight (LDAPS reachability, Duo API ping). Non-fatal so a
|
||||
# temporarily unreachable DC doesn't crash-loop the container.
|
||||
/opt/duoauthproxy/bin/authproxy_connectivity_tool || true
|
||||
|
||||
exec /opt/duoauthproxy/bin/authproxy
|
||||
@@ -0,0 +1,2 @@
|
||||
# RackNerd VPN hub — route between tun0 (OpenVPN clients) and wg0 (sites)
|
||||
net.ipv4.ip_forward = 1
|
||||
@@ -0,0 +1,54 @@
|
||||
# iptables ruleset for the RackNerd VPN hub.
|
||||
# ${PUBIF} is substituted by host/setup.sh with the detected public interface.
|
||||
# Installed to /etc/iptables/rules.v4 (iptables-persistent).
|
||||
#
|
||||
# NOTE: after any manual `netfilter-persistent reload`, run
|
||||
# `systemctl restart docker` — the reload flushes Docker's chains.
|
||||
|
||||
*filter
|
||||
:INPUT DROP [0:0]
|
||||
:FORWARD DROP [0:0]
|
||||
:OUTPUT ACCEPT [0:0]
|
||||
:DOCKER-USER - [0:0]
|
||||
|
||||
# ── INPUT ──────────────────────────────────────────────────────────────
|
||||
-A INPUT -i lo -j ACCEPT
|
||||
-A INPUT -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
-A INPUT -m conntrack --ctstate INVALID -j DROP
|
||||
-A INPUT -p icmp -j ACCEPT
|
||||
|
||||
# Public interface: SSH (key-only!), WireGuard, OpenVPN — nothing else.
|
||||
-A INPUT -i ${PUBIF} -p tcp --dport 22 -m conntrack --ctstate NEW -j ACCEPT
|
||||
-A INPUT -i ${PUBIF} -p udp --dport 51820 -j ACCEPT
|
||||
-A INPUT -i ${PUBIF} -p udp --dport 1194 -j ACCEPT
|
||||
|
||||
# OpenVPN clients: DNS to Unbound only (no SSH, no mgmt from clients)
|
||||
-A INPUT -i tun0 -d 172.31.8.1/32 -p udp --dport 53 -j ACCEPT
|
||||
-A INPUT -i tun0 -d 172.31.8.1/32 -p tcp --dport 53 -j ACCEPT
|
||||
|
||||
# Site tunnels: allow DNS in case site hosts want to use Unbound
|
||||
-A INPUT -i wg0 -p udp --dport 53 -j ACCEPT
|
||||
-A INPUT -i wg0 -p tcp --dport 53 -j ACCEPT
|
||||
|
||||
# ── FORWARD (routed, no NAT — DCs see real client IPs) ────────────────
|
||||
-A FORWARD -m conntrack --ctstate ESTABLISHED,RELATED -j ACCEPT
|
||||
-A FORWARD -m conntrack --ctstate INVALID -j DROP
|
||||
|
||||
# Roaming clients -> both sites + WG transfer net
|
||||
-A FORWARD -i tun0 -o wg0 -s 172.31.8.0/24 -d 192.168.20.0/24 -j ACCEPT
|
||||
-A FORWARD -i tun0 -o wg0 -s 172.31.8.0/24 -d 192.168.7.0/24 -j ACCEPT
|
||||
-A FORWARD -i tun0 -o wg0 -s 172.31.8.0/24 -d 172.31.255.0/24 -j ACCEPT
|
||||
|
||||
# Site -> client NEW connections (e.g. RDP/RSAT from a DC to a roaming
|
||||
# laptop). Off by default; uncomment if you need it.
|
||||
#-A FORWARD -i wg0 -o tun0 -d 172.31.8.0/24 -j ACCEPT
|
||||
|
||||
COMMIT
|
||||
|
||||
# ── Fallback per project brief ─────────────────────────────────────────
|
||||
# If site-side return routing for 172.31.8.0/24 becomes a pain, NAT the
|
||||
# clients behind the wg0 address instead (DCs then see 172.31.255.1):
|
||||
#*nat
|
||||
#:POSTROUTING ACCEPT [0:0]
|
||||
#-A POSTROUTING -s 172.31.8.0/24 -o wg0 -j MASQUERADE
|
||||
#COMMIT
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
# Host-side bootstrap for the RackNerd VPN hub (Ubuntu).
|
||||
# Run as root from the repo: sudo host/setup.sh
|
||||
# Idempotent: safe to re-run; never overwrites an existing wg0.conf.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
[ "$(id -u)" -eq 0 ] || { echo "Run as root."; exit 1; }
|
||||
|
||||
PUBIF=$(ip -4 route show default | awk '{print $5; exit}')
|
||||
echo "==> Public interface: ${PUBIF}"
|
||||
|
||||
echo "==> Installing wireguard + iptables-persistent"
|
||||
apt-get update -qq
|
||||
DEBIAN_FRONTEND=noninteractive apt-get install -y wireguard iptables-persistent
|
||||
|
||||
echo "==> sysctl (ip_forward)"
|
||||
install -m 0644 99-vpnhub-sysctl.conf /etc/sysctl.d/99-vpnhub.conf
|
||||
sysctl --system >/dev/null
|
||||
|
||||
echo "==> Firewall (WARNING: INPUT policy DROP — port 22 stays open)"
|
||||
sed "s/\${PUBIF}/${PUBIF}/g" rules.v4.template > /etc/iptables/rules.v4
|
||||
netfilter-persistent reload
|
||||
# reload flushed Docker's chains; put them back if Docker is running
|
||||
if systemctl is-active --quiet docker; then
|
||||
systemctl restart docker
|
||||
fi
|
||||
|
||||
echo "==> WireGuard"
|
||||
if [ ! -f /etc/wireguard/wg0.conf ]; then
|
||||
umask 077
|
||||
PRIV=$(wg genkey)
|
||||
sed "s|<RACKNERD_WG_PRIVATE_KEY>|${PRIV}|" wg0.conf.example > /etc/wireguard/wg0.conf
|
||||
echo
|
||||
echo " RackNerd WG public key (add as a peer at BOTH sites):"
|
||||
echo " $(echo "${PRIV}" | wg pubkey)"
|
||||
echo
|
||||
echo " >>> Now edit /etc/wireguard/wg0.conf and paste in the"
|
||||
echo " >>> Trochalakis and Oswald public keys, then:"
|
||||
echo " >>> systemctl enable --now wg-quick@wg0"
|
||||
else
|
||||
echo " /etc/wireguard/wg0.conf exists — leaving it alone."
|
||||
systemctl enable --now wg-quick@wg0
|
||||
fi
|
||||
|
||||
echo "==> Done. Next: cp .env.example .env, fill it in, docker compose up -d --build"
|
||||
@@ -0,0 +1,27 @@
|
||||
# /etc/wireguard/wg0.conf — RackNerd hub end of the three-way mesh.
|
||||
# host/setup.sh installs this with a freshly generated private key; you
|
||||
# paste in each site's public key afterwards.
|
||||
|
||||
[Interface]
|
||||
Address = 172.31.255.1/24
|
||||
ListenPort = 51820
|
||||
PrivateKey = <RACKNERD_WG_PRIVATE_KEY>
|
||||
# wg-quick auto-adds routes for each peer's AllowedIPs — that is exactly
|
||||
# the routing we want, so no PostUp needed. No MASQUERADE: client traffic
|
||||
# is routed, DCs see real 172.31.8.x source IPs.
|
||||
|
||||
# ── Trochalakis (AHDC01 / AHDC03 behind this peer) ──
|
||||
[Peer]
|
||||
PublicKey = <TROCHALAKIS_WG_PUBLIC_KEY>
|
||||
AllowedIPs = 172.31.255.2/32, 192.168.20.0/24
|
||||
# No Endpoint: the site initiates to our static IP and holds the tunnel
|
||||
# open with PersistentKeepalive = 25 on its side (see docs/SITE-SIDE.md).
|
||||
|
||||
# ── Oswald (AH-OSWLDC behind this peer) ──
|
||||
[Peer]
|
||||
PublicKey = <OSWALD_WG_PUBLIC_KEY>
|
||||
AllowedIPs = 172.31.255.3/32, 192.168.7.0/24
|
||||
|
||||
# Deliberately NOT listing the other site's LAN under either peer:
|
||||
# Oswald<->Trochalakis traffic must stay on their direct tunnel, never
|
||||
# hairpin through this box.
|
||||
@@ -0,0 +1,13 @@
|
||||
FROM debian:bookworm-slim
|
||||
|
||||
# libpam-radius-auth: PAM -> RADIUS bridge to the Duo Auth Proxy on 127.0.0.1
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends \
|
||||
openvpn libpam-radius-auth iproute2 procps ca-certificates \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY pam-openvpn /etc/pam.d/openvpn
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,14 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${RADIUS_SECRET:?RADIUS_SECRET must be set in .env}"
|
||||
|
||||
# pam_radius format: server[:port] secret timeout
|
||||
# 60s timeout leaves room for the user to approve a Duo push.
|
||||
printf '127.0.0.1:1812\t%s\t60\n' "$RADIUS_SECRET" > /etc/pam_radius_auth.conf
|
||||
chmod 600 /etc/pam_radius_auth.conf
|
||||
|
||||
mkdir -p /var/lib/openvpn
|
||||
[ -c /dev/net/tun ] || { mkdir -p /dev/net; mknod /dev/net/tun c 10 200; }
|
||||
|
||||
exec openvpn --cd /etc/openvpn --config server.conf
|
||||
@@ -0,0 +1,5 @@
|
||||
# PAM service used by openvpn-plugin-auth-pam.
|
||||
# pam_radius_auth reads /etc/pam_radius_auth.conf (written by entrypoint.sh)
|
||||
# and forwards the username/password to the Duo Auth Proxy on 127.0.0.1:1812.
|
||||
auth required pam_radius_auth.so
|
||||
account required pam_permit.so
|
||||
@@ -0,0 +1,50 @@
|
||||
# OpenVPN PKI — offline CA workflow
|
||||
|
||||
The CA private key **never touches the VPS**. Run easy-rsa on an offline
|
||||
machine (or at minimum a machine that isn't this server), then copy only the
|
||||
issued material here.
|
||||
|
||||
## One-time CA + server cert (on the offline machine)
|
||||
|
||||
```bash
|
||||
git clone https://github.com/OpenVPN/easy-rsa.git && cd easy-rsa/easyrsa3
|
||||
./easyrsa init-pki
|
||||
./easyrsa build-ca # sets the CA passphrase — keep offline
|
||||
./easyrsa build-server-full racknerd-vpn nopass
|
||||
openvpn --genkey tls-crypt ta.key # shared tls-crypt key
|
||||
./easyrsa gen-crl
|
||||
```
|
||||
|
||||
## Copy to the VPS (this directory)
|
||||
|
||||
| File on offline box | Name here |
|
||||
|--------------------------------------|---------------|
|
||||
| `pki/ca.crt` | `ca.crt` |
|
||||
| `pki/issued/racknerd-vpn.crt` | `server.crt` |
|
||||
| `pki/private/racknerd-vpn.key` | `server.key` |
|
||||
| `ta.key` | `ta.key` |
|
||||
| `pki/crl.pem` (optional, see server.conf) | `crl.pem` |
|
||||
|
||||
`chmod 600 server.key ta.key` after copying.
|
||||
|
||||
## Per-user client certs (offline)
|
||||
|
||||
```bash
|
||||
./easyrsa build-client-full nick nopass
|
||||
```
|
||||
|
||||
Build the client's `.ovpn` from `client/client.ovpn.template`, pasting
|
||||
`ca.crt`, the client cert/key, and `ta.key` into the inline blocks.
|
||||
CN should match the AD username — the server logs use it
|
||||
(`username-as-common-name` keeps them consistent).
|
||||
|
||||
## Revoking a client
|
||||
|
||||
```bash
|
||||
./easyrsa revoke nick && ./easyrsa gen-crl
|
||||
```
|
||||
|
||||
Copy the fresh `crl.pem` here, uncomment `crl-verify` in `server.conf`, and
|
||||
`docker compose restart openvpn`.
|
||||
|
||||
Everything in this directory except this README is gitignored.
|
||||
@@ -0,0 +1,65 @@
|
||||
# OpenVPN road-warrior endpoint — RackNerd hub
|
||||
# Auth chain: client cert (offline CA) + user/pass -> PAM -> RADIUS
|
||||
# -> Duo Auth Proxy (127.0.0.1:1812) -> LDAPS to the DCs.
|
||||
|
||||
port 1194
|
||||
proto udp4
|
||||
dev tun0
|
||||
topology subnet
|
||||
|
||||
server 172.31.8.0 255.255.255.0
|
||||
ifconfig-pool-persist /var/lib/openvpn/ipp.txt
|
||||
|
||||
# Split tunnel: only the AD sites + transfer net ride the VPN.
|
||||
push "route 192.168.20.0 255.255.255.0"
|
||||
push "route 192.168.7.0 255.255.255.0"
|
||||
push "route 172.31.255.0 255.255.255.0"
|
||||
|
||||
# All client DNS goes to Unbound on this box (block-outside-dns stops
|
||||
# Windows from leaking AD lookups to the local LAN resolver).
|
||||
push "dhcp-option DNS 172.31.8.1"
|
||||
push "dhcp-option DOMAIN ad.atlashorizon.net"
|
||||
push "dhcp-option DOMAIN-SEARCH ad.atlashorizon.net"
|
||||
push "dhcp-option DOMAIN-SEARCH ad.calliecorner.net"
|
||||
push "dhcp-option DOMAIN-SEARCH ad.oswaldtheotter.net"
|
||||
push "dhcp-option DOMAIN-SEARCH ad.trochalakis.net"
|
||||
push "block-outside-dns"
|
||||
|
||||
# PKI — issued from the offline CA, see pki/README.md
|
||||
ca /etc/openvpn/pki/ca.crt
|
||||
cert /etc/openvpn/pki/server.crt
|
||||
key /etc/openvpn/pki/server.key
|
||||
dh none
|
||||
tls-crypt /etc/openvpn/pki/ta.key
|
||||
# Enable once you publish a CRL from the offline CA (revoked laptops etc.):
|
||||
;crl-verify /etc/openvpn/pki/crl.pem
|
||||
|
||||
remote-cert-tls client
|
||||
tls-version-min 1.2
|
||||
data-ciphers AES-256-GCM:CHACHA20-POLY1305
|
||||
auth SHA256
|
||||
|
||||
# User auth via Duo (password only = push; "password,123456" = passcode)
|
||||
plugin /usr/lib/openvpn/openvpn-plugin-auth-pam.so openvpn
|
||||
verify-client-cert require
|
||||
username-as-common-name
|
||||
hand-window 120 # allow time for the Duo push round-trip
|
||||
reneg-sec 0 # no mid-session TLS renegotiation (would re-prompt Duo)
|
||||
auth-gen-token 43200 # reconnects within 12h reuse a token instead of a new push
|
||||
|
||||
keepalive 10 60
|
||||
explicit-exit-notify 1
|
||||
|
||||
# wg0 MTU is 1420; keep the client tunnel small enough that client->site
|
||||
# packets never fragment inside the WireGuard leg.
|
||||
tun-mtu 1400
|
||||
mssfix 1360
|
||||
|
||||
user nobody
|
||||
group nogroup
|
||||
persist-key
|
||||
persist-tun
|
||||
|
||||
status /var/lib/openvpn/status.log 30
|
||||
management 127.0.0.1 7505
|
||||
verb 3
|
||||
@@ -0,0 +1,12 @@
|
||||
FROM alpine:3.20
|
||||
|
||||
# gettext = envsubst (renders DC IPs from .env into the config)
|
||||
# bind-tools = dig, used by the compose healthcheck
|
||||
RUN apk add --no-cache unbound gettext bind-tools ca-certificates
|
||||
|
||||
COPY entrypoint.sh /entrypoint.sh
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
# Starts as root to bind :53 and set IP_FREEBIND, then unbound itself
|
||||
# drops to the "unbound" user (username: directive in the config).
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
@@ -0,0 +1,17 @@
|
||||
#!/bin/sh
|
||||
set -eu
|
||||
|
||||
: "${TROCH_DC1_IP:?TROCH_DC1_IP must be set in .env}"
|
||||
: "${TROCH_DC2_IP:?TROCH_DC2_IP must be set in .env}"
|
||||
: "${OSWALD_DC_IP:?OSWALD_DC_IP must be set in .env}"
|
||||
|
||||
envsubst '${TROCH_DC1_IP} ${TROCH_DC2_IP} ${OSWALD_DC_IP}' \
|
||||
< /etc/unbound/unbound.conf.template \
|
||||
> /etc/unbound/unbound.conf
|
||||
|
||||
# DNSSEC root trust anchor for public resolution (exit 1 just means "updated")
|
||||
unbound-anchor -a /etc/unbound/root.key || true
|
||||
chown unbound:unbound /etc/unbound/root.key
|
||||
|
||||
unbound-checkconf /etc/unbound/unbound.conf
|
||||
exec unbound -d -c /etc/unbound/unbound.conf
|
||||
@@ -0,0 +1,89 @@
|
||||
# Unbound — caching resolver + conditional forwarder for AD zones.
|
||||
# ${TROCH_DC1_IP}-style tokens are rendered from .env at container start.
|
||||
|
||||
server:
|
||||
verbosity: 1
|
||||
username: "unbound"
|
||||
do-daemonize: no
|
||||
chroot: ""
|
||||
|
||||
# Listen ONLY on loopback and tunnel-facing IPs — never the public IP.
|
||||
# ip-freebind lets us bind 172.31.8.1 even before OpenVPN brings up tun0.
|
||||
interface: 127.0.0.1
|
||||
interface: 172.31.8.1
|
||||
interface: 172.31.255.1
|
||||
ip-freebind: yes
|
||||
port: 53
|
||||
do-ip6: no
|
||||
|
||||
access-control: 0.0.0.0/0 refuse
|
||||
access-control: 127.0.0.0/8 allow
|
||||
access-control: 172.31.8.0/24 allow # OpenVPN clients
|
||||
access-control: 172.31.255.0/24 allow # WireGuard transfer net
|
||||
access-control: 192.168.20.0/24 allow # Trochalakis LAN (optional use)
|
||||
access-control: 192.168.7.0/24 allow # Oswald LAN (optional use)
|
||||
|
||||
hide-identity: yes
|
||||
hide-version: yes
|
||||
harden-glue: yes
|
||||
harden-dnssec-stripped: yes
|
||||
qname-minimisation: yes
|
||||
prefetch: yes
|
||||
msg-cache-size: 64m
|
||||
rrset-cache-size: 128m
|
||||
|
||||
auto-trust-anchor-file: "/etc/unbound/root.key"
|
||||
|
||||
# AD zones return RFC1918 answers and are unsigned — exempt them from
|
||||
# rebind protection and DNSSEC validation.
|
||||
private-address: 10.0.0.0/8
|
||||
private-address: 172.16.0.0/12
|
||||
private-address: 192.168.0.0/16
|
||||
private-domain: "ad.atlashorizon.net"
|
||||
private-domain: "ad.calliecorner.net"
|
||||
private-domain: "ad.oswaldtheotter.net"
|
||||
private-domain: "ad.trochalakis.net"
|
||||
domain-insecure: "ad.atlashorizon.net"
|
||||
domain-insecure: "ad.calliecorner.net"
|
||||
domain-insecure: "ad.oswaldtheotter.net"
|
||||
domain-insecure: "ad.trochalakis.net"
|
||||
domain-insecure: "20.168.192.in-addr.arpa"
|
||||
domain-insecure: "7.168.192.in-addr.arpa"
|
||||
local-zone: "20.168.192.in-addr.arpa." nodefault
|
||||
local-zone: "7.168.192.in-addr.arpa." nodefault
|
||||
|
||||
# ── Conditional forwarding: AD zones → DCs over the WireGuard tunnel. ──
|
||||
# Everything not listed below is resolved publicly by normal recursion.
|
||||
|
||||
forward-zone:
|
||||
name: "ad.atlashorizon.net." # also covers _msdcs.ad.atlashorizon.net
|
||||
forward-addr: ${TROCH_DC1_IP} # AHDC01
|
||||
forward-addr: ${TROCH_DC2_IP} # AHDC03
|
||||
|
||||
forward-zone:
|
||||
name: "ad.calliecorner.net."
|
||||
forward-addr: ${TROCH_DC1_IP}
|
||||
forward-addr: ${TROCH_DC2_IP}
|
||||
|
||||
forward-zone:
|
||||
name: "ad.trochalakis.net."
|
||||
forward-addr: ${TROCH_DC1_IP}
|
||||
forward-addr: ${TROCH_DC2_IP}
|
||||
|
||||
forward-zone:
|
||||
name: "ad.oswaldtheotter.net."
|
||||
forward-addr: ${OSWALD_DC_IP} # AH-OSWLDC
|
||||
# Root DCs as fallback — keep ONLY if this zone replicates forest-wide
|
||||
# (ForestDnsZones); remove these two lines otherwise.
|
||||
forward-addr: ${TROCH_DC1_IP}
|
||||
forward-addr: ${TROCH_DC2_IP}
|
||||
|
||||
# Reverse zones so PTR lookups for site hosts work
|
||||
forward-zone:
|
||||
name: "20.168.192.in-addr.arpa."
|
||||
forward-addr: ${TROCH_DC1_IP}
|
||||
forward-addr: ${TROCH_DC2_IP}
|
||||
|
||||
forward-zone:
|
||||
name: "7.168.192.in-addr.arpa."
|
||||
forward-addr: ${OSWALD_DC_IP}
|
||||
Reference in new issue
Block a user