CLAUDE.md 4.1 KB

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

Target Environment

  • OS: RHEL 9.6, NGINX: 1.20.1 at /etc/nginx/, Ansible: ansible_connection: local
  • TLS certs: /etc/letsencrypt/live/tftsr.com-0001/{fullchain,privkey}.pem
  • Services proxied: 15 internal services on *.tftsr.com / tftsr.com
  • sudo dnf install -y ansible-core is required before first run (not managed by this project)

Run Commands

# Full hardening (all three roles)
ansible-playbook -K site.yml

# Individual roles
ansible-playbook -K playbooks/nginx_hardening.yml
ansible-playbook -K playbooks/fail2ban.yml
ansible-playbook -K playbooks/geo_blocking.yml

# Refresh country IP ranges from ipdeny.com (run periodically)
ansible-playbook -K playbooks/update_geo_blocks.yml

# Dry run — no changes applied
ansible-playbook -K --check site.yml

Architecture

Three independent roles, each runnable standalone via playbooks/:

nginx_hardening

Deploys four files to /etc/nginx/conf.d/ prefixed 00- so they sort before all service configs:

  • 00-security-headers.confserver_tokens off, HSTS, X-Frame-Options, rate-limit zone, client body size
  • 00-ssl-params.conf — TLS 1.2/1.3 only, cipher suite, OCSP stapling, resolver
  • 00-proxy-params.conf — strips X-Powered-By/Server, sets X-Real-IP/X-Forwarded-* headers
  • 00-http-redirects.conf — port-80 301 redirect server blocks for the 11 services that lack them

Critical constraint: Existing service configs in /etc/nginx/conf.d/ are never modified. The 4 services that already have HTTP→HTTPS redirects (keycloak-proxy, vault, ollama-api, vaultwarden) are not in nginx_redirect_services. Do not add ssl_session_cache to 00-ssl-params.conf — all service configs already declare shared:SSL:1m in their server blocks and a conflicting http-level declaration will break nginx -t.

fail2ban

Installs fail2ban from EPEL, deploys filter definitions and jail.local. Three jails:

  • sshd/var/log/secure
  • nginx-4xx/var/log/nginx/access.log (regex: any 4xx)
  • nginx-auth/var/log/nginx/access.log (regex: 401/403 only)

geo_blocking

Downloads per-country CIDR files from ipdeny.com/ipblocks/data/aggregated/{cc}-aggregated.zone at runtime, assembles them into a single nftables set, and loads a standalone table inet geo_block (does not touch any existing nftables rules). The include line is appended to /etc/sysconfig/nftables.conf. Downloads use ignore_errors: yes — missing zone files are silently skipped.

To unblock a country: set blocked: false for its entry in roles/geo_blocking/defaults/main.yml and re-run update_geo_blocks.yml.

ipdeny-absent territories (no zone file exists — permanently blocked: false, no IPs to block): BV, CX, EH, GS, HM, PN, SH, SJ, TF, XK.

DMZ host has no outbound internet — zone files must be pre-downloaded elsewhere and copied over:

# On a machine WITH internet access:
./scripts/download-geo-zones.sh /tmp/geo_zones
rsync -av /tmp/geo_zones/ sarman@dmz-host:/opt/geo_zones/

# Then run with the local cache:
ansible-playbook -K playbooks/geo_blocking.yml -e geo_zone_files_dir=/opt/geo_zones

The role does a fast 8-second HEAD check to ipdeny.com first; if it fails and geo_zone_files_dir is unset, the play fails immediately rather than timing out on all 238 countries.

YAML boolean trap: code: NO (Norway) is parsed as boolean false by PyYAML (YAML 1.1). It must stay quoted as code: "NO". Watch for this if adding new entries.

Key Design Decisions

  • All template/copy/lineinfile tasks use backup: yes — timestamped backups are created automatically on every run alongside the modified file.
  • The nft template opens with add table inet geo_block + flush table inet geo_block for idempotency (safe to re-run).
  • The geo_blocking role downloads zone files to a tempfile directory and cleans it up at the end of every run.
  • Handlers fire only when a task reports changed — NGINX reload and fail2ban restart are not triggered on idempotent re-runs.