next ai changes
This commit is contained in:
1 parent
911b1f8c5d
commit
b745376a65
23 files changed
+705
-155
No files matched your search
@@ -0,0 +1,70 @@
|
||||
# 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:
|
||||
|
||||
```bash
|
||||
cd ansible
|
||||
ansible-galaxy collection install -r requirements.yml
|
||||
```
|
||||
|
||||
## First-time setup
|
||||
|
||||
```bash
|
||||
cp inventory.example.yml inventory.yml # set host/user
|
||||
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`.
|
||||
@@ -0,0 +1,8 @@
|
||||
[defaults]
|
||||
inventory = inventory.yml
|
||||
interpreter_python = auto_silent
|
||||
# Uncomment once group_vars/vpnhub.yml is vault-encrypted:
|
||||
# vault_password_file = ~/.vault-pass-vpnhub
|
||||
|
||||
[privilege_escalation]
|
||||
become_ask_pass = False
|
||||
@@ -0,0 +1,41 @@
|
||||
# Copy to vpnhub.yml, fill in, then ENCRYPT before committing anything:
|
||||
# ansible-vault encrypt group_vars/vpnhub.yml
|
||||
# (vpnhub.yml is gitignored regardless, belt and braces.)
|
||||
|
||||
# ── Domain controllers ──
|
||||
troch_dc1_ip: 192.168.20.CHANGEME # AHDC01
|
||||
troch_dc2_ip: 192.168.20.CHANGEME # AHDC03
|
||||
oswald_dc_ip: 192.168.7.CHANGEME # AH-OSWLDC
|
||||
|
||||
# ── WireGuard site peers ──
|
||||
# Leave empty on the first run: the playbook generates the hub key, prints
|
||||
# the hub PUBLIC key to add at both sites, and holds off starting wg0.
|
||||
# Paste each site's public key here and re-run.
|
||||
troch_wg_public_key: ""
|
||||
oswald_wg_public_key: ""
|
||||
|
||||
# ── Duo (Admin Panel -> Applications -> OpenVPN/RADIUS) ──
|
||||
duo_ikey: ""
|
||||
duo_skey: ""
|
||||
duo_api_host: api-XXXXXXXX.duosecurity.com
|
||||
|
||||
# ── AD service account for the Duo Auth Proxy (read-only) ──
|
||||
duo_svc_user: svc-duoproxy # sAMAccountName, not UPN
|
||||
duo_svc_password: ""
|
||||
ad_search_dn: DC=ad,DC=atlashorizon,DC=net
|
||||
# Only used if you uncomment security_group_dn in authproxy.cfg.template:
|
||||
duo_security_group_dn: ""
|
||||
|
||||
# ── OpenVPN <-> Duo RADIUS shared secret (openssl rand -hex 24) ──
|
||||
radius_secret: ""
|
||||
|
||||
# ── Technitium web console admin password ──
|
||||
# Seeded on the FIRST container start only; afterwards change it in the
|
||||
# console (this var must still match for the init job's API login).
|
||||
technitium_admin_password: ""
|
||||
|
||||
# ── Optional ──
|
||||
# Pin for deterministic Duo updates (e.g. "6.5.1"); bumping it forces a
|
||||
# refetch of the tarball on the next rebuild.
|
||||
duo_authproxy_version: latest
|
||||
# vpnhub_base_dir: /opt
|
||||
@@ -0,0 +1,11 @@
|
||||
# Copy to inventory.yml and adjust.
|
||||
all:
|
||||
children:
|
||||
vpnhub:
|
||||
hosts:
|
||||
racknerd:
|
||||
ansible_host: vpn.atlashorizon.net
|
||||
ansible_user: root
|
||||
# Non-root instead? Use:
|
||||
# ansible_user: nick
|
||||
# ansible_become: true
|
||||
@@ -0,0 +1,5 @@
|
||||
collections:
|
||||
- name: community.docker
|
||||
version: ">=3.10.0"
|
||||
- name: ansible.posix
|
||||
version: ">=1.5.0"
|
||||
@@ -0,0 +1,305 @@
|
||||
---
|
||||
# RackNerd VPN hub — full host + stack provisioning. Idempotent:
|
||||
# the first run is setup, every later run is the update path.
|
||||
#
|
||||
# ansible-playbook site.yml # converge everything
|
||||
# ansible-playbook site.yml --tags stack # only the compose stack
|
||||
# ansible-playbook site.yml -e vpnhub_force_rebuild=true # rebuild/pull images
|
||||
#
|
||||
# Deployed layout (per-service dirs under /opt):
|
||||
# /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/ (runtime state)
|
||||
#
|
||||
# Host files (firewall, wg0.conf) are rendered from the same host/*.template
|
||||
# files the manual setup uses — no duplicated config in ansible/.
|
||||
|
||||
- name: RackNerd VPN hub
|
||||
hosts: vpnhub
|
||||
become: true
|
||||
|
||||
vars:
|
||||
vpnhub_base_dir: /opt
|
||||
vpnhub_stack_dir: "{{ vpnhub_base_dir }}/vpnhub"
|
||||
vpnhub_force_rebuild: false
|
||||
public_iface: "{{ ansible_default_ipv4.interface }}"
|
||||
wg_peers_configured: >-
|
||||
{{ (troch_wg_public_key | default('')) != ''
|
||||
and (oswald_wg_public_key | default('')) != '' }}
|
||||
|
||||
handlers:
|
||||
- name: reload firewall
|
||||
ansible.builtin.shell: |
|
||||
netfilter-persistent reload
|
||||
# the reload flushes Docker's chains — put them back
|
||||
if systemctl is-active --quiet docker; then systemctl restart docker; fi
|
||||
|
||||
- name: restart wireguard
|
||||
ansible.builtin.systemd_service:
|
||||
name: wg-quick@wg0
|
||||
state: restarted
|
||||
when: wg_peers_configured | bool
|
||||
|
||||
- name: restart systemd-resolved
|
||||
ansible.builtin.shell: systemctl try-restart systemd-resolved || true
|
||||
|
||||
- name: rerun technitium init
|
||||
ansible.builtin.command: docker compose up -d --build --force-recreate technitium-init
|
||||
args:
|
||||
chdir: "{{ vpnhub_stack_dir }}"
|
||||
|
||||
- name: recreate duo-authproxy
|
||||
ansible.builtin.command: docker compose up -d --force-recreate duo-authproxy
|
||||
args:
|
||||
chdir: "{{ vpnhub_stack_dir }}"
|
||||
|
||||
- name: recreate openvpn
|
||||
ansible.builtin.command: docker compose up -d --force-recreate openvpn
|
||||
args:
|
||||
chdir: "{{ vpnhub_stack_dir }}"
|
||||
|
||||
- name: recreate stack
|
||||
ansible.builtin.command: docker compose up -d --force-recreate
|
||||
args:
|
||||
chdir: "{{ vpnhub_stack_dir }}"
|
||||
|
||||
tasks:
|
||||
# ── Docker engine ──────────────────────────────────────────────────
|
||||
- name: Docker engine
|
||||
tags: [host, docker]
|
||||
block:
|
||||
- name: Prerequisite packages
|
||||
ansible.builtin.apt:
|
||||
name: [ca-certificates, curl, gnupg]
|
||||
state: present
|
||||
update_cache: true
|
||||
cache_valid_time: 3600
|
||||
|
||||
- name: Docker apt signing key
|
||||
ansible.builtin.get_url:
|
||||
url: https://download.docker.com/linux/ubuntu/gpg
|
||||
dest: /etc/apt/keyrings/docker.asc
|
||||
mode: "0644"
|
||||
|
||||
- name: Docker apt repository
|
||||
ansible.builtin.apt_repository:
|
||||
repo: >-
|
||||
deb [arch={{ 'arm64' if ansible_architecture == 'aarch64' else 'amd64' }}
|
||||
signed-by=/etc/apt/keyrings/docker.asc]
|
||||
https://download.docker.com/linux/ubuntu
|
||||
{{ ansible_distribution_release }} stable
|
||||
filename: docker
|
||||
|
||||
- name: Docker packages
|
||||
ansible.builtin.apt:
|
||||
name:
|
||||
- docker-ce
|
||||
- docker-ce-cli
|
||||
- containerd.io
|
||||
- docker-buildx-plugin
|
||||
- docker-compose-plugin
|
||||
state: present
|
||||
|
||||
- name: Docker enabled and running
|
||||
ansible.builtin.systemd_service:
|
||||
name: docker
|
||||
enabled: true
|
||||
state: started
|
||||
|
||||
# ── Host: packages, sysctl, firewall ───────────────────────────────
|
||||
- name: WireGuard + iptables-persistent
|
||||
tags: host
|
||||
ansible.builtin.apt:
|
||||
name: [wireguard, iptables-persistent]
|
||||
state: present
|
||||
environment:
|
||||
DEBIAN_FRONTEND: noninteractive
|
||||
|
||||
- name: Enable IPv4 forwarding
|
||||
tags: host
|
||||
ansible.posix.sysctl:
|
||||
name: net.ipv4.ip_forward
|
||||
value: "1"
|
||||
sysctl_file: /etc/sysctl.d/99-vpnhub.conf
|
||||
reload: true
|
||||
|
||||
# Technitium binds 0.0.0.0:53 — systemd-resolved's stub listener on
|
||||
# 127.0.0.53:53 would collide with it.
|
||||
- name: resolved drop-in directory
|
||||
tags: host
|
||||
ansible.builtin.file:
|
||||
path: /etc/systemd/resolved.conf.d
|
||||
state: directory
|
||||
mode: "0755"
|
||||
|
||||
- name: Disable systemd-resolved stub listener (frees :53 for Technitium)
|
||||
tags: host
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/systemd/resolved.conf.d/99-vpnhub.conf
|
||||
mode: "0644"
|
||||
content: |
|
||||
[Resolve]
|
||||
DNSStubListener=no
|
||||
notify: restart systemd-resolved
|
||||
|
||||
- name: Point /etc/resolv.conf at resolved's upstream file (not the dead stub)
|
||||
tags: host
|
||||
ansible.builtin.file:
|
||||
src: /run/systemd/resolve/resolv.conf
|
||||
dest: /etc/resolv.conf
|
||||
state: link
|
||||
force: true
|
||||
|
||||
- name: Firewall rules (rendered from host/rules.v4/v6.template)
|
||||
tags: [host, firewall]
|
||||
ansible.builtin.copy:
|
||||
dest: "/etc/iptables/rules.{{ item }}"
|
||||
mode: "0640"
|
||||
content: "{{ lookup('file', playbook_dir ~ '/../host/rules.' ~ item ~ '.template')
|
||||
| replace('${PUBIF}', public_iface) }}\n"
|
||||
loop: [v4, v6]
|
||||
notify: reload firewall
|
||||
|
||||
# ── WireGuard (host-level wg-quick, key generated in place) ────────
|
||||
- name: Generate WireGuard private key (first run only)
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.shell: umask 077 && wg genkey > /etc/wireguard/wg0.key
|
||||
args:
|
||||
creates: /etc/wireguard/wg0.key
|
||||
|
||||
- name: Read WireGuard private key
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.slurp:
|
||||
src: /etc/wireguard/wg0.key
|
||||
register: wg_key_raw
|
||||
no_log: true
|
||||
|
||||
- name: Derive WireGuard public key
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.command: wg pubkey
|
||||
args:
|
||||
stdin: "{{ wg_key_raw.content | b64decode | trim }}"
|
||||
register: wg_pubkey
|
||||
changed_when: false
|
||||
no_log: true
|
||||
|
||||
- name: Hub public key — add as a peer at BOTH sites (docs/SITE-SIDE.md)
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.debug:
|
||||
msg: "RackNerd WG public key: {{ wg_pubkey.stdout }}"
|
||||
|
||||
- name: Deploy wg0.conf (rendered from host/wg0.conf.example)
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.copy:
|
||||
dest: /etc/wireguard/wg0.conf
|
||||
mode: "0600"
|
||||
content: "{{ lookup('file', playbook_dir ~ '/../host/wg0.conf.example')
|
||||
| replace('<RACKNERD_WG_PRIVATE_KEY>', wg_key_raw.content | b64decode | trim)
|
||||
| replace('<TROCHALAKIS_WG_PUBLIC_KEY>', troch_wg_public_key | default('<TROCHALAKIS_WG_PUBLIC_KEY>', true))
|
||||
| replace('<OSWALD_WG_PUBLIC_KEY>', oswald_wg_public_key | default('<OSWALD_WG_PUBLIC_KEY>', true)) }}\n"
|
||||
no_log: true
|
||||
notify: restart wireguard
|
||||
|
||||
- name: Enable wg-quick@wg0
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.systemd_service:
|
||||
name: wg-quick@wg0
|
||||
enabled: true
|
||||
state: started
|
||||
when: wg_peers_configured | bool
|
||||
|
||||
- name: Waiting on site public keys
|
||||
tags: [host, wireguard]
|
||||
ansible.builtin.debug:
|
||||
msg: >-
|
||||
troch_wg_public_key / oswald_wg_public_key not set in group_vars —
|
||||
wg0 deployed but NOT started. Fill them in and re-run.
|
||||
when: not (wg_peers_configured | bool)
|
||||
|
||||
- name: Apply pending host changes before touching the stack
|
||||
tags: [host, docker, firewall, wireguard, stack]
|
||||
ansible.builtin.meta: flush_handlers
|
||||
|
||||
# ── Compose stack under /opt ───────────────────────────────────────
|
||||
- name: Service directories
|
||||
tags: stack
|
||||
ansible.builtin.file:
|
||||
path: "{{ item }}"
|
||||
state: directory
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0755"
|
||||
loop:
|
||||
- "{{ vpnhub_stack_dir }}"
|
||||
- "{{ vpnhub_base_dir }}/openvpn"
|
||||
- "{{ vpnhub_base_dir }}/duo-authproxy"
|
||||
- "{{ vpnhub_base_dir }}/technitium"
|
||||
|
||||
# File-by-file on purpose: /opt/technitium/config is Technitium's live
|
||||
# state (zones DB) — a recursive dir copy must never touch it.
|
||||
- name: Sync technitium init-job files
|
||||
tags: stack
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/../technitium/{{ item }}"
|
||||
dest: "{{ vpnhub_base_dir }}/technitium/{{ item }}"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
loop:
|
||||
- Dockerfile
|
||||
- configure.sh
|
||||
notify: rerun technitium init
|
||||
|
||||
- name: Sync duo-authproxy service files (incl. certs/ if populated)
|
||||
tags: stack
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/../duo-authproxy/"
|
||||
dest: "{{ vpnhub_base_dir }}/duo-authproxy/"
|
||||
owner: root
|
||||
group: root
|
||||
notify: recreate duo-authproxy
|
||||
|
||||
- name: Sync openvpn service files (incl. pki/ if populated)
|
||||
tags: stack
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/../openvpn/"
|
||||
dest: "{{ vpnhub_base_dir }}/openvpn/"
|
||||
owner: root
|
||||
group: root
|
||||
notify: recreate openvpn
|
||||
|
||||
- name: Tighten OpenVPN key material permissions
|
||||
tags: stack
|
||||
ansible.builtin.shell: >-
|
||||
find {{ vpnhub_base_dir }}/openvpn/pki -type f -name '*.key'
|
||||
-exec chmod 600 {} + 2>/dev/null; true
|
||||
changed_when: false
|
||||
|
||||
- name: Deploy compose file
|
||||
tags: stack
|
||||
ansible.builtin.copy:
|
||||
src: "{{ playbook_dir }}/../docker-compose.yml"
|
||||
dest: "{{ vpnhub_stack_dir }}/docker-compose.yml"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0644"
|
||||
|
||||
- name: Render .env from group_vars
|
||||
tags: stack
|
||||
ansible.builtin.template:
|
||||
src: templates/env.j2
|
||||
dest: "{{ vpnhub_stack_dir }}/.env"
|
||||
owner: root
|
||||
group: root
|
||||
mode: "0600"
|
||||
notify: recreate stack
|
||||
|
||||
- name: Build and start the stack
|
||||
tags: stack
|
||||
community.docker.docker_compose_v2:
|
||||
project_src: "{{ vpnhub_stack_dir }}"
|
||||
state: present
|
||||
build: "{{ 'always' if vpnhub_force_rebuild | bool else 'policy' }}"
|
||||
pull: "{{ 'always' if vpnhub_force_rebuild | bool else 'policy' }}"
|
||||
remove_orphans: true
|
||||
@@ -0,0 +1,26 @@
|
||||
# Managed by Ansible — edit ansible/group_vars/vpnhub.yml and re-run
|
||||
# site.yml instead of touching this file.
|
||||
|
||||
TROCH_DC1_IP={{ troch_dc1_ip }}
|
||||
TROCH_DC2_IP={{ troch_dc2_ip }}
|
||||
OSWALD_DC_IP={{ oswald_dc_ip }}
|
||||
|
||||
DUO_IKEY={{ duo_ikey }}
|
||||
DUO_SKEY={{ duo_skey }}
|
||||
DUO_API_HOST={{ duo_api_host }}
|
||||
|
||||
DUO_SVC_USER={{ duo_svc_user }}
|
||||
DUO_SVC_PASSWORD={{ duo_svc_password }}
|
||||
AD_SEARCH_DN={{ ad_search_dn }}
|
||||
DUO_SECURITY_GROUP_DN={{ duo_security_group_dn | default('') }}
|
||||
|
||||
RADIUS_SECRET={{ radius_secret }}
|
||||
|
||||
TECHNITIUM_ADMIN_PASSWORD={{ technitium_admin_password }}
|
||||
|
||||
DUO_AUTHPROXY_VERSION={{ duo_authproxy_version | default('latest') }}
|
||||
|
||||
# /opt layout — consumed by compose ${*_DIR} interpolation
|
||||
OPENVPN_DIR={{ vpnhub_base_dir }}/openvpn
|
||||
DUO_DIR={{ vpnhub_base_dir }}/duo-authproxy
|
||||
TECHNITIUM_DIR={{ vpnhub_base_dir }}/technitium
|
||||
Reference in new issue
Block a user