Ansible deploy
One playbook does both jobs: the first run provisions the host (Docker, WireGuard, sysctl, firewall) and deploys the stack; every later run is the update path — it only touches what changed and restarts only the affected services.
Deployed layout on the VPS (per-service dirs under /opt):
| Path | Contents |
|---|---|
/opt/vpnhub/ |
docker-compose.yml, .env |
/opt/openvpn/ |
build context, server.conf, pki/ |
/opt/duo-authproxy/ |
build context, cfg template, certs/ |
/opt/technitium/ |
init-job build context + config/ (live zone DB — never overwritten by the playbook) |
The playbook renders the firewall and wg0 config from the same
host/rules.v4.template / host/wg0.conf.example files the manual path
uses — nothing is duplicated inside ansible/.
Prerequisites
A Linux control node (WSL is fine — this repo checked out inside WSL or
accessed via /mnt/c/...), ansible-core >= 2.15, then:
cd ansible
ansible-galaxy collection install -r requirements.yml
Bootstrap (once per fresh VPS)
bootstrap.yml connects as root (it overrides whatever ansible_user
the inventory has), creates the ansible automation user — SSH key only,
locked password, passwordless sudo — proves the new user can log in and
sudo in a second play, and only then locks sshd down: key-only auth
(PasswordAuthentication no) and root SSH login disabled
(PermitRootLogin no). Break-glass access after that is the provider's
console, not SSH:
cp inventory.example.yml inventory.yml
ansible-playbook bootstrap.yml
# custom key or username:
ansible-playbook bootstrap.yml -e bootstrap_ssh_public_key_file=~/.ssh/vpnhub.pub
It prompts for the root SSH password up front (install sshpass on the
control node for password auth). If root already logs in with your key,
just press Enter at the prompt.
Everything after this runs as ansible (already the inventory example's
default).
First-time setup
cp inventory.example.yml inventory.yml # set host/user (if not done above)
cp group_vars/vpnhub.example.yml group_vars/vpnhub.yml
$EDITOR group_vars/vpnhub.yml # DC IPs, Duo keys, secrets
ansible-vault encrypt group_vars/vpnhub.yml # both files are gitignored anyway
# Local key material (both gitignored, synced to /opt by the playbook):
# ../openvpn/pki/ server.crt, server.key, ta.key (offline CA)
# ../duo-authproxy/certs/ ad-ldaps-ca.pem
ansible-playbook site.yml
The first run prints the hub WireGuard public key and stops short of
starting wg0 (site peer keys are still empty). Add the hub as a peer at
both sites (docs/SITE-SIDE.md), paste each site's public key into
group_vars/vpnhub.yml (troch_wg_public_key / oswald_wg_public_key),
and run it again — wg0 comes up and the stack starts.
Updating
| What changed | Command |
|---|---|
Any config in the repo, .env values |
ansible-playbook site.yml (affected services recreate) |
| Just stack files, skip host checks | ansible-playbook site.yml --tags stack |
| Base images / rebuild everything | ansible-playbook site.yml -e vpnhub_force_rebuild=true |
| Duo Auth Proxy version | bump duo_authproxy_version, then force-rebuild as above |
| Firewall only | ansible-playbook site.yml --tags firewall |
| WireGuard peers/config | ansible-playbook site.yml --tags wireguard |
Note on duo_authproxy_version: latest: Docker's layer cache means
"latest" won't re-download a newer tarball on rebuild. Pin a real version
(the cache busts when it changes) — that's the reliable update mechanism.
Tags
docker, host (packages + sysctl + firewall + wg), firewall,
wireguard, stack.