- HTML 98.3%
- Shell 1.7%
| knowledge | ||
| monitoring | ||
| nginx | ||
| scripts | ||
| templates | ||
| .gitignore | ||
| LICENSE | ||
| README.md | ||
▐ ▄ ▄▄ • ▪ ▐ ▄ ▐▄• ▄ ▄▄▄ ▄▄▄·
•█▌▐█▐█ ▀ ▪██ •█▌▐█ █▌█▌▪▀▄ █·▐█ ▄█
▐█▐▐▌▄█ ▀█▄▐█·▐█▐▐▌ ·██· ▐▀▀▄ ██▀·
██▐█▌▐█▄▪▐█▐█▌██▐█▌▪▐█·█▌▐█•█▌▐█▪·•
▀▀ █▪·▀▀▀▀ ▀▀▀▀▀ █▪•▀▀ ▀▀.▀ ▀.▀ == 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
sudoand 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 sudoersDefaults 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
- nginx HTTP Core Module
- ACME module · source
- Limit Request Module · Stream module
- mapcidr · tarampampam/error-pages
- Machine-readable knowledge bundle:
knowledge/(OKF)