nginx reverse proxy — automated HTTPS, rate limiting, threat blocklists, and security headers, with install/site-creation scripts and docs
  • HTML 98.3%
  • Shell 1.7%
Find a file
2026-06-24 20:15:31 +02:00
knowledge docs: add open-ports prerequisite; Ubuntu host (not necessarily a VM) 2026-06-22 16:32:10 +02:00
monitoring Initial commit: nginx reverse proxy config, scripts, and docs 2026-06-21 23:11:02 +02:00
nginx fix: comment out IPv6 listen in default.conf (match template, IPv4-only hosts) 2026-06-22 16:06:42 +02:00
scripts fix: start nginx when not running; guard reload against empty PID file 2026-06-22 16:36:08 +02:00
templates fix: add commented IPv6 listen to stream-service template 2026-06-22 16:09:04 +02:00
.gitignore Initial commit: nginx reverse proxy config, scripts, and docs 2026-06-21 23:11:02 +02:00
LICENSE Initial commit: nginx reverse proxy config, scripts, and docs 2026-06-21 23:11:02 +02:00
README.md README.md aktualisiert 2026-06-24 20:15:31 +02:00

 ▐ ▄  ▄▄ • ▪   ▐ ▄ ▐▄• ▄ ▄▄▄   ▄▄▄·
•█▌▐█▐█ ▀ ▪██ •█▌▐█ █▌█▌▪▀▄ █·▐█ ▄█
▐█▐▐▌▄█ ▀█▄▐█·▐█▐▐▌ ·██· ▐▀▀▄  ██▀·
██▐█▌▐█▄▪▐█▐█▌██▐█▌▪▐█·█▌▐█•█▌▐█▪·•
▀▀ █▪·▀▀▀▀ ▀▀▀▀▀ █▪•▀▀ ▀▀.▀  ▀.▀     ==  nginx-rp

A production-ready nginx reverse proxy for a plain Ubuntu VM. One install script sets up automatic SSL, rate limiting, an auto-refreshed threat blocklist, and friendly error pages.

  • Automatic SSL — Let's Encrypt via the native ACME module (no certbot)
  • Rate limiting — per-IP request + connection zones, trusted IPs exempt
  • Threat blocklist — daily-aggregated IP blocklist from public feeds
  • HTTP & stream — HTTP/HTTPS vhosts and raw TCP/UDP services, both scripted
  • Custom error pages — themed 4xx/5xx pages from tarampampam/error-pages

Using this repo as an agent? A machine-readable Open Knowledge Format bundle lives in knowledge/ — start there.

Prerequisites

  • An Ubuntu machine with sudo and a public IP
  • DNS for your domain(s) pointing at it
  • Ports 80 + 443 open and routed to the machine (HTTP + HTTPS/ACME)
  • Your public IP (exempted from rate limiting)

install.sh installs everything else — nginx (nginx.org mainline) + the ACME module, and Go + mapcidr for the blocklist.

Installation

One line — clones to /opt/nginx-rp, installs nginx + the ACME module, prompts for your public IP (the rate-limit exemption), then wires everything up:

sudo bash -c "$(curl -fsSL https://quelloffen.ch/shm0rt/nginx-rp/raw/branch/main/scripts/install.sh)"

Pass the script as an argument — not … | sudo bash. Piping breaks the interactive prompt on hosts with sudoers Defaults use_pty (the modern Ubuntu/Debian default). For an unattended install, set the IP up front: sudo PUBLIC_IP=203.0.113.10 bash -c "$(curl -fsSL …)".

Prefer a checkout? Clone first, then run the same script:

sudo git clone https://quelloffen.ch/shm0rt/nginx-rp /opt/nginx-rp
sudo /opt/nginx-rp/scripts/install.sh

Only the nginx/ subtree is symlinked into /etc/nginx; repo files (LICENSE, scripts/, templates/, knowledge/) never enter the live config, and per-host values stay in gitignored files — so git pull always updates cleanly.

Common Commands

Command What it does
sudo ./scripts/install.sh Full setup — packages, config, /etc/nginx symlinks, daily cron
sudo ./scripts/update.sh Pull latest, nginx -t, reload (clean fast-forward)
sudo ./scripts/create-site.sh <fqdn> <ip:port> Add an HTTP/HTTPS site
sudo ./scripts/create-site.sh --stream <name> <port> <ip:port> Add a TCP/UDP service
sudo ./scripts/update-smart-blocklist.sh Rebuild the threat blocklist now
sudo systemctl reload nginx Apply manual config edits
sudo ./scripts/install.sh uninstall Remove symlinks + cron, restore *.bak backups
sudo ./scripts/install.sh link / unlink (Re)create / remove only the symlinks

uninstall/unlink leave the nginx/Go/mapcidr packages installed. Each command is covered in detail in its own section below.

Adding an HTTP Site

create-site.sh fills in templates/http-site.conf, writes nginx/conf.d/<domain>.conf, runs nginx -t, and reloads on success. The upstream id defaults to the domain with dots turned into dashes.

sudo ./scripts/create-site.sh                                  # interactive prompts
sudo ./scripts/create-site.sh cloud.example.com 10.0.0.5:8080  # fqdn  backend

The generated vhost redirects HTTP→HTTPS, requests a certificate automatically, and includes the recommended feature set (SSL, proxy headers, security headers, error pages, rate limiting, blocklist). Edit nginx/conf.d/<domain>.conf afterwards to tune it; your vhosts are gitignored.

Stream (TCP/UDP) Services

