Files

4.0 KiB
Raw Permalink Blame History

Ansible — dev_blog production host

Provisions the AlmaLinux 10 instance created by infra/ (Vultr) so it can run the Podman-managed dev-blog-app + dev-blog-caddy stack with:

  • Automatic security updates via dnf-automatic (upgrade_type=security, apply_updates=yes, reboot=never) plus a separate auto-reboot.timer that fires daily at 02:00 America/Chicago and reboots only when dnf needs-restarting -r says one is pending.
  • nftables as the host firewall (allow 22/80/443/tcp + 443/udp, rate-limited new SSH, policy drop everywhere else; firewalld masked).
  • fail2ban with the nftables-multiport banaction, watching the SSH journal and the Caddy / Coraza logs.
  • Podman + runtime deps (crun, netavark, aardvark-dns, slirp4netns, fuse-overlayfs, passt, container-selinux, skopeo, buildah) and the Quadlet units from ../quadlet/ (with the Image= lines rewritten to point at GHCR).
  • GHCR auth via the gh CLI: a PAT (ghcr_pat) is used to authenticate both gh and podman against ghcr.io, and the app/caddy images are pulled before the Quadlet units start.

Coraza → fail2ban → nftables wiring

[ Caddy + Coraza container ]
        │  writes to /var/log/caddy (bind-mounted from host)
        ▼
   /var/log/caddy/access.log         (Caddy JSON access log – status=403 lines)
   /var/log/caddy/coraza-audit.log   (Coraza serial audit log – any --A-- entry)
        │  tailed by
        ▼
[ fail2ban  jail: caddy-coraza ]
        │  banaction = nftables-multiport
        ▼
[ nftables  table f2b-table / set addr-set-caddy-coraza ]   →   packets dropped

Three repo files were updated to enable that wiring:

  • quadlet/dev-blog-caddy.container — adds Volume=/var/log/caddy:/var/log/caddy:Z.
  • caddy/Caddyfile — sends access logs to /var/log/caddy/access.log (JSON).
  • caddy/coraza.conf — enables SecAuditLog (serial) at /var/log/caddy/coraza-audit.log, only on relevant 4xx/5xx.

The fail2ban filter (roles/fail2ban/files/caddy-coraza.filter) matches either "remote_ip":"…","status":403 lines from the access log or --xxxx-A-- headers in the audit log. Either pattern produces a hit on <HOST>, so once a client crosses maxretry within findtime, fail2ban drops it into the f2b-table nft set for bantime.

Usage

cd ansible
cp inventory.yml.example inventory.yml   # set ansible_host = your Vultr IP

# Quick connectivity / fact gathering:
ansible -m ping blog

# Full provision:
ansible-playbook site.yml

# Just one role:
ansible-playbook site.yml --tags fail2ban   # (add `tags:` to roles if needed)

Tunables (group_vars/all.yml)

Var Default Notes
ssh_port 22
http_port / https_port 80 / 443
caddy_log_dir /var/log/caddy host path bind-mounted into Caddy
dnf_automatic_apply_updates true
dnf_automatic_upgrade_type security default for full updates
f2b_findtime / f2b_bantime / f2b_maxretry 10m / 1h / 5 Caddy/Coraza jail thresholds
ghcr_owner / ghcr_repo OWNER / dev_blog GHCR namespace
image_tag latest Tag to pull (CI sets to commit SHA)

Notes

  • Don't enable both firewalld and the nftables service — this playbook masks firewalld. Podman's netavark writes to its own nft tables and doesn't conflict.
  • The synchronize task uses ansible.posix.synchronize. Install ansible-galaxy collection install ansible.posix once on the controller.
  • After first run, verify with:
    • systemctl status dnf-automatic.timer nftables fail2ban
    • nft list ruleset
    • fail2ban-client status caddy-coraza