When to Use
- Changing how the orchestrator launches or secures a scan container (recon_orchestrator/container_manager.py).
For the no-env_file knob rule, see the recon_orchestrator
AGENTS.md CRITICAL RULES (not repeated here).
Critical Rules
- NEVER add
cap_drop: [ALL]to a scan container that writes to a host-owned source bind mount. It stripsCAP_DAC_OVERRIDE, so root-in-container can no longer write the host-owned files, and the scan breaks. This was reverted after breaking recon/partial spawns; hardening is deliberately deferred withdrop_caps=Falseat every spawn site (container_manager.py:837, :1798, :2168). Keep it deferred unless the mount is not host-owned. - NEVER add
security_opt: no-new-privilegesto these spawns. It breaksexecvefor non-root users inside the recon image (reverted once already): container_manager.py:939. - NEVER add a
tmpfsmount withoutuid/gid/modewhen the container runs as a NON-ROOT user and the mount lands on a path that user must write. Docker mounts a tmpfs root-owned 0755 unless told otherwise (only/tmpgets the 1777 default), and the mount SHADOWS whatever the image built at that path - so a tmpfs added to give a non-root user writable scratch is what takes it away. This shipped: the TruffleHog spawn's/home/trufflehogtmpfs hid the home diruseradd --create-homehad given uid 10001, andgithub_experimentaldied on "failed to create .trufflehog folder in user's home directory" while the other thirteen sources were fine, because it is the only one that writes to$HOME. Build the spec in_trufflehog_tmpfs(), not inline, and size-cap every entry - an uncapped tmpfs is host RAM a hostile archive can exhaust. - ALWAYS apply hardening through
_scanner_hardening()(container_manager.py:567), not ad-hoc per spawn, so all three spawn sites stay consistent. - ALWAYS keep
sibling_host_path()robust to BOTH POSIX (/) and Windows (\) host paths (container_manager.py:53). It derives a sibling source dir's host path for bind mounts; a POSIX-only assumption breaks spawns on Windows hosts. Its two companionsparent_host_path()andjoin_host_path()carry the same POSIX+Windows discipline - never swap inpathlib/Path(...).parent, which collapses a Windows host path on the Linux orchestrator. - NEVER assume a scanner source dir is a repo-root sibling. Scanners live two
levels deep under
scanners/<name>/, so a bind mount to a repo-root sibling (e.g.graph_db) must climb out ofscanners/first:sibling_host_path(parent_host_path(scanner_path), "graph_db"), and ascanners/-nested sibling is reached withjoin_host_path(parent_host_path(recon_path), "scanners", "supply_chain_common"). The oldsibling_host_path(scanner_path, "graph_db")now resolves to a nonexistentscanners/graph_db; Docker silently binds an empty root-owned dir there and graph writes / imports fail with no error. The build context climbs two parents:parent_host_path(parent_host_path(scanner_path)). - NEVER bind
/app/graph_dbdirectly at a spawn site. Always route it throughself._graph_db_mount(<derived>, baked_into_image=...)(container_manager.py:605). Deriving graph_db's host path is a LAST RESORT, not the mechanism: the real path is auto-detected from the orchestrator's own./graph_db:/app/graph_db:romount (GRAPH_DB_PATH, resolved inapi.pyexactly likeRECON_PATH). The derivation is only right when Docker reports the literal repo path - Docker Desktop on Windows/WSL2 reports rewritten bindSourcestrings whose sibling is nowhere, Docker auto-creates that path EMPTY, and the empty dir shadows the graph_db baked into the scan image. Every spawned scan then dies withcannot import name 'Neo4jClient' from 'graph_db' (unknown location)(issue #169).baked_into_image=Truefor recon / gvm / github-hunt (they COPY graph_db, so no mount beats a wrong mount);Falseonly for supply-chain, which does not bake it. TruffleHog has NO graph_db mount at all: its container is the dirty half of a dirty/clean split and holds no Neo4j credentials, so the orchestrator ingests its findings afterwards. - ALWAYS resolve a new host source path with
_get_host_path()+ a compose mount, not by string surgery on another path. If a spawn needs host dirX, mountXinto the orchestrator so Docker itself reports its source. A missing bind source is not an error to Docker; it silently becomes an empty directory.
Why these flags break here
Scan containers run as root and bind-mount host-owned source (the live
working tree) so a .py change is picked up without a rebuild. Standard
container hardening (drop all caps, no-new-privileges) assumes the container owns
its filesystem and runs unprivileged - neither holds here, so the "secure
defaults" a reviewer would add are exactly what broke production twice.
Commands
bashdocker compose restart recon-orchestrator # container_manager.py is volume-mounted ./redamon.sh test unit # recon_orchestrator section
Resources
- recon_orchestrator/container_manager.py - the three spawn sites and
_scanner_hardening - Related: recon_orchestrator AGENTS.md CRITICAL RULES (the no-
env_fileknob rule)

