Honeypots & threat-based banning

Turning predictable attacker probes into instant long bans, without writing a single line of honeypot code.

4 min read · verified against [email protected]

A FastAPI app doesn't have /wp-login.php. A Python service doesn't have /phpmyadmin. Anyone hitting these routes is fingerprinting your stack with known scanner playbooks.

The probe itself is the signal

Probe = hostile. Single access. No threshold needed. No need for volume. The URL itself is the smoking gun. This is qualitatively different from rate-limit thinking; there's no "how often" to argue about.

Common scanner targets (all are honeypots in the example app):

/wp-login.php · /admin.php - WordPress admin probes /phpmyadmin - DB admin panel probes /.env · /.env.production - config exfiltration /.git/config - repo exfiltration /.aws/credentials - cloud key exfiltration /server-status - Apache status leak

Threat-based ban vs rate-limit ban

Both end in a ban. They get there by counting different things.

Rate-limit banThreat-based ban
Triggertoo many requests in windowN detections of a specific attack class
Counterrequests per IP, regardless of contentdetections per IP, per class
Knobauto_ban_threshold + auto_ban_durationthreat_ban_config["<class>"]
Use casenoisy traffic of any shapeknown-bad signals; honeypots

A single high-confidence signal can outweigh thousands of borderline ones. That's the whole game.

There's no honeypot code path

Honeypots work because the route's path matches detection patterns. Same pipeline as any other threat detection - no special-casing:

GET /.env → path matches file-inclusion patterns → detection emitted with file-inclusion context → counter increments → ban if > threshold

Honeypots are an emergent property of routes whose paths happen to match patterns the detector is already looking for. Define more honeypot routes by adding more decorators - no library changes needed.

Four detection groups, four config keys

Detection patterns are organized into four groups. Each group has one corresponding key in threat_ban_config.

Detection groupExample patternsthreat_ban_config key
SQLi' OR 1=1 --, UNION SELECT ..., '; DROP TABLE"sqli"
Command injection; rm -rf /, $(curl http://...), `whoami`"cmd_injection"
SSRFhttp://localhost, http://169.254.169.254, file://"ssrf"
File inclusion../../etc/passwd, /admin.php, /.env, /wp-login.php"file_inclusion"

Honeypot paths all fall in the file-inclusion group. That's the only key that matters for honeypot-driven bans.

threat_ban_config - threshold and duration per class

A dict of class-name → policy. Each entry independent. Keys you omit silently disable threat-based ban for that class. Detections still flow to the dashboard, but no auto-ban kicks in.

threat_ban_config={
    "sqli":           {"threshold": 2, "duration": 7200},   # 2 hits → 2h ban
    "cmd_injection":  {"threshold": 1, "duration": 86400},  # 1 hit  → 24h ban
    "ssrf":           {"threshold": 2, "duration": 7200},
    "file_inclusion": {"threshold": 2, "duration": 7200},
}

threshold - number of detections (of this class, from this IP) before the ban fires. Counter is bounded. Entries age out on the counter's TTL. Not a lifetime tally. threshold=1 means one detection = ban.

duration - ban length in seconds. Fixed. No exponential backoff. A second ban is the same length as the first.

Honeypot routes - stacked decorators on one handler

FastAPI stacks decorators on a single function. The body is irrelevant. What matters is that the path matches detection patterns the middleware already scans for.

@app.get("/admin.php")
@app.get("/wp-login.php")
@app.get("/wp-admin/admin-ajax.php")
@app.get("/phpmyadmin")
@app.get("/server-status")
@app.get("/.env")
@app.get("/.env.production")
@app.get("/.git/config")
@app.get("/.aws/credentials")
async def honeypot(request: Request):
    return {"trapped": True, "path": request.url.path}

Adding a new honeypot path is one line: another @app.get(...) above the function. No middleware change, no config change. Response body is noise - return 200 with bait JSON, 404, or 418 - equivalent for security purposes.

Honeypots want file_inclusion threshold = 1

With threshold=2: GET /admin.php → detection → counter=1, no ban. GET /.env → detection → counter=2, BAN. Two free probes.

With threshold=1: GET /admin.php → detection → counter=1, BAN. First probe is the last.

If you have legacy paths that might trigger file-inclusion patterns, keep threshold=2, or rename them first.

Troubleshooting

SymptomLikely causeFix
Detections show in dashboard but no ban appliedDetection's class isn't a key in threat_ban_config - detection-only mode for that classAdd the missing key with appropriate threshold and duration
Real users hit /admin.php (legacy redirect)A path you still serve was added to the honeypot stackMove legitimate traffic to a different route first. Never honeypot a path that has real users.
Internal scanners and monitoring keep tripping honeypotsVulnerability scanners and uptime monitors probe common pathsWhitelist scanner IPs, or point internal monitoring at a route that bypasses the middleware