Honeypot Layers
Where the deception happens: separate decoy backends, or one eshop with a swapped database
Two Layers of Deception
Both layers protect the same target demo application: a WordPress + WooCommerce storefront (a coffee eshop with real products and carts). The honeypot is built for and around that shop — the reverse-proxy level clones the whole WooCommerce backend per session, and the database level swaps only the WooCommerce database — so the deception is tuned to the same WordPress and WooCommerce surface an adversary would actually probe.
The ids stack can run its honeypot at one of two layers. Both are the same Compose
project — same project name, same .env, same data — and differ only in which Compose
file is used and, therefore, in where a suspicious session is diverted. You pick the layer
from the dashboard's Services page ("Honeypot layer"); the choice is persisted in
AppState.honeypot_layer and read by core.docker_ctl.Target, so every
docker compose call the dashboard makes follows it automatically.
Choosing a Layer
| Reverse-proxy level | Database level | |
|---|---|---|
| Compose file | docker-compose.yml | docker-compose.db-proxy.yml |
| What is switched per suspicious session | The whole backend (separate honeypot WordPress pools) | Only the database (one eshop, production vs honeypot DB) |
| Honeypot pools | Per-session pools, autoscaled | None — POOL_COUNT is 0 |
| Resource cost per pool | ~1536 MB / 1.5 CPU (eshop + database) | ~512 MB / 0.5 CPU (database alone) |
| Isolates file / upload / RCE attacks | Yes — the adversary runs against a real decoy | No — there is no separate backend to land in |
| Honeypot data volume | Per-pool databases | honeypot_database_proxy_data |
Both layers have been segmentation-validated: a ten-window run on each layer gave every flagged session a
honeypot pool of its own (verdict EXCLUSIVE). See Validation Results for
the recorded figures. The database-level run exercises only data-layer patterns and checks the shared
filesystem afterwards, because it has no separate backend to contain a file write (ids/db_proxy/COVERAGE.md).
The ~1536 MB / 1.5 CPU above is the hard container ceiling per reverse-proxy
pool (1,024 MB eshop + 512 MB database), and the scaling budget charges a deliberately
generous 768 MB per pool as headroom. At rest a pool uses far less: in the 2026-10-24 ten-window
run the pool containers peaked at a measured ~469 MB each (4,687 MB across 10
pools). Adding a pool took a median 27 s (max 40 s), dominated by the template copy, and a
diverted page load was as fast as production (median 372 ms vs 360 ms). See
Validation Results for the full figures.
Reverse-Proxy Level
The default, recommended layer. Suspicious sessions are routed to separate honeypot WordPress containers — the per-session pools described on the Engine page. Because each diverted adversary gets a full, real decoy eshop and database of their own, every class of attack has somewhere to "work": upload a web shell, exploit an RCE, dump the decoy database — all against fake data, fully isolated from production and from other adversaries.
Database Level
A lighter alternative. One production_eshop serves every session, and
wp-content/db.php acts as a per-request database router: a suspicious session's
requests are connected to the honeypot database, a clean session's to the production database. The
routing decision still comes from the proxy's Lua logic; proxy_params sets
X-Honeypot-Backend from that decision and always overwrites the header, so a client
cannot forge it.
Because there is only one eshop and only the database is swapped, this layer isolates database-layer attacks (for example SQL injection reading the decoy database) but not attacks that need a separate backend — a file upload or a code-execution exploit has no honeypot container to land in. Use it when resources are tight or the focus is data-layer deception; prefer the reverse-proxy level otherwise.
Switching Layers
Use the Services page, or by hand from ids/:
docker compose up -d --remove-orphans # reverse-proxy level
docker compose -f docker-compose.db-proxy.yml up -d --remove-orphans # database level
Both layers share the project name, so --remove-orphans replaces the services that
differ and removes the other layer's extra services. The honeypot database volume is the
exception: the database layer uses its own honeypot_database_proxy_data.
What the Database Layer Overlays
docker-compose.db-proxy.yml bind-mounts whole-file replacements from
ids/db_proxy/ over the shared copies, at the same relative paths:
reverse_proxy_enhanced/nginx.conf— no pool upstreams; every location proxies toproduction_backend.reverse_proxy_enhanced/proxy_params— setsX-Honeypot-Backendfrom the Lua routing decision, always overwriting it.lua/health_check.luaandlua/init_worker.lua— setPOOL_COUNTto 0 so nothing probes honeypot eshops that don't exist; health is mirrored from the database pools in Redis instead.scripts/replicate_content_to_honeypot.sh— syncs content into the singlehoneypot_database.honeypot_database_migrations/01_clean-honeypot-data.sql— DELETEs that tolerate missing tables, for the clone-of-production database.
Two files live in the shared production_eshop_files/ because Docker cannot overlay
paths that don't exist in the default tree; they do nothing unless
HONEYPOT_LAYER=database, which only this layer's Compose file sets:
wp-content/db.php (the per-request database router) and
wp-content/mu-plugins/honeypot-db-backend-header.php (adds X-DB-* debug
headers for the dashboard; requires INTERNAL_TEST_SECRET). See
ids/db_proxy/README.md for the complete file list.