Files

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/443 unconditionally 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_keys entry is restricted with no-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:

  • request records method, raw URI and every raw header (including User-Agent).
  • request_body stores the exact raw body bytes as a content-addressed artifact for every request; multipart parts become upload events, other bodies upload_or_raw_payload, form posts form_fields. Filenames are metadata only.
  • response / response_sent store the bytes returned.
  • request_end stores the exact HTTP bytes the parser consumed, even for malformed, partial or rejected requests.
  • Log-poisoning candidates become injection_candidate; each extracted command is then simulated_command/unsupported (with argv, cwd and output) or, in VM mode, execution_* plus guest_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

  1. Keep the listener in simulation mode.
  2. Put an edge proxy (Cloudflare) in front; pass the original client IP via CF-Connecting-IP and configure --trusted-forwarded-from with the edge ranges (deploy/cloudflare-ips.txt).
  3. Origin must only accept traffic from the edge; do not expose the origin directly.
  4. Terminate TLS at the edge so the decoy sees HTTP and can read headers.
  5. 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-config now 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.