No description
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
pra 6a948bb283 Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO
- roles/wireguard (+ wireguard.yml, wireguard_relay.yml): WireGuard tunnel between a home
  gateway VM and a public relay VPS, so nginx_proxy_manager can complete Let's Encrypt's
  HTTP-01 challenge without exposing the home network or a router port-forward. Same role
  handles both ends via wireguard_forward_in_interface/out_interface. Relay VPS lives under a
  new external_hosts inventory group, kept out of proxmox_vms so site.yml never touches it.
- roles/nginx_proxy_manager: switched from jc21/nginx-proxy-manager to NPMplus
  (docker.io/zoeyvid/npmplus) — network_mode host, single /data volume, hardened
  cap-drop/no-new-privileges, and an opt-in AUTH_REQUEST_AUTHELIA_UPSTREAM env for Authelia's
  Auth Request integration.
- roles/authelia (+ authelia.yml): Authelia running alongside NPMplus on the proxy host —
  file-backed users/access-control for the Auth Request login-wall, plus a full OIDC provider
  (identity_providers.oidc) used for real SSO into Forgejo.
- New vault secrets (session/JWT/storage/OIDC HMAC + RSA key) in
  inventory/group_vars/all/vault.yml.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 02:31:58 +02:00
inventory Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
roles Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
.gitignore init 2026-08-23 22:09:45 +02:00
ansible.cfg init 2026-08-23 22:09:45 +02:00
apt_cache_server.yml Add update, apt-cache-server, and nginx-proxy-manager roles/playbooks 2026-08-24 22:27:17 +02:00
authelia.yml Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
common.yml init 2026-08-23 22:09:45 +02:00
docker.yml Add docker role to install Docker Engine and Portainer 2026-08-24 19:59:11 +02:00
hardening.yml init 2026-08-23 22:09:45 +02:00
nginx_proxy_manager.yml Add update, apt-cache-server, and nginx-proxy-manager roles/playbooks 2026-08-24 22:27:17 +02:00
README.md Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
site.yml Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
update.yml Add update, apt-cache-server, and nginx-proxy-manager roles/playbooks 2026-08-24 22:27:17 +02:00
wireguard.yml Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00
wireguard_relay.yml Add WireGuard tunnel, migrate to NPMplus, add Authelia SSO 2026-08-26 02:31:58 +02:00

proxmox-ansible

