Architecture
System architecture, request pipeline and the pure/adapter split
Architecture Overview
The honeypot system is built on a layered architecture following a pure/adapter split pattern. All core
logic lives in *_rules.lua modules with zero ngx.* dependencies, making them
unit-testable under a plain Lua interpreter. Adapters do the I/O — Redis, Nginx, the file system,
AbuseIPDB — and delegate every decision back to a pure rule module.
The system can run as two hosts (edge and SIEM, the production shape) or as one host for evaluation. Topology does not change the detection logic; it only changes where telemetry is stored.
Layers and Responsibilities
Request Pipeline
Client Request → Nginx access_by_lua_block → pattern_analyzer.analyze_request()
→ router.decide_route() (12 stages) → pool_router.get_or_assign_pool()
→ session_handler.update_session() → Redis session_store
→ Suricata eve.json → init_worker.lua → threat_intel shared dict
→ abuseipdb_client.report_ip_async() → AbuseIPDB
The pipeline is deliberately synchronous in the decision path. Configured work that must not block the response — AbuseIPDB reporting, pool provisioning, content sync — is queued or handled by a worker.
Routing stages
The twelve stages are ordered from cheapest and most certain to most heuristic. A static asset is released immediately; a session already bound to a pool never has to be evaluated again for stickiness.
| Stage | Condition | Route | Reason |
|---|---|---|---|
| 1 | Static asset | Production | No attack surface |
| 1a | Consent-gated gdpr_signature match (returning honeypot visitor) | Honeypot | Known-bad browser re-consented |
| 1b | install.php on uninstalled WordPress | Production | Legitimate setup wizard |
| 2 | Session already honeypot-bound | Honeypot | Sticky binding |
| 3 | Risk score ≥ threshold | Honeypot | High risk |
| 4 | CVE pattern matched | Honeypot | Known exploit attempt |
| 5 | Vulnerable plugin URI | Honeypot | Direct plugin endpoint access |
| 6 | Bad IP reputation (> 50) | Honeypot | Flagged source |
| 7 | Multiple admin attempts (≥ 3) | Honeypot | WSTG-ATHN-04 |
| 8 | Accumulated suspicious activity (≥ 5) | Honeypot | Cumulative signals |
| 9 | Rapid automation (score ≥ 40) | Honeypot | WSTG-INFO-02 |
| 10 | Suspicious file upload | Honeypot | WSTG-INPV-10 |
| 11 | Default | Production | No rule fired |
Lua Modules
The system uses 18 pure modules and their adapter counterparts. Pure modules contain no ngx.* dependency and are unit-testable under a plain lua interpreter.
| Pure Module | Adapter | Purpose |
|---|---|---|
| pattern_rules.lua | pattern_analyzer.lua | URI/header/CVE pattern scoring |
| prompt_injection_filter.lua | pattern_analyzer.lua | LLM prompt-injection pattern probe |
| vulnerability_rules.lua | vulnerability_handler.lua | CVE/exploit-kit pattern matching |
| suricata_rules.lua | init_worker.lua | eve.json alert parsing/scoring |
| abuseipdb_rules.lua | abuseipdb_client.lua | Report eligibility logic |
| router_rules.lua | router.lua | Routing predicates |
| upload_rules.lua | upload_handler.lua | File-upload pattern analysis |
| admin_rules.lua | admin_handler.lua | Admin-access decision logic |
| session_rules.lua | session_handler.lua | Session anomaly detection |
| decay_policy.lua | router_rules/suricata_rules | Escalation-aware decay |
| lua_pattern_utils.lua | — | Shared string helpers |
| ip_rules.lua | init.lua (utils) | IPv4/IPv6 CIDR matching |
| honeytoken_rules.lua | honeytoken_handler.lua | Token-in-corpus matching |
| gdpr_rules.lua | gdpr_handler.lua | Consent-gated identity |
| shared_ip_rules.lua | pattern_analyzer.lua | Shared-address policy |
| pool_router_rules.lua | pool_router.lua | Pool routing logic |
| session_identity_rules.lua | session_handler.lua | Session cookie identity |
| maintenance.lua | nginx.conf | Scheduled-maintenance gate |
Run-time details collected from the modules' comments — shared dicts, background timers, the Redis key schema, tuned constants and the bugs behind them — are in Lua Internals.
Pooling System
Each user session is permanently assigned to a specific honeypot pool instance (honeypot_eshop_N + honeypot_database_N). Pools are cloned from a pre-built template. Assignment is atomic via Redis EVAL. Scale-up and scale-down are managed by pool_manager.
- Exclusive first. A free ready pool is claimed atomically, so two sessions cannot share an exclusive assignment.
- Spare provisioning. Every assignment queues a wake-up, and the manager builds the next pool pair in parallel.
- Borrow, then move. A waiting session borrows a ready pool and is moved when its own pool finishes building.
- Bounded reuse. At the resource ceiling, sessions share ready pools round-robin instead of failing.
- Idle retirement. Assignments expire after 24 hours; pools above the spare count are removed after the idle timeout.
- Health fallback. If the assigned pool is unhealthy, the router serves the next healthy pool without changing the stored assignment.
See Honeypot Engine for the pool lifecycle, configuration knobs and isolation model.
State and Failure Modes
Redis holds the state that makes sessions sticky: session blobs, accumulated risk, offense counters and pool assignments. Nginx also keeps 10 MB shared dictionaries for sessions and threat intelligence — shared across all worker processes of the Nginx instance, not per-worker — so hot paths avoid a round-trip when possible.
- Redis unavailable. Session stickiness degrades; the router must prefer a safe production fallback rather than blocking the shop.
- Pool unhealthy. The router falls back to the next healthy pool without changing the stored assignment.
- Suricata silent. Application-layer scoring continues; only the network signal is lost.
- SIEM unreachable. The edge keeps serving. Vector buffers and retries; detection is never coupled to Kibana availability.
- Signing key unset. A random key is generated at startup; cookies from the previous process no longer verify. Set
SESSION_SIGNING_KEYexplicitly in production.
GDPR Identity
A second, consent-gated identifier runs alongside the signed SERVERID cookie. Only after the
visitor agrees does the browser RSA-OAEP-encrypt a few characteristics and a timestamp into a
gdpr_token; the proxy decrypts it in memory, checks freshness and hashes it into a one-way
gdpr_signature. Only the signature is stored, and only while bound to a honeypot. It recovers a
returning adversary's session (routing stage 1a); it is not a tracking identifier for legitimate visitors.
The full flow and settings are on Device & GDPR Signature.