Honeypot
An alert-only watcher that tails the Caddy access log and flags scanner traffic.
pocket-homeserver ships an optional, alert-only honeypot: a small native
Python watcher that tails the core Caddy access log, flags high-confidence
scanner probes (requests for /.env, /.git, /wp-login.php, /phpmyadmin,
shell-upload endpoints, …), and writes a JSONL ledger that the web admin panel
renders as a Security console.
It is off by default. Enable it with ENABLE_HONEYPOT=true in .env.
What it is — and what it is not
- No inbound listener, no new attack surface. The watcher does not open a socket and does not change Caddy. It only reads the access log that Caddy already writes. Cloudflare’s WAF / Bot Fight Mode filters most junk before it ever reaches your tunnel, so the ledger is usually quiet — that is normal.
- Alert-only by default. Out of the box the watcher does exactly one thing:
append matched probes to the ledger. Matrix alerts and Cloudflare edge
blocking are both opt-in (see below) and both off until you create the
relevant
0600files under${DATA_DIR}/secrets. - Native, not in the userland. Like the admin panel, the watcher runs Termux-native because it tails the host-side Caddy log and may call the Cloudflare API / the loopback Matrix API. It is stdlib-only Python.
- Defensive only. Nothing the honeypot does ever sends traffic to a source IP. The strongest action it can take is to add an IP Access Rule on your own Cloudflare account edge — and only if you explicitly opt in.
Enabling it
-
Set in
.env:ENABLE_HONEYPOT=true -
(Re-)run the installer. The honeypot step self-gates on the flag:
./pocket.sh # menu → Install (or: bash scripts/install.sh --force)The step
scripts/steps/77-install-honeypot.sh:- seeds
${DATA_DIR}/secrets/honeypot.mode=alert(0600) if absent, - seeds a
${DATA_DIR}/secrets/honeypot-safelist.txttemplate (0600), - touches the ledger
${POCKET_LOG_DIR}/honeypot.log(0600), - supervises the watcher (recorded in state so
start-stack.shre-supervises it on every bring-up, surviving a reboot).
- seeds
-
Open the admin panel → Security (
/honeypot). Hits appear there as the watcher records them. The console also exposes per-IP drill-down, passive enrichment (RDAP / reverse-DNS / offline geo / threat-intel deep-links), and an abuse-report draft generator.
The ledger
The ledger is the source of truth and always works (the admin console reads it directly):
${POCKET_LOG_DIR}/honeypot.log # one JSON object per line (JSONL), chmod 600
Each line records the timestamp (UTC), client IP, host, request path, the matched detection rule, the HTTP status Caddy returned, and any action taken.
The safelist
${DATA_DIR}/secrets/honeypot-safelist.txt (0600) lists IPs/CIDRs the watcher
never alerts on or blocks. Loopback and all Cloudflare edge ranges are built
into the watcher already; use this file for your own egress / known-good
addresses — one IPv4/IPv6 address or CIDR per line, # comments allowed:
# my home/office egress
203.0.113.10
2001:db8:1234::/48
You can also add an IP to the safelist from the admin console (the safelist action on a per-IP page), which appends an annotated line here.
Optional: Matrix alerts
By default the watcher is ledger-only. To also post alerts to a Matrix room,
create the file ${DATA_DIR}/secrets/honeypot-alert.env (0600) with:
HP_MATRIX_HS=http://127.0.0.1:8448 # your homeserver base URL (loopback is fine)
HP_MATRIX_TOKEN=syt_your_bot_access_token # an access token for a bot/alert account
HP_MATRIX_ROOM=!yourRoomId:example.com # the room to post alerts into
chmod 600 "$DATA_DIR/secrets/honeypot-alert.env"
Then restart the watcher (admin panel → Security → restart watcher, or
bash scripts/ops/restart.sh honeypot-watcher). The token is read from the
0600 file inside the supervisor and is never placed on a process command
line. If the file or any of the three variables is absent, Matrix alerting is
silently skipped — the ledger still records everything.
Alerts are posted via the standard Matrix client-server API
(PUT /_matrix/client/v3/rooms/{room}/send/m.room.message/{txn}), and throttled
per source IP so a noisy scanner cannot flood the room.
Optional: offline geo / ASN enrichment
By default ledger hits carry no geo/ASN data. You can enable offline enrichment — country, ASN, and an advisory hosting/datacenter flag — by deploying the free DB-IP lite datasets (CC-BY 4.0). It is computed entirely locally (no live IP-intel lookups, nothing sent to the source), and it is purely advisory: geo never affects detection, the safelist, or blocking.
Drop the two *.csv.gz files into scripts/honeypot/geo/ (the datasets are not
shipped — they are large and regenerable) and restart the watcher. The download
URLs, expected filenames, the required CC-BY attribution, and a one-liner refresh
command are in scripts/honeypot/geo/README.md.
With no dataset present the watcher never imports the geo module and every lookup
is a no-op, so the ledger record is byte-identical to a geo-less run. Once a
dataset is in place, the Security console shows top countries / ASNs and a
hosting-ASN ratio, and per-IP pages show the network owner.
Optional: Cloudflare edge blocking (triple-gated, off by default)
Blocking is the most powerful action, so it is triple-gated — all three must be true before the watcher will challenge or block a source IP:
- Mode.
${DATA_DIR}/secrets/honeypot.modecontainschallengeorblock(the default, and the only value the installer writes, isalert). - Opt-in marker. The file
${DATA_DIR}/secrets/honeypot-allow-blockingexists (any content). This means a tampered mode file alone can never enable mass-blocking — an attacker with write access to your secrets would also have to create this marker. - Token-scope self-check. The Cloudflare API token in
${DATA_DIR}/secrets/cf-honeypot.envpasses the watcher’s over-scope tripwire: it must be scoped to onlyAccount → Firewall Access Rules → Editand nothing more. An over-privileged token is refused.
Provisioning the Cloudflare credentials
Create ${DATA_DIR}/secrets/cf-honeypot.env (0600):
CF_API_TOKEN=your_scoped_token
CF_ACCOUNT_ID=your_account_id
chmod 600 "$DATA_DIR/secrets/cf-honeypot.env"
Scoping the token to ONLY Firewall Access Rules: Edit
In the Cloudflare dashboard:
- My Profile → API Tokens → Create Token → Create Custom Token.
- Under Permissions, add exactly one row:
Account·Account Firewall Access Rules·Edit. - Under Account Resources, scope it to the single account you use.
- Add nothing else — no Zone permissions, no DNS, no other account permissions. The watcher’s self-check refuses a token that carries any extra scope.
- Create the token, copy it into
cf-honeypot.env, and put your account ID in the same file.
The over-scope self-check is deliberate defence-in-depth: even if the secrets directory were compromised, a correctly-scoped token can do nothing beyond adding/removing IP Access Rules, and a stolen broad token would be rejected by the watcher rather than used.
Turning blocking on
With cf-honeypot.env provisioned and the honeypot-allow-blocking marker
created, set the mode and restart the watcher:
printf 'challenge\n' > "$DATA_DIR/secrets/honeypot.mode" # or: block
chmod 600 "$DATA_DIR/secrets/honeypot.mode"
touch "$DATA_DIR/secrets/honeypot-allow-blocking"
bash scripts/ops/restart.sh honeypot-watcher
challengeadds an IP Access Rule in managed_challenge mode — real humans can still pass an interstitial, automated scanners fail. CGNAT-safe.blockhard-refuses every request from the IP at the edge (heavier; a shared / CGNAT address would take collateral).
Both add exactly one rule per source IP, tagged honeypot-auto in its notes,
on your own account edge. To keep the edge ruleset from growing without bound,
the watcher ships a --reap mode that prunes aged honeypot-auto rules — but
pocket-homeserver does not schedule it for you. Run python3 scripts/honeypot/honeypot-watcher.py --reap periodically (e.g. from your own
cron / termux-job-scheduler), or just remove rules on demand with the console’s
unblock action.
Unblocking / undo
From the admin console, open the per-IP page and use:
- unblock — deletes every
honeypot-autoIP Access Rule that targets that IP (challenge or block). Only rules the honeypot itself created are touched; your manual Cloudflare rules are never affected. - safelist — appends the IP to
honeypot-safelist.txtso it is never alerted on or auto-blocked again. (Note: safelisting does not remove an existing Cloudflare rule — run unblock as well if one is active.)
To turn blocking off entirely, set the mode back to alert-only and restart:
printf 'alert\n' > "$DATA_DIR/secrets/honeypot.mode"
rm -f "$DATA_DIR/secrets/honeypot-allow-blocking"
bash scripts/ops/restart.sh honeypot-watcher
Existing edge rules are not removed automatically — use the console’s unblock action (or the Cloudflare dashboard) to clear any you no longer want.
Optional: the rate-jail (fail2ban-style, off by default)
The scanner rules above match what a request is (a probe for /.env,
/wp-login.php, …). The rate-jail is a complementary rule that watches how a
single IP behaves over time: it is an auth-failure-burst detector — the
fail2ban pattern. It is off by default and adds no listener; it is just extra
logic in the same watcher, reading the same Caddy log.
RATE_JAIL_MODE=off # off | alert | enforce (default off)
off(default) — a strict no-op. The detector returns immediately; the watcher’s behaviour is byte-identical to a build without it.alert— when an IP trips the threshold, append arate-jailline to the ledger and (if Matrix alerts are configured) post a dedicated “auth-failure burst” alert. Never blocks.enforce— same asalert, and apply a Cloudflare managed-challenge via the same triple-gatedcf_blockpath the scanner rules use. Because it reuses that path,enforcesafely degrades to alert-only unless you have also opted into blocking (thehoneypot-allow-blockingmarker + a correctly-scoped token +cf-honeypot.env). Soenforcealone never starts challenging — the blocking opt-in is still required.
What trips it (low false-positive by design)
It counts only auth-failure responses — HTTP 401 / 403 / 429
(RATE_JAIL_STATUSES) — from one IP within a sliding window. Normal browsing
(2xx/3xx) is ignored entirely; it never jails on raw request volume. When an
IP produces RATE_JAIL_FAILS such responses inside RATE_JAIL_WINDOW seconds, it
trips once per RATE_JAIL_COOLDOWN (so a sustained burst ledgers/alerts once, not on
every failing request). The safelist (loopback + Cloudflare edge + your own entries)
is honoured, and the per-IP tracking dict is bounded (RATE_JAIL_IP_CAP,
defaults to the scanner IP_STATE_CAP) with oldest-failure eviction, so a
high-cardinality scanner cannot grow it without bound.
RATE_JAIL_WINDOW=300 # sliding window, seconds
RATE_JAIL_FAILS=12 # auth-fails within the window → trip
RATE_JAIL_STATUSES=401,403,429
RATE_JAIL_IP_CAP=<IP_STATE_CAP> # max tracked IPs (bounded)
RATE_JAIL_COOLDOWN=<ALERT_COALESCE> # re-trip throttle, seconds
Set the mode in .env (the setup.sh wizard offers alert when you enable the
jailer) and restart the watcher. rate-jail hits show up in the Security console
and the ledger like any other hit (hit_rule: "rate-jail", mode: "rate:<mode>").
Log-coverage caveat. The watcher tails the core Caddy access log, so the rate-jail only sees auth failures for services fronted by that Caddy edge. Failed logins on a backend that does its own logging elsewhere are not visible to it.
Disabling the honeypot
Set ENABLE_HONEYPOT=false in .env and stop the watcher:
bash scripts/ops/restart.sh honeypot-watcher # picks up nothing to start
# or stop it directly via the panel's Security console
The ledger, mode file, and safelist under ${DATA_DIR}/secrets are left in place
so you can re-enable later without reconfiguring.