123 lines
6.4 KiB
Markdown
123 lines
6.4 KiB
Markdown
# 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.
|