An open API service indexing awesome lists of open source software.

https://github.com/gistrec/nginx-scanner-trap

Turn an nginx box into a honeypot for vulnerability scanners: the first probe of /.env or /.git gets the bot banned on all ports via fail2ban + nftables. One script, verified install.
https://github.com/gistrec/nginx-scanner-trap

bash bot-detection debian devops fail2ban hardening honeypot intrusion-prevention linux nftables nginx security server-security sysadmin ubuntu vulnerability-scanner web-security

Last synced: 3 days ago
JSON representation

Turn an nginx box into a honeypot for vulnerability scanners: the first probe of /.env or /.git gets the bot banned on all ports via fail2ban + nftables. One script, verified install.

Awesome Lists containing this project

README

          

# nginx-scanner-trap

**English** · [Русский](README.ru.md)

[![CI](https://github.com/gistrec/nginx-scanner-trap/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/gistrec/nginx-scanner-trap/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/gistrec/nginx-scanner-trap)](https://github.com/gistrec/nginx-scanner-trap/releases) ![Platform](https://img.shields.io/badge/platform-Debian%20%7C%20Ubuntu-blue) [![License](https://img.shields.io/github/license/gistrec/nginx-scanner-trap)](LICENSE)

Turn an nginx box into a honeypot for vulnerability scanners — and ban them automatically with **fail2ban + nftables**. One script, works out of the box.

Bots probe every public IP for `/.env`, `/.git/config`, `/wp-login.php`, `/phpmyadmin`… around the clock. Each is a harmless 404, but in bulk it's wasted load and noise that buries real 404s in your alerts. `setup-honeypot.sh` makes the **first** such request cost the bot its access to the whole host.

> Full write-up — the why and the how, step by step (in Russian): https://gistrec.cloud/blog/nginx-honeypot-fail2ban/ru/

## How it works

1. A scanner requests a path no real client ever asks for: `/.env`, `/.git/config`, `/backup.sql`, …
2. nginx answers with a plain 404 and writes one line to `/var/log/nginx/honeypot.log`.
3. fail2ban watches that log with `maxretry = 1` — the first probe is enough.
4. nftables blocks the IP on **all** ports, SSH included. Repeat offenders get longer bans, up to a week.

The log line from step 2 looks like this:

```
203.0.113.7 - [04/Jul/2026:03:12:45 +0000] "GET /.env HTTP/1.1"
```

## What the script sets up

Everything below is idempotent — re-running the script is safe.

- **A honeypot `log_format`** (http context) that records the client IP, the timestamp, and the request line — so you can see *what* was probed.
- **`honeypot.conf`** — traps paths that only scanners request: `/.env`, `/.git`, `/.aws`, `/.ssh`, `config.php.bak`, `backup.sql`. Logs the probe, returns 404.
- **Aggressive extras, opt-in** via `--aggressive`: `wp-login.php`, `xmlrpc.php`, `phpmyadmin`. Off by default — on WordPress or phpMyAdmin hosts these are legitimate paths.
- **`deny-dotfiles.conf`** — 404 for every other dotfile, with an exception for `/.well-known/` so Let's Encrypt renewals keep working.
- **Wiring** — both snippets go into every `server { }` in `sites-enabled/`. Backups land outside the config tree, and if `nginx -t` fails the script rolls everything back on its own.
- **A fail2ban jail** — `maxretry=1`, `nftables-allports`, incremental bans. The script verifies that the jail loaded, that it watches the honeypot log, and — via a synthetic ban of a TEST-NET IP — that bans actually reach nftables.
- **A whitelist that keeps you out of your own trap** — the script detects your IP, validates it, asks for extras, and stops on an empty whitelist: proceeding takes an explicit confirmation, or `--allow-no-whitelist` in `-y` mode.

## Requirements

Debian/Ubuntu, nginx already installed, root access. The script installs `fail2ban` and `nftables` itself if they're missing.

## Quick start

> ⚠️ The script runs as root and edits nginx + fail2ban configs. **Read it before you run it** — never pipe a random script into `sudo bash` blind.

Step 1 — download:

```bash
curl -fsSLO https://raw.githubusercontent.com/gistrec/nginx-scanner-trap/main/setup-honeypot.sh
```

Step 2 — verify it against the published checksum of the current `main`:

```bash
echo "d7bd727335602b57ea5b250d03c53ca5 setup-honeypot.sh" | md5sum -c -
# → setup-honeypot.sh: OK

# or the stronger one:
sha256sum setup-honeypot.sh
# expected: 2c34338c3973b3edf8df565eb8d5122d33d66c1aa2ed3d61166b65ee6f2eff23
```

> The checksums change with every script update — CI fails if the published sums drift from the script. A hash served from the same repo only catches transport corruption, not a tampered repo — so still read the script.

Step 3 — read it:

```bash
less setup-honeypot.sh
```

Step 4 — run it (interactive: confirms your IP, asks for extra whitelist entries):

```bash
sudo bash setup-honeypot.sh
```

Prefer a preview first? `--dry-run` shows every change without making any:

```bash
sudo bash setup-honeypot.sh --dry-run
```

One-liner — only if you trust it:

```bash
curl -fsSL https://raw.githubusercontent.com/gistrec/nginx-scanner-trap/main/setup-honeypot.sh | sudo bash
```

## Options

| flag | meaning |
|------|---------|
| `--ip ` | your admin IP (skip auto-detection) |
| `--extra ""` | extra IPs/CIDRs to whitelist (space-separated) |
| `--aggressive` | also trap `wp-login.php` / `phpmyadmin` / `xmlrpc.php` (not for WordPress/phpMyAdmin hosts) |
| `--no-wire` | don't touch `sites-enabled`; print the include lines instead |
| `--allow-no-whitelist` | proceed even if no admin IP gets whitelisted (risky) |
| `--bantime ` | base ban time (default `86400`; accepts `1h`/`1d`/`1w`) |
| `--maxtime ` | max incremental ban (default `1w`) |
| `-y, --yes` | assume yes, no prompts |
| `--dry-run` | show what would happen, change nothing |
| `-h, --help` | show this help and exit |

## Safety

- **Idempotent** — re-running never duplicates includes; the script rewrites its managed files in place.
- **Backups outside nginx** — every touched file gets a copy under `/var/backups/nginx-scanner-trap//`, away from the config tree where `include sites-enabled/*` could load a stray `.bak`.
- **`nginx -t` with rollback** — if wiring breaks the config, the script restores every file it created or modified and aborts.
- **Won't lock you out** — the whitelist gets your IP (detected via `SSH_CONNECTION` → `SSH_CLIENT` → `who am i` → `ss`, then validated), anything you add, and always `127.0.0.1/8 ::1`. With an empty whitelist the script stops and asks for an explicit confirmation; in `-y` mode it refuses unless you pass `--allow-no-whitelist`.
- **Scoped fail2ban changes** — only `ignoreip` goes into `jail.local` `[DEFAULT]`, so how other jails such as `sshd` ban stays untouched; the script never overwrites an existing `jail.local`.

## Behind a CDN / reverse proxy

If nginx sits behind Cloudflare or any reverse proxy, configure [`real_ip`](https://nginx.org/en/docs/http/ngx_http_realip_module.html) (`set_real_ip_from` + `real_ip_header`) **before** enabling the trap. Otherwise `$remote_addr` holds the proxy's address, and fail2ban bans your proxy or CDN instead of the scanner.

## Check / unban

```bash
sudo fail2ban-client status nginx-honeypot
sudo nft list table inet f2b-table
sudo fail2ban-client set nginx-honeypot unbanip # one IP
sudo fail2ban-client unban --all # everything
```

A healthy jail looks like this:

```
Status for the jail: nginx-honeypot
|- Filter
| |- Currently failed: 0
| |- Total failed: 23
| `- File list: /var/log/nginx/honeypot.log
`- Actions
|- Currently banned: 17
|- Total banned: 23
`- Banned IP list: 203.0.113.7 198.51.100.23 …
```

## Uninstall

Everything the script manages is listed below; removal is four steps. Order matters — un-wire the includes first, or `nginx -t` fails on missing snippets.

```bash
# 1. Un-wire: drop the two include lines from every server block
# (--follow-symlinks keeps the sites-enabled symlinks intact)
sudo sed -i --follow-symlinks \
'\#include /etc/nginx/snippets/honeypot.conf;#d; \#include /etc/nginx/snippets/deny-dotfiles.conf;#d' \
/etc/nginx/sites-enabled/*

# 2. Delete the managed nginx files, validate, reload
sudo rm -f /etc/nginx/conf.d/honeypot-log.conf \
/etc/nginx/snippets/honeypot.conf \
/etc/nginx/snippets/deny-dotfiles.conf
sudo nginx -t && sudo systemctl reload nginx

# 3. Stop the jail (this also clears its nftables rules), delete its config
sudo fail2ban-client stop nginx-honeypot
sudo rm -f /etc/fail2ban/jail.d/nginx-honeypot.conf \
/etc/fail2ban/filter.d/nginx-honeypot.conf
sudo fail2ban-client reload

# 4. Optional: the honeypot log and the backups
sudo rm -f /var/log/nginx/honeypot.log
sudo rm -rf /var/backups/nginx-scanner-trap
```

If you wired the includes into files outside `sites-enabled/` (the `--no-wire` path), remove those two lines there as well.

Deliberately left in place:

- `/etc/fail2ban/jail.local` — if the script created it, its `[DEFAULT]` `ignoreip` is a global whitelist that other jails (e.g. `sshd`) may now rely on. Remove it only if nothing else needs it.
- The `fail2ban` / `nftables` packages — other jails may be using them.

Heads-up: without `deny-dotfiles.conf`, dotfiles (`/.git`, `/.env`, …) are reachable again unless your vhosts block them some other way.

## License

[MIT](LICENSE) © Aleksandr Kovalko