Ansible project for configuring VMs that run on Proxmox. Ansible does not create or manage the VMs in Proxmox itself — it connects over SSH to VMs that already exist (created however you provision them, e.g. from a template with cloud-init) and configures the guest OS from there. It also manages one piece of infrastructure outside Proxmox: a public relay VPS for the WireGuard tunnel (see inventory/hosts.yml's external_hosts group and the WireGuard section below) — kept separate from the proxmox_vms tree so it's never swept into site.yml's fleet-wide runs.

Only ansible-core (ansible.builtin.* modules) is used — no Ansible Galaxy collections.

Layout

ansible.cfg                      # inventory/roles path, vault password file, become defaults
site.yml                         # entry point — imports per-tier playbooks
apt_cache_server.yml             # applies role: apt_cache_server to the apt_cache_servers group
common.yml                       # applies role: common to every VM
hardening.yml                    # applies role: hardening to every VM
docker.yml                       # applies role: docker to the docker_hosts group
nginx_proxy_manager.yml          # applies role: nginx_proxy_manager to the proxy_hosts group
authelia.yml                     # applies role: authelia to the authelia_hosts group
wireguard.yml                    # applies role: wireguard (gateway) to the wireguard_hosts group
wireguard_relay.yml              # standalone — NOT imported by site.yml (targets external_hosts, run on demand)
update.yml                       # standalone — NOT imported by site.yml, run on demand
inventory/
  hosts.yml                      # proxmox_vms group + sub-groups, plus external_hosts for non-Proxmox infra
  group_vars/all/
    vars.yml                     # shared, non-secret variables
    vault.yml                    # secrets — see Secrets (ansible-vault) below
  host_vars/                     # per-host overrides, one file per host (e.g. apt-cache.yml)
roles/
  common/                        # baseline config applied to every VM
  hardening/                     # basic security baseline applied to every VM
  docker/                        # installs Docker + Portainer, applied to docker_hosts
  update/                        # full package update/upgrade, applied by update.yml
  apt_cache_server/              # installs apt-cacher-ng, applied to apt_cache_servers
  nginx_proxy_manager/           # runs NPMplus (nginx-proxy-manager fork), applied to proxy_hosts
  wireguard/                     # installs WireGuard, applied to both wireguard_hosts and wireguard_relay_hosts
  authelia/                      # runs Authelia (SSO/2FA), applied to authelia_hosts

As the fleet grows: add a new role under roles/, a matching top-level playbook (e.g. webservers.yml), and an import_playbook: line for it in site.yml. Group VMs by service in inventory/hosts.yml and point group/host vars at them as needed.

Prerequisites

  • ansible-core installed locally (e.g. sudo pacman -S ansible-core on Arch/CachyOS). No ansible-galaxy install step is needed — this project has no collection dependencies.
  • A dedicated SSH key for Ansible's connection, e.g.:
    ssh-keygen -t ed25519 -f ~/.ssh/proxmox_ansible_ed25519 -C ansible@proxmox
    
    Put the public half in authorized_keys for the ansible user on each VM (e.g. baked into your VM template / cloud-init). The private key path is referenced from inventory/group_vars/all/vars.yml and must never be committed to this repo.
  • The ansible user on each VM should ideally have passwordless sudo. If it doesn't, set vault_ansible_become_password (see below).

Secrets (ansible-vault)

Secrets live in inventory/group_vars/all/vault.yml, referenced from the plain inventory/group_vars/all/vars.yml via a vault_<name> variable (e.g. ansible_become_password: "{{ vault_ansible_become_password | default(omit) }}"). This keeps variable names readable in plain files while values stay encrypted.

vault.yml is currently a plaintext placeholder — encrypt it before putting real secrets in it or committing it:

# one-time: create a vault password and keep it out of git (.vault_pass is gitignored)
echo "$(openssl rand -base64 32)" > .vault_pass

ansible-vault encrypt inventory/group_vars/all/vault.yml

Day to day:

ansible-vault edit inventory/group_vars/all/vault.yml     # edit secrets
ansible-vault view inventory/group_vars/all/vault.yml     # view without editing

Because ansible.cfg sets vault_password_file = .vault_pass, ansible-playbook/ansible-vault pick it up automatically — no --ask-vault-pass needed.

Adding a VM

  1. Add it to inventory/hosts.yml under proxmox_vms (and any relevant sub-group).
  2. If it needs overrides, add inventory/host_vars/<hostname>.yml.
  3. Make sure the dedicated ansible SSH key is authorized on it, then run:
    ansible-playbook site.yml --limit <hostname>
    

Running

ansible-playbook site.yml

Package cache (apt-cacher-ng)

apt_cache_server.yml installs apt-cacher-ng on the apt_cache_servers group (see roles/apt_cache_server) — a caching proxy so the fleet's VMs don't each re-download the same apt packages from the internet.

The client side lives in roles/common: every VM gets /etc/apt/apt.conf.d/95proxy pointing apt at the cache, controlled by common_apt_cache_proxy. This repo sets it fleet-wide in inventory/group_vars/all/vars.yml to http://192.168.178.170:3142/ (the apt-cache host defined in inventory/hosts.yml) — set it to "" there to disable it for the whole fleet, or override per-group/host to opt specific VMs out.

site.yml imports apt_cache_server.yml before common.yml, so on a full ansible-playbook site.yml run the cache is already up by the time other VMs install packages.

Notes:

  • HTTPS repositories (e.g. Docker's, added by roles/docker) aren't cached and are explicitly set to bypass the proxy (Acquire::https::Proxy "DIRECT" in roles/common) — apt-cacher-ng rejects HTTPS tunnels by default, so without that line HTTPS repos fail outright instead of just going uncached. See roles/apt_cache_server's README for details.
  • The cache server's ufw port (3142/tcp) is opened via inventory/host_vars/apt-cache.yml — required because hardening applies a SSH-only ufw default fleet-wide.

Reverse proxy (NPMplus)

nginx_proxy_manager.yml runs NPMplus — a fork of Nginx Proxy Manager with HTTP/3, OIDC, Crowdsec support, and improved TLS defaults — as a Docker container on the proxy_hosts group (see roles/nginx_proxy_manager), currently just the proxy host (192.168.178.179). It requires Docker, so proxy is also in docker_hosts, and site.yml imports nginx_proxy_manager.yml after docker.yml. Unlike most containers in this fleet, it runs with --network host (NPMplus's own recommendation), so ufw actually governs access to it — Docker's usual iptables-bypass caveat doesn't apply here.

After the first run, finish NPMplus's own first-time setup at https://192.168.178.179:81 (note: https, not http) and add proxy hosts through its UI. Certificate/PKI automation beyond Let's Encrypt (a local CA so internal services get trusted TLS without manual per-service uploads) is a deliberate follow-up and isn't wired in yet — see roles/nginx_proxy_manager/README.md's caveats. That README also documents the one-way migration procedure from the original jc21 image, if you're moving an existing instance rather than starting fresh.

Single sign-on / 2FA (Authelia)

authelia.yml runs Authelia (see roles/authelia) on the proxy host itself, alongside NPMplus, using NPMplus's built-in "Auth Request" integration (nginx_proxy_manager_auth_request_authelia_upstream) — select "authelia (modern)" per proxy host in NPMplus's UI to put it behind a login (optionally requiring 2FA per user/group via authelia_access_control_rules), no custom nginx config needed. See roles/authelia/README.md for the full setup flow, including generating the first user's password hash (never stored as plaintext) and the three secrets this role needs in inventory/group_vars/all/vault.yml.

WireGuard tunnel (Let's Encrypt without exposing the home network)

Since Proxmox lives on a home network with no public IP, this repo manages both ends of a WireGuard tunnel to a public relay VPS, so NPM can complete Let's Encrypt's HTTP-01 challenge without forwarding router ports or depending on a DNS provider's API:

  • wireguard.yml runs WireGuard (see roles/wireguard) on a dedicated Proxmox VM (the wireguard_hosts group) that terminates the tunnel and relays the VPS's traffic on to the nginx_proxy_manager host.
  • wireguard_relay.yml runs the same role on a public VPS (the wireguard_relay_hosts group, under external_hostsnot a Proxmox VM, so it's excluded from proxmox_vms and never touched by site.yml) that relays its own public 80/443 into the tunnel toward the gateway.

Both sides generate their own keypair on first run (never leaving the host or passing through Ansible's vault) and print their public key for you to hand to the other side. Both use the same wireguard_forward_ports mechanism to bake DNAT/FORWARD/MASQUERADE iptables rules directly into the wg-quick config's PostUp/PostDown commands — just in opposite directions (wireguard_forward_in_interface/wireguard_forward_out_interface). See roles/wireguard/README.md for the full setup flow and caveats — including the NPM-sees-the-gateway's-IP tradeoff from MASQUERADE, and the Docker/DOCKER-USER gotcha if the relay VPS ever runs Docker (e.g. an existing reverse proxy already bound to 80/443 there will intercept traffic before this role's rules ever see it — use a VPS dedicated to just the tunnel).

Updating VMs

update.yml does a full package update/upgrade (see roles/update). It's standalone — not part of site.yml — so it only runs when you trigger it:

ansible-playbook update.yml                                    # rolling, one host at a time, no auto-reboot
ansible-playbook update.yml -e update_reboot_if_required=true   # also reboot hosts that need it
ansible-playbook update.yml -e update_serial=100%               # update all hosts in parallel instead
ansible-playbook update.yml --limit <hostname>                  # just one host