Skip to content

feat(security): fail-closed auth, scope enforcement, audit logging, Docker isolation - #226

Open
fxinfo24 wants to merge 9 commits into
0x4m4:masterfrom
fxinfo24:feat/security-docker-hardening
Open

feat(security): fail-closed auth, scope enforcement, audit logging, Docker isolation#226
fxinfo24 wants to merge 9 commits into
0x4m4:masterfrom
fxinfo24:feat/security-docker-hardening

Conversation

@fxinfo24

@fxinfo24 fxinfo24 commented Aug 7, 2026

Copy link
Copy Markdown

Summary

This server executes arbitrary commands and 150+ offensive-security tools (/api/command, /api/python/execute, /api/files/create, etc.) with zero authentication and a hardcoded 0.0.0.0 bind — the README even flags this ('Consider implementing authentication for production deployments') without actually shipping any. Anyone on the same LAN as a running instance currently has unauthenticated remote command execution. This PR closes that gap and adds the controls a real pentest engagement needs: scope enforcement and an audit trail.

Changes

Auth (fail-closed)

  • Server now binds 127.0.0.1 by default (--host/HEXSTRIKE_HOST to override)
  • New shared-secret auth via X-HexStrike-Token header, hmac.compare_digest, checked on every route
  • Server refuses to start at all if HEXSTRIKE_API_TOKEN is unset — no silent unauthenticated fallback

New /ping endpoint

  • /health does a full ~30s sweep across all tools (subprocess-based detection) — too slow for liveness checks/monitoring
  • /ping is instant, no tool sweep, for that use case

Engagement scope enforcement (opt-in via HEXSTRIKE_SCOPE_FILE)

  • Allowlist authorized domains/CIDRs per engagement (scope.example.json)
  • Checks both structured params (target/host/domain/ip/url/rhost/hostname) and targets parsed out of raw command strings (conservative — only known-TLD-suffixed tokens, avoids false positives on filenames like results.txt)
  • 403 on anything out of scope
  • No-op if unset, so existing lab/CTF usage isn't affected

Audit log

  • JSONL, one line per authenticated request: timestamp, source IP, method, path, extracted targets, status
  • Needed for engagement records / chain of custody, not just debugging

