Shaun Arman f188c046ed Initial commit vor 5 Tagen
..
inventory f188c046ed Initial commit vor 5 Tagen
playbooks f188c046ed Initial commit vor 5 Tagen
roles f188c046ed Initial commit vor 5 Tagen
scripts f188c046ed Initial commit vor 5 Tagen
CLAUDE.md f188c046ed Initial commit vor 5 Tagen
README.md f188c046ed Initial commit vor 5 Tagen
ansible.cfg f188c046ed Initial commit vor 5 Tagen
site.yml f188c046ed Initial commit vor 5 Tagen

README.md

nginx-hardening

Ansible project to harden an NGINX reverse proxy to a production security posture. Applies security headers, TLS hardening, HTTP→HTTPS redirects, fail2ban jails, and nftables-based country geo-blocking — without modifying any existing service configurations.

Target environment

  • OS: RHEL 9 / Rocky Linux 9 / AlmaLinux 9
  • NGINX: 1.20+ with existing service configs in /etc/nginx/conf.d/
  • EPEL: Must be installed before running (dnf install -y epel-release)
  • nftables: Installed but not required to be running (managed by this project)
  • firewalld: Should be inactive to avoid nftables coexistence issues

What it does

Role: nginx_hardening

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

File Purpose
00-security-headers.conf server_tokens off, HSTS, X-Frame-Options, X-Content-Type-Options, CSP, rate-limit zone
00-ssl-params.conf TLS 1.2/1.3 only, hardened cipher suite, OCSP stapling, session timeout
00-proxy-params.conf Strips X-Powered-By/Server, sets X-Real-IP and X-Forwarded-* headers
00-http-redirects.conf Port-80 → HTTPS 301 redirects for services listed in nginx_redirect_services

No existing service configs are modified.

Role: fail2ban

Installs fail2ban from EPEL and configures three jails:

Jail Log Trigger
sshd /var/log/secure Failed SSH logins
nginx-4xx /var/log/nginx/access.log Repeated 4xx responses
nginx-auth /var/log/nginx/access.log Repeated 401/403 responses

Role: geo_blocking

Builds a standalone table inet geo_block nftables ruleset populated with CIDRs for every country except the US, downloaded from ipdeny.com. The table is loaded at boot via /etc/sysconfig/nftables.conf.

Prerequisites

On the Ansible control node (the machine you run ansible-playbook from):

# Ansible itself
pip install ansible-core
# or
dnf install -y ansible-core

On the target host (applied automatically by the playbooks):

  • EPEL repo must already be installed
  • SSH access with a user that can sudo

Setup

1. Configure your inventory

Edit inventory/hosts.yml:

all:
  hosts:
    nginx-proxy:
      ansible_host: 192.168.1.10          # your server's IP or hostname
      ansible_user: your_ssh_user
      # ansible_ssh_private_key_file: ~/.ssh/id_rsa

2. Configure HTTP→HTTPS redirects

Edit roles/nginx_hardening/defaults/main.yml and populate nginx_redirect_services with any services that are missing a port-80 redirect in their existing NGINX config:

nginx_redirect_services:
  - name: myapp
    server_name: myapp.example.com
  - name: dashboard
    server_name: dashboard.example.com

Services that already have a redirect in their existing conf.d/ file should not be listed here.

3. (Optional) Tune defaults

All tunable variables live in each role's defaults/main.yml:

Variable Default Description
nginx_hsts_max_age 31536000 HSTS max-age in seconds
nginx_rate_limit_req_zone 30r/m Rate limit zone definition
nginx_client_max_body_size 10m Max upload body size
fail2ban_bantime 3600 Ban duration (seconds)
fail2ban_maxretry_ssh 5 SSH failures before ban
fail2ban_maxretry_nginx_auth 5 401/403 failures before ban

Running

# Full hardening (all 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 (run periodically — ipdeny.com updates regularly)
ansible-playbook -K playbooks/update_geo_blocks.yml

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

-K prompts for the sudo password. Omit it if your user has passwordless sudo.

Geo-blocking: servers without direct internet access

If your target server cannot reach ipdeny.com, pre-download the zone files on a machine that can and copy them over:

# On a machine WITH unrestricted internet access:
./scripts/download-geo-zones.sh /tmp/geo_zones

# Copy to the target server:
rsync -av --no-group /tmp/geo_zones/ user@your-server:/opt/geo_zones/

# Run the playbook pointing at the local cache:
ansible-playbook -K playbooks/geo_blocking.yml -e geo_zone_files_dir=/opt/geo_zones

To make the cache path permanent, add it to your inventory:

all:
  hosts:
    nginx-proxy:
      ansible_host: 192.168.1.10
      ansible_user: your_ssh_user
      geo_zone_files_dir: /opt/geo_zones

Unblocking a country

Set blocked: false for the desired country code in roles/geo_blocking/defaults/main.yml, then re-run update_geo_blocks.yml.

Verification

After a successful run:

# NGINX config is valid
sudo nginx -t

# Security headers are present
curl -sI https://your-domain.com | grep -i 'strict\|x-frame\|x-content'

# HTTP redirects to HTTPS
curl -I http://your-domain.com   # expect: 301 Moved Permanently

# fail2ban jails are active
sudo fail2ban-client status
sudo fail2ban-client status nginx-4xx

# nftables geo-block table is loaded
sudo nft list table inet geo_block

Files written to the target host

Path Action
/etc/nginx/conf.d/00-security-headers.conf Created
/etc/nginx/conf.d/00-ssl-params.conf Created
/etc/nginx/conf.d/00-proxy-params.conf Created
/etc/nginx/conf.d/00-http-redirects.conf Created
/etc/fail2ban/jail.local Created
/etc/fail2ban/filter.d/nginx-4xx.conf Created
/etc/fail2ban/filter.d/nginx-auth.conf Created
/etc/nftables.d/geo-block.nft Created
/etc/sysconfig/nftables.conf Appended (include line)

All tasks that write files use backup: yes — a timestamped copy is created automatically before each overwrite.