Files
citrixpot/SECURITY.md
T

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.