Docker

  • Dockerfile (Kali-rolling base): apt-based core tool set + Go-based recon tools (nuclei, subfinder, httpx, ffuf) via a timeout-capped, non-fatal install loop so one stalled module fetch can't block the build
  • Python deps isolated into a venv (Kali ships several packages as dpkg-managed system packages — pip --break-system-packages can't safely touch those under PEP 668; hit this directly with bcrypt)
  • Non-root execution (uid 1000); nmap/masscan get raw-socket capabilities via setcap on the binaries specifically, not the container as a whole
  • README documents a per-engagement pattern: one container per engagement, isolated scope file + workspace/audit-log volume, so concurrent engagements can't cross-contaminate

Testing

All changes verified against a live instance, both bare-metal and containerized:

  • 401 without token, working auth with it, server exits at startup with no token
  • Scope enforcement: exact domain match, subdomain match, CIDR match, both structured-param and raw-command-string targets, correctly blocking/allowing across all cases
  • Audit log confirmed writing valid JSONL
  • Docker: confirmed non-root (whoami/id), confirmed nmap functional despite non-root, confirmed workspace + audit isolation (file written inside container correctly landed in the host-side per-engagement directory)

Compatibility

Everything except the auth requirement is opt-in or additive. The one breaking change is intentional: the server no longer starts without HEXSTRIKE_API_TOKEN set. Given what /api/command does, I don't think that should be optional.

fxinfo24 and others added 9 commits August 6, 2026 03:35
…udit log

- app.run() no longer hardcodes host=0.0.0.0; new --host/HEXSTRIKE_HOST
  flag defaults to 127.0.0.1 so the 150+ API routes (including
  /api/command's arbitrary shell exec) aren't LAN-exposed by default
- add shared-secret auth via X-HexStrike-Token header, checked with
  hmac.compare_digest on every route; server exits at startup if
  HEXSTRIKE_API_TOKEN is unset rather than running unauthenticated
- add lightweight GET /ping for fast liveness checks; /health remains
  the full ~30s tool-availability diagnostic sweep
- add opt-in engagement scope enforcement (HEXSTRIKE_SCOPE_FILE):
  allowlist authorized domains/CIDRs, 403 any request whose target
  (structured param or parsed out of a raw command string) falls
  outside it — see scope.example.json
- add JSONL audit log (HEXSTRIKE_AUDIT_LOG) of every authenticated
  request: timestamp, source, method, path, targets, status
- update README: document new env vars, fix install/verify examples
  to include the token header, add .gitignore
…tion

- add Dockerfile (kalilinux/kali-rolling base): apt-based core tool set
  (nmap, masscan, gobuster, sqlmap, hydra, hashcat, radare2, binwalk,
  etc.) plus Go-based recon tools (nuclei, subfinder, httpx, ffuf) via
  a timeout-capped, non-fatal install loop so one stalled module fetch
  can't block the whole build
- python deps isolated into a venv rather than system pip, avoiding
  PEP 668 conflicts with Kali's dpkg-managed packages (bcrypt etc.)
- non-root execution (uid 1000); nmap/masscan get raw-socket
  capabilities via setcap on the binaries specifically, not on the
  container as a whole
- add entrypoint.sh: fails fast with a clear message if
  HEXSTRIKE_API_TOKEN is unset, reports scope-enforcement status
- add .dockerignore
- README: document the Docker path with a per-engagement run example
  (isolated scope file + workspace/audit-log volume per engagement),
  and clarify why the container's internal 0.0.0.0 bind doesn't
  contradict the loopback-only guidance elsewhere in the doc

Verified end-to-end against a running container: auth enforced,
non-root confirmed via whoami/id, workspace + audit log correctly
isolated to the per-engagement host directory.
…, xsstrike, sstimap

- arsenal-ng, gef, wpprobe, xsstrike, sstimap installed via apt (confirmed
  present in Kali's live apt index before adding, avoiding a blind
  package-name guess)
- atomic-operator (MITRE ATT&CK/Atomic Red Team technique execution)
  isn't in apt — installed via pip into its own isolated venv, not the
  main app venv. Its dependency atomic-operator-runner hard-pins
  pydantic 1.x (confirmed via its pyproject.toml), which conflicts with
  mcp/fastmcp's pydantic 2.x requirement — installing it into the shared
  venv silently downgraded pydantic and broke hexstrike_mcp.py's imports
  at runtime (verified by hand, then fixed via isolation)
- atomic-operator's other dependency  still imports the stdlib
   module, removed in Python 3.13 (Kali-rolling's default
  python3) — a known, still-open upstream bug (google/python-fire#444)
  with no fixed release. Patched via a local sed (pipes.quote and
  shlex.quote are functionally identical — that's fire's own documented
  migration path)
- gef: the apt package only drops gef.py at /usr/share/gdb/gef.py
  without wiring it into gdb's startup — added a system-wide gdbinit
  source line
- xsstrike: pre-installed fuzzywuzzy (it pip-installs this at runtime on
  first use otherwise, requiring outbound network access mid-engagement)
- arsenal-ng installs fine but is a TUI app requiring a real tty — not
  usable via /api/command's non-interactive execution, noted as a
  usage-pattern limitation rather than a bug

Sourced from a 10-tool proposal; the other 4 were evaluated and not
included: sstimap replaces the already-listed tplmap (unmaintained
since ~2019) rather than being added alongside it, legba skipped as
redundant with the existing hydra/medusa/patator, fluxion held back
over collateral-impact concerns (WiFi deauth affects every client on
the channel, not just the scoped target), hydra was already present.

All 6 verified working end-to-end against a running container.
…), not pip

The previous commit worked around a pydantic 1.x/2.x conflict by
isolating atomic-operator into its own pip venv plus a manual sed patch
for a stdlib 'pipes' module removal in Python 3.13 (fire's
still-open upstream bug, google/python-fire#444).

Turns out that was solving a problem the real fix doesn't have: Kali
packages this as python3-atomic-operator (confirmed against the live
apt index and via kali.org/tools/atomic-operator), with its own
dependency graph (python3-fire, python3-atomic-operator-runner, etc.)
resolved entirely through apt — completely separate from this
project's pip venv, so there's no pydantic conflict to isolate against
in the first place, and no pipes-removal patch needed (Kali's own
package build already handles it).

Net effect: one apt package instead of an isolated venv + symlink +
sed patch. Verified clean: hexstrike_mcp.py still imports correctly
(pydantic stays at 2.13.4 in the main venv), atomic-operator's CLI
works standalone.
…rtipy; retire dirb

7 tools from a 10-tool proposal (3 already covered: Nuclei/ffuf/Responder
were already installed). All verified working end-to-end.

- rustscan, amass, feroxbuster, python3-impacket, netexec, certipy-ad —
  confirmed against the live apt index before adding
- dirb removed: gobuster + feroxbuster now cover directory brute-forcing
  more capably, running three overlapping tools isn't worth it
- amass's apt wrapper shells out to 'sudo libpostal_data download all'
  on first run if libpostal's data dir is missing — fails under the
  non-root hexstrike user, and would pull ~1-2GB even as root for an
  address-normalization feature amass's actual recon functionality
  (DNS enum, ASN lookups, cert-transparency, subdomain brute-force)
  doesn't touch. Satisfied the wrapper's existence check with an empty
  sentinel instead of downloading real data — verified amass's real
  subcommands (enum, etc.) work fine without it.

fix(docker): stop shadowing system python3 for apt-packaged tool wrappers

Root cause, not a one-off patch: this Dockerfile prepended /opt/venv/bin
to the global PATH so our own app would resolve pydantic 2.x/mcp/fastmcp
correctly. Several apt-packaged tools ship thin wrapper scripts that
call bare `python3` rather than an absolute path (confirmed:
impacket-secretsdump and the rest of the impacket-* family) — with
venv-first PATH, those silently resolved to our isolated venv instead
of system python3, and since venvs don't see system dist-packages,
they failed with ModuleNotFoundError for a library that was actually
installed and present on disk. Root-caused by hand: checked the
wrapper's exec line, confirmed the library was on disk via dpkg -L,
confirmed system python3's sys.path included dist-packages, isolated
it to the PATH shadowing specifically.

Fix: removed the global PATH prepend entirely. Our own app now
references the venv by absolute path (/opt/venv/bin/pip at build time,
/opt/venv/bin/python3 in entrypoint.sh) instead of relying on PATH
order. Bare `python3` now resolves to the system interpreter
everywhere, so every apt-packaged tool's wrapper works as its
maintainer intended. This wasn't impacket-specific — any Kali tool
with a similar wrapper pattern was at risk before this fix, impacket
just happened to be the one that surfaced it.

Verified after the fix: impacket-secretsdump works correctly, and
hexstrike_mcp.py still imports cleanly with pydantic held at 2.x in
the main venv (checked explicitly via /opt/venv/bin/python3).
hexstrike_mcp.py (the actual client Claude Desktop/Code/VS Code spawn to
talk to the server) predates the auth work entirely — HexStrikeClient's
requests.Session() never set any header, so every single tool call would
have 401'd against the now-hardened server, bare-metal or Docker, with
no indication why beyond generic request failures.

- inject X-HexStrike-Token from HEXSTRIKE_API_TOKEN env var; log a clear
  error (not a silent failure) if it's unset, matching the server's own
  fail-closed posture
- switch the startup connectivity check from /health to /ping — /health
  runs a ~30s full tool-availability sweep, which would stall every MCP
  client startup for no reason; /ping is the fast liveness check that
  exists for exactly this

Verified against a live Docker engagement: confirmed the auth header is
actually sent, confirmed the client connects and the server reports
healthy immediately rather than after a 30s wait.
…t field

Discovered live, during actual authorized testing against pathosbay.com:
scope enforcement checked payload['command'] specifically for embedded
domains/IPs, plus a fixed list of structured keys (target/host/url/etc).
/api/python/execute uses a different field entirely — 'script' — which
accepts arbitrary Python that can make its own HTTP requests to any
target. None of that was covered, so scope enforcement had zero
visibility into what a submitted script actually touched: it was a full
bypass, not a partial gap, for that endpoint and any other
similarly-shaped one we hadn't specifically audited.

Confirmed exploitable in practice: a python-execute call against an
out-of-scope-at-the-time domain returned real page content before this
fix landed.

Fix: generalized _extract_targets to scan every string value in the
request payload for embedded domains/IPs (same heuristic already used
for 'command'), not just a hardcoded field name. Closes the specific
/api/python/execute gap and any future endpoint with a differently
named free-text field, without needing to enumerate every endpoint's
parameter naming by hand.

Verified against a live engagement: out-of-scope target via
/api/python/execute now correctly 403s; in-scope target passes the
scope check through to actual execution.
… errors

- ENV PATH appended /opt/go/bin instead of prepending, so pip's
  httpx[cli] console script (installed elsewhere on PATH) silently
  shadowed ProjectDiscovery's real httpx binary. Go tools now take
  priority for every binary in the go-install list, not just httpx.
- waybackurls was never in the go-install list despite being wired
  into the MCP tool surface (waybackurls_discovery) -- added.
- go-install loop swallowed all stderr via a bare "|| echo SKIPPED",
  making install failures silent. Now tees the last 20 lines of
  actual output so failures are diagnosable from the build log.
  Also bumped the per-package timeout 180s -> 300s; nuclei's install
  legitimately needs more than 180s on a cold module cache.
…timeout

- _extract_targets() never split comma-separated values before the
  scope-domain match, so any multi-host batch call (httpx, dnsx, ...)
  in a single target/host field was falsely rejected as out_of_scope.
  Confirmed live against the pathosbay.com engagement scope.
- execute_httpx_scan() and /api/tools/httpx both called a bare httpx,
  which resolves to pip's httpx[cli] HTTP client (same name, unrelated
  tool) ahead of the real recon binary on PATH -- see Dockerfile commit.
  Pinned both call sites to HEXSTRIKE_HTTPX_BIN (absolute path,
  env-overridable) and replaced "-l {target}" (expects a filename, not
  an inline host string) with stdin piping via printf %b.
- execute_amass_scan() and /api/tools/amass never bounded amass's own
  runtime. Passive/active enum can run well past the generic 300s
  command-timeout wrapper and gets hard-killed mid-write instead of
  exiting cleanly. Added an explicit -timeout (HEXSTRIKE_AMASS_TIMEOUT_MIN,
  default 4 min), skipped if the caller already supplied one.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants