This repository has no description
PHP 69%
Shell 23%
6%
Dockerfile 2%

README.md

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.

Dashboard

How it works #

  1. Web server rewrite rules (nginx or Apache) redirect unauthenticated requests to /wp-login.php and /wp-admin to wp-trap.php
  2. The fake login page looks identical to a real WordPress login (same CSS, same error messages, same headers)
  3. 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
  4. Progressive tarpit delays: 2s, 4s, 6s... up to 30s per attempt (configurable)
  5. 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