No description
- Jinja 100%
| group_vars/all | ||
| host_vars | ||
| playbooks | ||
| roles | ||
| .gitignore | ||
| ansible.cfg | ||
| inventory.ini | ||
| README.md | ||
| site.yml | ||
Homelab Ansible
Ansible configuration for a Proxmox VE homelab. VM provisioning lives in a separate Terraform repo; this repo manages everything inside the guest OSes.
Architecture
Networks (VLAN-segmented)
| Subnet | VLAN | Purpose |
|---|---|---|
| 10.100.30.0/24 | 30 | management — Ansible, SSH, admin access |
| 10.100.20.0/24 | 20 | services — internal service-to-service (Caddy → backends) |
| 10.50.0.0/24 | 2 | LAN — client-facing (Caddy serves here) |
| 10.100.40.0/24 | 40 | DMZ |
| 10.50.100.0/24 | — | VPN — VPN clients |
Hosts
| Name | VMID | Mgmt IP | Other IPs | Role |
|---|---|---|---|---|
| enterprise | — | 10.100.30.10 | — | Proxmox VE host (bare metal) |
| unbound | 100 | 10.100.30.2 | LAN 10.50.0.2, DMZ 10.100.40.2 | Recursive DNS (DNSSEC) |
| mailgw | 101 | 10.100.30.12 | — | Postfix outbound relay → SES |
| caddy | 102 | 10.100.30.13 | — | Reverse proxy + internal CA |
| forgejo | 103 | 10.100.40.11 | — | Git forge |
All guests are Debian 13 (Trixie) VMs. DNS names (*.ringzero.dev) are served
internally by unbound and externally point at Caddy.
Repo layout
ansible.cfg # inventory, roles_path, vault_password_file, become
inventory.ini # guests group: unbound, mailgw, caddy, forgejo
site.yml # runs baseline + all service playbooks
group_vars/
all/admin_keys.yml # admin SSH public keys deployed to all guests
host_vars/
<host>/vars.yml # per-service non-secret config
<host>/vault.yml # per-service secrets (ENCRYPTED)
unbound/network.yml # unbound multi-homed network config (policy routing)
playbooks/
baseline.yml # OS baseline applied to all guests
caddy.yml
forgejo.yml
mailgw.yml
unbound.yml
ops/
unbound-netapply.yml # safely activate unbound policy routing (see below)
roles/
baseline/
caddy/
forgejo/
mailgw/
unbound/
Services
baseline (playbooks/baseline.yml, all guests)
- Waits for SSH after VM boot.
- Installs base packages (
sudo curl vim htop locales). - Generates
en_US.UTF-8locale. - Deploys admin SSH keys to the
ansibleuser. - Installs and enables chrony (time sync).
- Strict SSH hardening drop-in: no root login, key auth only.
mailgw (Postfix outbound relay → Amazon SES)
- debconf pre-seeded so install is non-interactive.
- Templated
main.cf: relays to SES via TLS+SASL, envelope rewriting (normalize sender to domain, catch-all redirect to Gmail). mynetworkstightened to actual subnets, explicit relay/client restrictions, HELO required, VRFY disabled.- SES credentials in
host_vars/mailgw/vault.yml(encrypted).
unbound (recursive DNS + DNSSEC)
- Drop-in configs (server, remote-control, DNSSEC trust anchor).
- Internal A records templated from
unbound_recordsinhost_vars/unbound/vars.yml. - Split-horizon: internal names served locally; everything else recurses publicly.
- Multi-homed policy routing: each of the three NICs has its own routing
table so replies leave via the arrival interface. Managed via
interfaces.j2+rt_tables.j2. Activate changes withplaybooks/ops/unbound-netapply.yml(console open — see below).
caddy (reverse proxy + internal CA)
- Installed from the official Caddy apt repo (cloudsmith).
- Templated
Caddyfile;tls internalfor all proxied services. - CA persistence: internal CA root cert+key captured into
host_vars/caddy/vault.ymlon first run and restored on rebuild — clients never have to re-trust after a rebuild. - forgejo-ssh socat forwarder: forwards LAN-IP:22 → forgejo:2222 so git-over-SSH works on the standard port.
- Uses
ssh.socketactivation (pinned to mgmt IP) to free up :22 on the LAN IP for socat.
forgejo (git forge)
- Installed from pinned binary (
forgejo_version+forgejo_sha256). - Templated
app.ini. Secrets (INTERNAL_TOKEN, JWT, LFS_JWT) are self-bootstrapping: generated on first run if absent, written to vault and encrypted automatically.
Common procedures
Routine
ansible all -m ping # health check
ansible-playbook playbooks/baseline.yml # re-apply baseline to all guests
ansible-playbook playbooks/<service>.yml # re-apply a single service
ansible-playbook site.yml # re-apply everything
Always dry-run first: --check --diff
Update Forgejo
- Edit
host_vars/forgejo/vars.yml: bumpforgejo_versionandforgejo_sha256.curl -sL https://codeberg.org/forgejo/forgejo/releases/download/vX.Y.Z/forgejo-X.Y.Z-linux-amd64.sha256 ansible-playbook playbooks/forgejo.yml
Add an admin SSH key
Add the public key to group_vars/all/admin_keys.yml, then:
ansible-playbook playbooks/baseline.yml
Add an internal DNS record
Add to unbound_records in host_vars/unbound/vars.yml, then:
ansible-playbook playbooks/unbound.yml
Apply unbound network changes (RISKY)
ansible-playbook playbooks/unbound.yml— writes interfaces file, prints a reminder if policy routing is not yet live.- Have console access ready (
qm terminal 100on the Proxmox host). ansible-playbook playbooks/ops/unbound-netapply.yml— sanity-checks the config, runsifreload -a, asserts mgmt/lan/dmz source rules are present, and verifies an internal record resolves.- If it fails, recover via the open console.
Secrets / vault
- Encrypted files:
host_vars/mailgw/vault.yml,host_vars/forgejo/vault.yml,host_vars/caddy/vault.yml. - Edit:
ansible-vault edit <file>(uses~/.vault_passautomatically). - Vault password is in your password manager; never in the repo.
Rebuild procedures
VMs are provisioned by the Terraform repo (separate). Once a VM is up and
the ansible user exists via cloud-init, run:
Single VM
ansible-playbook playbooks/baseline.yml --limit <name>
ansible-playbook playbooks/<name>.yml
# unbound only:
ansible-playbook playbooks/ops/unbound-netapply.yml
Full recovery (all VMs from scratch)
Provision unbound first — all others depend on it for DNS.
# Bring up unbound first
ansible-playbook playbooks/baseline.yml --limit unbound
ansible-playbook playbooks/unbound.yml
ansible-playbook playbooks/ops/unbound-netapply.yml
# Bring up the rest
ansible-playbook playbooks/baseline.yml --limit mailgw,caddy,forgejo
ansible-playbook playbooks/mailgw.yml
ansible-playbook playbooks/caddy.yml
ansible-playbook playbooks/forgejo.yml
Forgejo enrollment window
forgejo_disable_registration defaults to true. On a fresh instance:
- Set to
falseinhost_vars/forgejo/vars.yml. - Run
ansible-playbook playbooks/forgejo.yml. - Self-enroll your first account via the web UI (becomes admin).
- Set back to
trueand re-run to lock.
Security model
- SSH: key-only, no root, hardening drop-in on all guests (baseline role). Proxmox host SSH is managed manually.
- Network segmentation: guests on management VLAN only for Ansible/SSH; service traffic on separate VLANs.
- Secrets: ansible-vault (AES256); password outside the repo.
- Mail relay: tightened mynetworks; outbound-only; SES credentials in vault.
TODO
- Proxmox VM backups — vzdump jobs for all VMs (real safety net).
- Forgejo data backup — tar
/var/lib/forgejobefore binary upgrades. - Mail notification wiring — route cron/smartd alerts through mailgw.
- Cloudflare Tunnel — external access; would retire the socat forwarder.