# 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.