No description
Find a file
2026-05-27 19:58:30 -05:00
group_vars/all replaced hacked ansible bootstrap with proper terraform tennant 2026-05-25 02:43:02 -05:00
host_vars hardened forgejo 2026-05-27 19:58:30 -05:00
playbooks updated forgejo ssh config 2026-05-25 16:58:37 -05:00
roles hardened forgejo 2026-05-27 19:58:30 -05:00
.gitignore Initial commit 2026-05-21 02:59:37 +00:00
ansible.cfg updated some stuff 2026-05-22 05:29:03 +00:00
inventory.ini cleaned up 2026-05-27 11:17:59 -05:00
README.md cleaned up 2026-05-27 11:17:59 -05:00
site.yml replaced hacked ansible bootstrap with proper terraform tennant 2026-05-25 02:43:02 -05:00

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-8 locale.
  • Deploys admin SSH keys to the ansible user.
  • 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).
  • mynetworks tightened 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_records in host_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 with playbooks/ops/unbound-netapply.yml (console open — see below).

caddy (reverse proxy + internal CA)

  • Installed from the official Caddy apt repo (cloudsmith).
  • Templated Caddyfile; tls internal for all proxied services.
  • CA persistence: internal CA root cert+key captured into host_vars/caddy/vault.yml on 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.socket activation (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

  1. Edit host_vars/forgejo/vars.yml: bump forgejo_version and forgejo_sha256.
    curl -sL https://codeberg.org/forgejo/forgejo/releases/download/vX.Y.Z/forgejo-X.Y.Z-linux-amd64.sha256
    
  2. 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)

  1. ansible-playbook playbooks/unbound.yml — writes interfaces file, prints a reminder if policy routing is not yet live.
  2. Have console access ready (qm terminal 100 on the Proxmox host).
  3. ansible-playbook playbooks/ops/unbound-netapply.yml — sanity-checks the config, runs ifreload -a, asserts mgmt/lan/dmz source rules are present, and verifies an internal record resolves.
  4. 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_pass automatically).
  • 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:

  1. Set to false in host_vars/forgejo/vars.yml.
  2. Run ansible-playbook playbooks/forgejo.yml.
  3. Self-enroll your first account via the web UI (becomes admin).
  4. Set back to true and 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/forgejo before binary upgrades.
  • Mail notification wiring — route cron/smartd alerts through mailgw.
  • Cloudflare Tunnel — external access; would retire the socat forwarder.