pitcherplant #
A WordPress login honeypot and tarpit. Serves a pixel-perfect fake /wp-login.php page that logs attacker credentials, applies progressive delays, and feeds fail2ban for long-duration IP bans.
Your real login lives at a hidden URL (via WPS Hide Login or similar). Attackers hitting the default WordPress login paths get trapped.

How it works #
- Web server rewrite rules (nginx or Apache) redirect unauthenticated requests to
/wp-login.phpand/wp-admintowp-trap.php - The fake login page looks identical to a real WordPress login (same CSS, same error messages, same headers)
- Each login attempt is logged to two files:
- Simple log for fail2ban:
[timestamp] HONEYPOT: IP - attempt N - user=X - Full JSONL intelligence: credentials, headers, user agent, Cloudflare country, timing
- Simple log for fail2ban:
- Progressive tarpit delays: 2s, 4s, 6s... up to 30s per attempt (configurable)
- After 20 attempts (configurable), fail2ban bans the IP for 30 days via iptables
Logged-in administrators with valid WordPress cookies bypass the honeypot entirely.
Quick start #
# 1. Clone the repo
git clone <repo-url> pitcherplant && cd pitcherplant
# 2. Configure for your site
cp config.env.example config.env
# Edit config.env with your site name, URL, and target info
# 3. Deploy
./install.sh
Docker #
docker pull ghcr.io/cache8063/pitcherplant:latest
docker run -d -p 8080:80 ghcr.io/cache8063/pitcherplant:latest
Edit wp-trap-config.php inside the container to set your site name and URL. Logs are written to /var/log/wp-honeypot-intel.jsonl inside the container. The dashboard is available at /dashboard/ — set $dashboard_token in wp-trap-config.php first; the dashboard fails closed when the token is empty.
The container exposes /health (200 OK, no PHP) for docker/k8s healthchecks. The entrypoint runs php-fpm and nginx in parallel and exits if either crashes.
Install modes #
# Local (running on the WordPress server itself)
./install.sh --local /var/www/html
# Remote via SSH
./install.sh --ssh root@webserver /var/www/html
# Proxmox LXC container
./install.sh --pct root@proxmox-node 550 /var/www/html
# Auto-detect from config.env
./install.sh
Configuration #
Installer config (config.env) #
Copy config.env.example to config.env:
| Variable | Default | Description |
|---|---|---|
SITE_NAME |
My WordPress Site |
Shown on fake login page |
SITE_URL |
https://example.com |
Used in fake WP headers |
LOG_FILE |
/var/log/wp-honeypot.log |
fail2ban watches this |
INTEL_FILE |
/var/log/wp-honeypot-intel.jsonl |
Full intelligence log |
STATE_DIR |
/var/lib/wp-honeypot |
Per-IP state tracking |
MAX_DELAY |
30 |
Maximum tarpit delay (seconds) |
F2B_MAXRETRY |
20 |
Attempts before ban |
F2B_FINDTIME |
86400 |
Detection window (seconds) |
F2B_BANTIME |
2592000 |
Ban duration (30 days) |
Runtime config (wp-trap-config.php) #
These live in the deployed PHP config file. install.sh writes the first
group from config.env; edit the file in place to tune the rest.
| Variable | Default | Description |
|---|---|---|
$trusted_proxies |
loopback + CF v4/v6 | CIDRs whose REMOTE_ADDR can set forwarded IP headers. Anything outside the list falls back to REMOTE_ADDR. |
$max_field_len |
256 |
Bytes kept from submitted user/pass/redirect before logging. |
$dashboard_token |
'' (closed) |
Required token for /dashboard/. Empty value = dashboard returns 403. |
$dashboard_max_entries |
5000 |
Tail-window size for the dashboard. |
Intelligence viewer #
./tools/honeypot-intel.sh summary # Overview: top usernames, passwords, IPs
./tools/honeypot-intel.sh live # Real-time feed
./tools/honeypot-intel.sh creds # All username:password pairs
./tools/honeypot-intel.sh passwords # Password frequency ranking
./tools/honeypot-intel.sh usernames # Username frequency ranking
./tools/honeypot-intel.sh countries # Attacks by country (Cloudflare)
./tools/honeypot-intel.sh timeline # Hourly attack histogram
./tools/honeypot-intel.sh ip 1.2.3.4 # Drill down on specific IP
./tools/honeypot-intel.sh banned # Currently banned IPs
What gets logged #
Every POST (login attempt) records:
{
"timestamp": "2026-03-10T14:22:33+00:00",
"ip": "203.0.113.45",
"attempt": 5,
"username": "admin",
"password": "password123",
"remember_me": false,
"redirect_to": "/wp-admin/",
"headers": {
"user_agent": "Mozilla/5.0 ...",
"cf_ipcountry": "CN",
"cf_ray": "abc123"
},
"delay_applied": 10,
"country": "CN"
}
GET requests (reconnaissance) are also logged with URI, headers, and country.
Requirements #
- PHP 7.0+ (uses
??null-coalesce +hash_equals) - nginx or Apache 2.4+ with
mod_rewrite - fail2ban
- iptables (for banning)
- Python 3 (for the intel viewer)
- Cloudflare (optional, for country-level geo data)
The Docker image uses nginx + php-fpm on Alpine for a minimal footprint (~60MB).
Trusted proxies #
wp-trap.php only honors CF-Connecting-IP / X-Forwarded-For when REMOTE_ADDR is in $trusted_proxies. The shipped config trusts loopback + the published Cloudflare ranges. If you front the trap with a different reverse proxy, add its CIDR; otherwise attackers can spoof the forwarded header and have fail2ban ban the wrong IP. Refresh the Cloudflare ranges from https://www.cloudflare.com/ips-v4 and https://www.cloudflare.com/ips-v6 when they change.
Co-locating with WordPress (install.sh path) #
install.sh drops the trap next to WordPress on the host's PHP-FPM pool. Under a parallel-burst attack the 30s tarpit sleep() can pin every worker and DoS the real site through the trap. If that's a concern, provision a dedicated FPM pool for the trap with its own pm.max_children so worker exhaustion stays local. The Docker path runs an isolated FPM pool by default and is unaffected.
File layout #
pitcherplant/
├── install.sh # Deployment script (local / SSH / pct)
├── config.env.example # Installer config template
├── Dockerfile # php-fpm + nginx on Alpine (digest-pinned)
├── trap/
│ ├── wp-trap.php # Honeypot script (goes in WP root)
│ └── wp-trap-config.php # Runtime config template (goes in WP root)
├── fail2ban/
│ ├── filter.d/wp-honeypot.conf
│ ├── jail.d/wp-honeypot.conf
│ ├── jail.local
│ ├── logrotate-wp-honeypot # /etc/logrotate.d/wp-honeypot (daily, 14 keeps)
│ └── tmpfiles-wp-honeypot.conf # /etc/tmpfiles.d/wp-honeypot.conf (state-file GC, 30d)
├── apache/
│ ├── honeypot-rewrite.conf # .htaccess rewrites (cookie-anchored)
│ └── security-headers.conf # Bonus security headers
├── docker/
│ ├── nginx.conf # nginx site config (cookie-anchored, /health)
│ └── entrypoint.sh # php-fpm + nginx with wait -n
└── tools/
├── dashboard.php # Web-based intel viewer (token-gated)
└── honeypot-intel.sh # CLI intel viewer
Math #
With default settings (maxretry=20, delay=2s per attempt increment):
- Attempts 1-20: 2+4+6+8+10+12+14+16+18+20+22+24+26+28+30+30+30+30+30+30 = ~7 minutes of tarpit
- Then: 30-day iptables ban
License #
MIT