6.4 KiB
Security model and threat model
This is a deliberately attacked system. Everything here is designed so that an attacker who "exploits" the decoy can upload files, run commands and receive plausible output without doing anything real to the collector or the host.
Two execution modes
| Mode | Default | What happens to attacker commands |
|---|---|---|
simulation |
yes | Parsed, logged and answered with canned output. Never executed. No shell, no syscalls, no sockets. |
disposable_vm |
no | Real bash runs inside disposable VM223. Requires both --vm-config and --allow-real-execution. |
The simulator is pure Python dict manipulation (simulate()); it has no path to
os.system, subprocess, eval or a real filesystem path. Uploaded bodies are
stored as inert 0600 content-addressed blobs. This is the safe mode for an
internet-facing listener and is what deploy/decoy.service runs.
The VM mode exists for private labs that want to observe real payload behaviour.
It must not be enabled on a public listener. The two-flag gate above makes
accidental activation impossible, and the process logs a loud warning plus
real_execution: true in the start event.
Isolation of the disposable execution guest (VM223)
- QEMU user networking with
restrict=on; no bridge NIC, so the guest cannot reach the LAN, the management network or the internet directly. - No host mounts, no shared folders, no 9p, no virtiofs, no SPICE. The only block device is the guest's own discardable disk.
- Guest TCP is redirected (iptables
REDIRECT) to a local relay that speaks only to the outside recording broker. A separate guestfwd path reaches the broker; the broker is the sole policy point for every outbound connection. - The broker allows public IPv4
80/443unconditionally and other ports only to recently observed attacker peers. Private, loopback, metadata and LAN destinations are denied. DNS returns public A records only. - The guest is reset every hour: disk is archived to a validated qcow2 and
rolled back to
decoy-clean; unflushed RAM is discarded. - A host watchdog stops VM223 on pcap quota, disk/thin-pool reserve, collector service failure or a STOP latch. STOP is never auto-cleared.
- Collector→guest SSH uses a dedicated key whose
authorized_keysentry is restricted withno-agent-forwarding,no-port-forwarding,no-X11-forwarding. - Packet capture lives on the host (
execution.pcap, 512-byte prefixes), so it survives guest tampering.
Residual risk in VM mode is a QEMU/KVM or guest-kernel escape. Keep the hypervisor patched, never enable VM mode on a public listener, and treat the guest disk and pcap as untrusted binary evidence.
Collector isolation
decoy.service runs under systemd with ProtectSystem=strict,
NoNewPrivileges, PrivateTmp, RestrictAddressFamilies, MemoryMax and
TasksMax, writing only under /var/lib/decoy (root-only, 0700). The ingress
proxy runs as a DynamicUser with no capabilities. The local viewer is
loopback-only and read-only.
Real client IP behind the edge
The decoy never trusts attacker-supplied X-Forwarded-For. Forwarded client
headers (CF-Connecting-IP, True-Client-IP, X-Real-IP, then the nearest
X-Forwarded-For hop) are honoured only when the connecting hop is inside a
configured trusted network (--trusted-forwarded-from, e.g. Cloudflare's
published ranges plus loopback). The real client IP is recorded as peer; the
proxy hop is retained separately as proxy_peer. Spoofed private/reserved
addresses are rejected.
Logging guarantee (capture everything)
Every request is journaled in full and its bytes are stored, so nothing depends on the decoy recognising it:
requestrecords method, raw URI and every raw header (includingUser-Agent).request_bodystores the exact raw body bytes as a content-addressed artifact for every request; multipart parts becomeuploadevents, other bodiesupload_or_raw_payload, form postsform_fields. Filenames are metadata only.response/response_sentstore the bytes returned.request_endstores the exact HTTP bytes the parser consumed, even for malformed, partial or rejected requests.- Log-poisoning candidates become
injection_candidate; each extracted command is thensimulated_command/unsupported(with argv, cwd and output) or, in VM mode,execution_*plusguest_stdout/guest_syscalls_stderr/guest_file_*. - Known Citrix exploit indicators are additionally labelled
exploit_attempt(CVE-2019-19781,path-traversal,config-read,management-probe). This is a convenience label on top of the full capture, not a filter — unflagged requests are still stored.
Uploaded payloads are inert evidence on the collector and are never executed there.
Evidence
The append-only JSONL journal and content-addressed blobs are 0600 under a
0700 directory. The service fails closed and exits non-zero when the evidence
quota or disk fails, rather than dropping or overwriting records. Uploaded files
are never executed by the collector and are never copied into a guest.
Reporting boundary
No reporting UI is served on the attacker listener. The evidence viewer is a separate loopback-only process reached over SSH port-forwarding. Do not add reporting routes to the decoy listener.
Public exposure checklist
- Keep the listener in
simulationmode. - Put an edge proxy (Cloudflare) in front; pass the original client IP via
CF-Connecting-IPand configure--trusted-forwarded-fromwith the edge ranges (deploy/cloudflare-ips.txt). - Origin must only accept traffic from the edge; do not expose the origin directly.
- Terminate TLS at the edge so the decoy sees HTTP and can read headers.
- Keep the STOP latch and watchdog enabled; archive evidence before clearing it.
Audit notes (2026-09-28)
- Verified the simulator contains no command-execution path.
- Found the deployed collector running
--vm-config(real execution). Hardened:--vm-confignow also requires--allow-real-execution; the deployed unit was switched to simulation-only; a loud startup warning is emitted in VM mode. - Verified VM223 has no host mounts, no bridge,
restrict=on, and a locked-down management key. - Added trusted-edge client-IP extraction with spoof rejection.
- Added the loopback-only read-only viewer and its hardened systemd unit.