- Jinja 100%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
- 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> |
||
| inventory | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| apt_cache_server.yml | ||
| authelia.yml | ||
| common.yml | ||
| docker.yml | ||
| hardening.yml | ||
| nginx_proxy_manager.yml | ||
| README.md | ||
| site.yml | ||
| update.yml | ||
| wireguard.yml | ||
| wireguard_relay.yml | ||
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-coreinstalled locally (e.g.sudo pacman -S ansible-coreon Arch/CachyOS). Noansible-galaxy installstep is needed — this project has no collection dependencies.- A dedicated SSH key for Ansible's connection, e.g.:
Put the public half inssh-keygen -t ed25519 -f ~/.ssh/proxmox_ansible_ed25519 -C ansible@proxmoxauthorized_keysfor theansibleuser on each VM (e.g. baked into your VM template / cloud-init). The private key path is referenced frominventory/group_vars/all/vars.ymland must never be committed to this repo. - The
ansibleuser on each VM should ideally have passwordless sudo. If it doesn't, setvault_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
- Add it to
inventory/hosts.ymlunderproxmox_vms(and any relevant sub-group). - If it needs overrides, add
inventory/host_vars/<hostname>.yml. - 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"inroles/common) — apt-cacher-ng rejects HTTPS tunnels by default, so without that line HTTPS repos fail outright instead of just going uncached. Seeroles/apt_cache_server's README for details. - The cache server's
ufwport (3142/tcp) is opened viainventory/host_vars/apt-cache.yml— required becausehardeningapplies a SSH-onlyufwdefault 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.ymlruns WireGuard (seeroles/wireguard) on a dedicated Proxmox VM (thewireguard_hostsgroup) that terminates the tunnel and relays the VPS's traffic on to thenginx_proxy_managerhost.wireguard_relay.ymlruns the same role on a public VPS (thewireguard_relay_hostsgroup, underexternal_hosts— not a Proxmox VM, so it's excluded fromproxmox_vmsand never touched bysite.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