-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathdocker-compose.sandbox.yml
More file actions
242 lines (233 loc) · 10.8 KB
/
Copy pathdocker-compose.sandbox.yml
File metadata and controls
242 lines (233 loc) · 10.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
# docker-compose.sandbox.yml — Phase 4 gateway services for the Windows
# detonation guest. See docs/sandbox/windows/IMPLEMENTATION_PLAN.md §Phase 4.
#
# ┌─ THIS IS NOT PART OF APIARY ───────────────────────────────────────────┐
# │ Do not merge these services into docker-compose.yml and do not start │
# │ them alongside it. They exist to answer live malware on an isolated │
# │ bridge and are started and stopped around a single detonation: │
# │ │
# │ docker compose -f docker-compose.sandbox.yml up -d zeek suricata \ │
# │ tcpdump (start_gateway_services)│
# │ docker compose -f docker-compose.sandbox.yml rm -f -s zeek \ │
# │ suricata tcpdump (stop_gateway_services)│
# │ │
# │ orchestrate/run_sample.py's detonate_inguest() drives both, per │
# │ sample, with SANDBOX_RESULTS_DIR set to that sample's own out_dir -- │
# │ not the ./sandbox/results/current default below, which only applies │
# │ to a manual `docker compose up` run outside the orchestrator (#510). │
# │ │
# │ INetSim and mitmproxy are NOT started automatically: in-guest │
# │ FakeNet-NG (started by the orchestrator inside the guest itself) │
# │ already supersedes INetSim's fake-internet job, and mitmproxy stays │
# │ opt-in (`--profile mitm`) as documented below. Only Zeek/Suricata/ │
# │ tcpdump have no in-guest equivalent -- they see the host-side bridge, │
# │ the only vantage point for traffic the guest sends to an address │
# │ nothing answers on. │
# └───────────────────────────────────────────────────────────────────────┘
#
# Address plan on 10.10.10.0/24 (sandbox/windows/setup/sandbox-network.xml):
#
# 10.10.10.1 INetSim the fake internet — DNS, HTTP/S, SMTP, FTP, IRC
# 10.10.10.2 the guest static DHCP lease pinned to its MAC
# 10.10.10.3 mitmproxy opt-in TLS interception (profile: mitm)
# 10.10.10.254 the host libvirt bridge address; no forwarding to WAN
#
# Zeek and Suricata are not on that subnet at all. They sniff the bridge from
# the host side, which is the only vantage point that sees traffic the guest
# sends to an address nothing answers on.
#
# Isolation, restated because it is the whole point: the macvlan below is
# `internal`, the libvirt network has no <forward>, and Phase 0 adds an
# iptables DROP pair across virbr-sandbox. Three independent barriers. Removing
# any one of them because "the container needs to pull something" puts live
# malware on the internet — build images ahead of time instead.
name: honeypot-sandbox-gateway
x-gateway-defaults: &gateway-defaults
restart: "no" # per-detonation lifecycle; never resurrect a stopped capture
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
services:
# ===== INETSIM — answers everything =====
# A sample that gets a connection refused stops early and tells you nothing.
# INetSim resolves every name to itself and serves a plausible response on
# every port, so the sample keeps going and the whole C2 conversation lands
# in the logs.
inetsim:
<<: *gateway-defaults
build:
context: ./sandbox/windows/gateway/inetsim
image: honeypot-sandbox-inetsim:local
container_name: sbx-inetsim
networks:
sandbox:
ipv4_address: 10.10.10.1
volumes:
# dns_default_ip in this file is 10.10.10.1 — every lookup the guest
# makes comes straight back here.
- ./sandbox/windows/config/inetsim.conf:/etc/inetsim/inetsim.conf:ro
# Reports, service logs, and — the valuable part — anything the sample
# downloaded. RESULTS_DIR is set per detonation by run_sample.py.
- ${SANDBOX_RESULTS_DIR:-./sandbox/results/current}/fakenet_logs:/var/log/inetsim
cap_drop:
- ALL
cap_add:
# Binds 21/25/53/80/443. Nothing else is needed.
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
# ===== MITMPROXY — opt-in, one sample at a time =====
# Off by default (`--profile mitm` to start it), because turning it on means
# editing dns_default_ip to 10.10.10.3 so HTTP/S lands here first — and that
# same change points SMTP, FTP and IRC at a proxy that does not speak them.
# Worth it when you need decrypted request bodies from a specific sample;
# a net loss as a standing default.
#
# The guest must trust the mitmproxy CA for TLS interception to be silent.
# Install it in the golden image before taking GOLDEN_READY — a certificate
# error is itself an evasion trigger for some families.
mitmproxy:
<<: *gateway-defaults
profiles: ["mitm"]
image: mitmproxy/mitmproxy:latest@sha256:00b77b5d8804c8ad18cb6caefbf9d5849e895e8986c5ce011f4ae30f4385962f
container_name: sbx-mitmproxy
networks:
sandbox:
ipv4_address: 10.10.10.3
command: >
mitmdump
--mode reverse:http://10.10.10.1
--listen-host 0.0.0.0
--listen-port 80
--set stream_large_bodies=10m
--save-stream-file /flows/mitmproxy_flows.bin
volumes:
- ${SANDBOX_RESULTS_DIR:-./sandbox/results/current}:/flows
cap_drop:
- ALL
cap_add:
- NET_BIND_SERVICE
security_opt:
- no-new-privileges:true
# ===== ZEEK — protocol record of the whole detonation =====
# host networking plus NET_ADMIN/NET_RAW because it needs the bridge device
# itself, not a container interface. This is a genuine privilege grant and it
# is why only the two sniffers get it — INetSim and mitmproxy above run with
# ALL capabilities dropped.
zeek:
<<: *gateway-defaults
image: zeek/zeek:latest@sha256:703f0b22af150d9418739b2a012fbfb5d01ee004aded3bd43b0175010db05928
container_name: sbx-zeek
network_mode: host
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
security_opt:
- no-new-privileges:true
command: >
zeek -i ${SANDBOX_BRIDGE:-virbr-sandbox}
-e "redef Log::default_logdir = \"/zeek-logs\";"
local
volumes:
- ${SANDBOX_RESULTS_DIR:-./sandbox/results/current}/zeek_logs:/zeek-logs
# ===== SURICATA — signature alerts on the same bridge =====
# Same image as the VPS sensor, so rule handling and eve.json parsing behave
# identically to everything else in this repository.
suricata:
<<: *gateway-defaults
image: jasonish/suricata:latest@sha256:51a59543dcb6e5f9d586c06053c6b71d462cea919b45719090443f4af1428780
container_name: sbx-suricata
network_mode: host
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
- SYS_NICE
# jasonish/suricata's own docker-entrypoint.sh (fix_perms(), gated on
# having sys_nice+net_admin -- both granted above) chowns
# /etc/suricata, /var/lib/suricata, etc to the suricata user, then
# execs suricata itself with --user suricata --group suricata.
# Without cap_drop: ALL none of this needed spelling out; with it,
# every one of these five is independently required and the failure
# mode for each is a different, unrelated-looking error -- confirmed
# live 2026-08-05, the first time this compose file was ever actually
# invoked (#510):
# - CHOWN: fix_perms()'s chown fails ("Operation not permitted"),
# and the entrypoint's `set -e` kills the container immediately.
# - DAC_OVERRIDE: even after a successful chown, cap_drop: ALL also
# strips root's normal DAC bypass -- "root" in this container is
# not exempt from file permission checks without this, so the
# entrypoint (or suricata itself, pre-drop) fails to open
# /etc/suricata/suricata.yaml ("Permission denied") regardless of
# which user actually owns it.
# - SETUID/SETGID: suricata's own internal privilege drop (from
# --user/--group) needs these directly -- without them it fails
# at startup with "capng_change_id for main thread failed".
# - SETPCAP: also required by that same capng_change_id() call, to
# manage which capabilities (net_admin/net_raw/sys_nice) survive
# the uid switch so packet capture still works as the
# unprivileged suricata user afterward.
- CHOWN
- DAC_OVERRIDE
- SETUID
- SETGID
- SETPCAP
security_opt:
- no-new-privileges:true
command: >
-i ${SANDBOX_BRIDGE:-virbr-sandbox}
--set outputs.1.eve-log.filename=suricata_alerts.json
volumes:
- ${SANDBOX_RESULTS_DIR:-./sandbox/results/current}:/var/log/suricata
environment:
# Rules are refreshed on the host, on demand, with internet. Never from
# here: a detonation must not depend on a network fetch, and this
# container has no route to one anyway.
- SURICATA_OPTIONS=
# ===== TCPDUMP — the raw pcap Phase 6 lists =====
# Zeek's logs are derived evidence. The pcap is the primary record, and the
# one thing you cannot reconstruct afterwards if a parser turns out to have
# been wrong about a protocol.
tcpdump:
<<: *gateway-defaults
image: nicolaka/netshoot:latest@sha256:b09d9b21381f47a79b3cbcb30da25266dc17186ea00ae65e99fdc51396f48e70
container_name: sbx-tcpdump
network_mode: host
cap_drop:
- ALL
cap_add:
- NET_ADMIN
- NET_RAW
security_opt:
- no-new-privileges:true
command: >
tcpdump -i ${SANDBOX_BRIDGE:-virbr-sandbox}
-s 0 -U -w /capture/network.pcap
volumes:
- ${SANDBOX_RESULTS_DIR:-./sandbox/results/current}:/capture
networks:
# macvlan over the libvirt bridge: the containers get real addresses on the
# guest's L2 segment, so the guest reaches them without any routing.
#
# `internal: true` removes the default route Docker would otherwise install
# via the gateway. That gateway is the host, whose FORWARD chain already
# drops sandbox traffic — this is the second of the three barriers, not the
# only one.
#
# Side effect of macvlan, and it is the expected one: the host itself cannot
# talk to these containers. Collect their output from the bind-mounted
# results directory, not over the network.
sandbox:
driver: macvlan
internal: true
driver_opts:
parent: ${SANDBOX_BRIDGE:-virbr-sandbox}
ipam:
config:
- subnet: 10.10.10.0/24
gateway: 10.10.10.254