The 'Add aggressive blocklists' task notified a handler that didn't exist
in main.yml, causing deployment failure. Removed the notify since the
gravity update is already handled by the subsequent task.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- cloudflared: create dedicated service account, run as non-root with
ProtectSystem=strict and full hardening directives
- cloudflared: skip credentials overwrite when existing tunnel has valid
credentials on disk; delete and recreate if credentials are missing
- wireguard: add systemd drop-in with ProtectHome, ProtectClock,
ProtectHostname, ProtectKernelLogs, PrivateTmp
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
When an existing Cloudflare tunnel was found, the script would overwrite
the credentials file with an empty TunnelSecret, breaking the service.
Now validates existing credentials and only recreates the tunnel if the
credentials file is missing or invalid.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Smart TLS: skip certbot for IPs/.local/.lan, self-signed with SAN,
HSTS max-age=0 for self-signed certs, split LAN vs public messages
- Dynamic PHP: versionless meta-packages, runtime detection via php_ver fact
- Vaultwarden: fail-fast on armv7l (32-bit ARM not supported upstream)
- Module prompts: accept IPs for matrix/cloud/vault/media, hard error
on email with IP, all_in_one skips certbot email for LAN
- Matrix: skip matrix. prefix strip for IPs, warn about immutable server_name
- OS family guards: ansible_os_family == Debian on all apt tasks
- SSH key path: expanduser().resolve() on user-provided key paths
- Cloudflare Tunnel: post-deploy script (setup-tunnel.sh) using CF API
token — no browser auth needed, creates tunnel + credentials + DNS + systemd
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
software-properties-common removed — dropped from Debian 13 (trixie).
unattended-upgrades and apt-listchanges moved to optional task with
ignore_errors so missing packages don't fail the entire deployment.
When deploying to existing servers as non-root, Ansible needs
become_password for sudo. Now prompts for it during credential
gathering and passes it via ansible_become_password in the
inventory (file chmod 0600). Blank input = NOPASSWD sudo assumed.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Root cause: regex_replace chains to extract subnet base from CIDR
weren't working in Ansible shell blocks, producing 10.66.66.0/24.2/32
instead of 10.66.66.2/32 — invalid CIDR that wg-quick can't parse.
Fix: replace fragile regex with simple string split via set_fact
(_subnet_base = wg_subnet.split('.')[0:3] | join('.')). Use echo
statements for lines needing shell expansion (PrivateKey, peer keys)
and quoted heredoc for static Jinja2-rendered lines (PostUp/PostDown).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- wg-quick runs under set -e; any non-zero exit kills the service
- 2>/dev/null only hides stderr, doesn't change exit code — need || true
- Split PostUp back to multiple lines (one command per line, cleaner)
- Use Jinja2 vars instead of shell heredoc for interface/subnet values
(quoted heredoc 'CONFEOF' prevents shell expansion issues, PrivateKey
written separately via echo since it needs shell expansion)
- Use -m conntrack --ctstate instead of deprecated -m state --state
- Add config dump debug task to capture generated config on failure
- Separate heredoc markers (CONFEOF/CLIENTEOF) to avoid conflicts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- ip6tables nat commands now redirect stderr to /dev/null so missing
ip6table_nat module doesn't fail PostUp and kill the service
- Pre-load ip6table_nat kernel module (ignore_errors for minimal kernels)
- Collapse PostUp/PostDown to single-line semicolon-chained commands
(avoids any heredoc whitespace or multi-line parsing issues)
- Remove comments from inside wg0.conf config body
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add IPv6 ip6tables rules to prevent IPv6 traffic leaks (forwarding was
enabled but only IPv4 masquerade existed)
- Tighten FORWARD chain: only allow wg0→internet and established return,
instead of blanket ACCEPT from wg0
- Scope NAT masquerade to VPN subnet only
- Block VPN clients from server-local services (SSH/80/443) via INPUT rules
- Set UFW DEFAULT_FORWARD_POLICY=ACCEPT (required for VPN routing; base
hardening sets default deny which blocks forwarded packets)
- Add SaveConfig = false to prevent runtime state overwriting config
- Add reload ufw handler
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
VPN/DNS/Vault → nanode/t3.micro, Matrix → g6-standard-2/t3.medium,
Cloud/Media/Email → g6-standard-1/t3.small, All-in-One → g6-standard-2/t3.medium.
Previously all services used the same provider-level default (often pulled
from c2itall vars), which meant a WireGuard VPN would default to g6-standard-2.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>