For raw TCP/UDP proxying — databases, SMTP, SSH, game servers — nginx.conf has a stream {} block that includes stream.d/*.conf. Streams route by listen port (there is no server_name), so each service needs its own port.

The same generator with --stream fills in templates/stream-service.conf and writes nginx/stream.d/<name>.conf:

sudo ./scripts/create-site.sh --stream                              # interactive prompts
sudo ./scripts/create-site.sh --stream postgres 5432 10.0.0.5:5432  # name port backend
sudo ./scripts/create-site.sh --stream dns 53 10.0.0.5:53 --udp     # add --udp for UDP

Or copy the template into nginx/stream.d/ and edit by hand. The stream module is built into the nginx.org mainline package the installer uses; stream services are gitignored like vhosts.

Updating

sudo ./scripts/update.sh

Pulls the latest repo (clean fast-forward), runs nginx -t, and reloads. Because every per-host value lives in gitignored files (nginx/local/, your vhosts, stream services, and the blocklist), the tracked tree is never modified on the VM — so updates never hit a merge conflict. If a pull ever isn't a fast-forward, the script stops and leaves the tree untouched.

Smart Blocklist

scripts/update-smart-blocklist.sh rebuilds nginx/blocklist/smart-blocklist.txt from public threat-intelligence feeds, aggregates the CIDR ranges with mapcidr, writes them as nginx map entries, then reloads nginx. The installer schedules it via cron (daily, 06:00); run it anytime:

sudo /opt/nginx-rp/scripts/update-smart-blocklist.sh

Sources (one fetch each, then aggregated with mapcidr): Spamhaus DROP + DROPv6, Feodo Tracker, DShield block list, and Blocklist.de (apache). The generated file is gitignored and feeds the map $remote_addr $is_blocked block, consumed by block-by-ip-lists.conf (returns 444 on a match).

Custom entries: add your own to nginx/blocklist/custom-blocklist.txt (IP_OR_CIDR 1;, one per line):

1.2.3.4 1;
2.3.4.0/24 1;

Feature Reference

Composable snippets in nginx/features/, included from a vhost (see templates/http-site.conf). At runtime they resolve under /etc/nginx/features/.

Automatic SSL — acme-ssl.conf

Native ACME (no certbot): certificates are requested and renewed automatically. The issuer in nginx.conf uses the tls-alpn-01 challenge over TLSv1.3 and caches state in /var/cache/nginx/acme; the include points ssl_certificate at the ACME-managed cert (HSTS lives in security-headers.conf). Your contact email is the contact line in the acme_issuer block of nginx.conf.

Rate limiting — default-rate-limit.conf, login-rate-limit.conf

All zones key on $untrusted_ip, which is empty for trusted IPs (RFC1918, localhost, and your public IP via geo $is_trusted_ip) so they are never limited; exceeding a limit returns 429. nginx.conf defines four zones:

Zone Limit Snippet / use
normal_limit 30 r/s default-rate-limit.conf (burst=200 nodelay) — general traffic, applied site-wide by the template
login_limit 5 r/s login-rate-limit.conf (burst=10 nodelay) — wired to ^/(login|auth|signin|api/auth) in the template
strict_limit 2 r/s no snippet — add limit_req zone=strict_limit; to a location for sensitive endpoints
conn_limit per-IP conns no snippet — add limit_conn conn_limit <n>; to cap concurrent connections

strict_limit and conn_limit are defined and ready but not applied anywhere by default — opt in per location when you need them.

Security headers — security-headers.conf

HSTS, X-Frame-Options, X-Content-Type-Options, and Referrer-Policy on every response. Included in the site template.

Threat blocklist — block-by-ip-lists.conf

Returns 444 (connection closed, no response) to any IP in the smart blocklist or your custom list.

Custom error pages — error-pages.conf

Themed pages from tarampampam/error-pages (the lost-in-space theme) for 400 401 403 404 405 407 408 429 500 502 503 504. The error_page directives live in nginx.conf; error-pages.conf serves the files from the internal /error-pages/ location (alias /etc/nginx/error-pages/). Pages live in nginx/error-pages/ — edit the HTML/CSS to rebrand.

WebSocket & proxy headers — proxy-headers.conf, websocket.conf

proxy-headers.conf forwards Host, X-Real-IP, X-Forwarded-For/Proto/Host/Port and the $connection_upgrade-aware Connection header over proxy_http_version 1.1 — use on every proxied location. websocket.conf adds Upgrade: $http_upgrade and raises proxy_read_timeout/proxy_send_timeout to 1h for long-lived sockets.

Local-only — local-only.conf

Restricts a site or location to trusted networks (RFC1918 + localhost), returning 403 to everyone else. Useful for admin panels.

Default server (rickroll) — conf.d/default.conf

Catch-all for undefined domains and direct-IP scans: HTTP is redirected to a rickroll and HTTPS handshakes are rejected (ssl_reject_handshake on) so scanners can't enumerate hosted domains.

Monitoring — Prometheus exporter (opt-in)

Not installed by default. The nginx-prometheus-exporter lives in its own script:

sudo ./scripts/monitoring/prometheus-exporter.sh            # install
sudo ./scripts/monitoring/prometheus-exporter.sh uninstall  # remove

Install builds the exporter, enables monitoring/stub_status.conf (a localhost-only stub_status endpoint on 127.0.0.1:8080) by copying it into nginx/conf.d/, and runs a systemd service. Metrics are served on http://127.0.0.1:9113/metrics. uninstall reverses it (the binary is left in /usr/local/bin).

Documentation