DEV Community

Cover image for Dockerizing a Kali Security Toolset Without Losing a Weekend
Mikalai Karytka
Mikalai Karytka

Posted on

Dockerizing a Kali Security Toolset Without Losing a Weekend

I wanted a reproducible container: a Kali base, a focused set of web-pentest tools, and a Python service on top. Simple, in theory. In practice I hit three walls that turn a 20-minute Dockerfile into an afternoon. All three are on Apple Silicon (arm64), and all three are avoidable once you know them.

This is the writeup I wanted while fighting it. Examples use a Kali base image and an open-source MCP tool server, but the lessons apply to any Python-on-Kali build.

Wall 1: Kali ships Python 3.14, and the wheels aren't there yet

The naive Dockerfile:

FROM kalilinux/kali-rolling
RUN apt-get update && apt-get install -y python3 python3-venv python3-pip
RUN python3 -m venv /opt/venv && /opt/venv/bin/pip install -r requirements.txt
Enter fullscreen mode Exit fullscreen mode

…dies on the pip step with something like:

error: [Errno 2] No such file or directory: 'aarch64-linux-gnu-gcc'
ERROR: Failed building wheel for cffi
ERROR: Failed to build 'zstandard' ...
Enter fullscreen mode Exit fullscreen mode

Two things collided. Kali rolling moved to Python 3.14, and for arm64 there are no prebuilt wheels yet for the heavy C/Rust dependencies (cffi, zstandard, cryptography, pydantic-core, aiohttp, psutil). pip falls back to compiling from source, and there's no compiler in the image.

You could install a full toolchain and compile everything. It works, but it's slow (Rust builds for cryptography and pydantic-core are not quick) and it bloats the image.

Fix: let apt do the heavy lifting, pip the rest

Kali packages most of those heavy libs as prebuilt apt packages. Install those, create the venv with --system-site-packages so it inherits them, and pip-install only the handful that apt doesn't carry.

# Prebuilt heavy deps from apt (no compiling), plus a small toolchain for the rest
RUN apt-get update && apt-get install -y --no-install-recommends \
      python3 python3-venv python3-pip python3-dev \
      build-essential libffi-dev libssl-dev pkg-config cargo rustc \
      mitmproxy python3-flask python3-requests python3-psutil \
      python3-bs4 python3-selenium python3-aiohttp \
    && rm -rf /var/lib/apt/lists/*

# venv inherits the apt libs; pip only adds what's missing
COPY requirements-lean.txt /opt/requirements-lean.txt
RUN python3 -m venv --system-site-packages /opt/venv \
    && /opt/venv/bin/pip install --no-cache-dir -r /opt/requirements-lean.txt
Enter fullscreen mode Exit fullscreen mode

requirements-lean.txt then contains only the packages apt can't provide: in my case two pure-Python ones plus pydantic-core, which the small cargo/rustc toolchain compiles quickly. The build went from "fails" to a couple of minutes.

The general principle: on a bleeding-edge Python, prefer the distro's prebuilt packages over pip wheels for anything with a C/Rust extension. --system-site-packages is the bridge that lets a venv use them.

Wall 2: a tool name that isn't the tool you think

My service shelled out to httpx and every call failed with:

Error: No such option: -l
Enter fullscreen mode Exit fullscreen mode

The trap: on Kali, /usr/bin/httpx is the Python HTTP client, not ProjectDiscovery's httpx probe. The security tool is packaged as httpx-toolkit. Same name, completely different CLI.

Fix: point the name you call at the tool you meant.

RUN apt-get install -y --no-install-recommends httpx-toolkit \
 && if [ -x /usr/bin/httpx-toolkit ]; then \
      ln -sf /usr/bin/httpx-toolkit /usr/local/bin/httpx; \
    fi
Enter fullscreen mode Exit fullscreen mode

Lesson that generalizes: in a big toolbox, verify what a binary actually is (command -v, <tool> -version) before wiring it into automation. Name collisions between a language package and a security tool are more common than you'd expect (http, dig, ab, and friends).

Wall 3: installing 150 tools and failing on the one that moved

A single apt line with dozens of packages fails entirely if one name changed or isn't available for your arch. Split it: a core line that must succeed, and a best-effort line that tolerates individual misses.

# core: must succeed
RUN apt-get update && apt-get install -y --no-install-recommends \
      nmap nuclei sqlmap gobuster ffuf nikto subfinder \
    && rm -rf /var/lib/apt/lists/*

# best-effort: don't sink the build over one unavailable package
RUN apt-get update && for p in katana dalfox hakrawler httpx-toolkit chromium ; do \
      apt-get install -y --no-install-recommends "$p" || echo "[skip] $p"; \
    done; rm -rf /var/lib/apt/lists/*
Enter fullscreen mode Exit fullscreen mode

Now a renamed or arch-missing extra prints [skip] <name> instead of failing the whole image, and you can see at a glance what didn't make it.

A few more things that paid off

  • Pin the source. If you clone an upstream tool inside the image, check out a specific commit via an ARG so rebuilds are reproducible: ARG TOOL_SHA=....
  • Keep it lean on purpose. I only needed a web-app subset, so I skipped the binary-analysis and forensics tooling. Smaller image, faster build, less to patch.
  • Add a healthcheck for any service in the image so docker ps tells the truth: HEALTHCHECK CMD curl -fsS http://127.0.0.1:PORT/health || exit 1.
  • Bind services to localhost (-p 127.0.0.1:PORT:PORT): a security toolbox is the last thing you want listening on all interfaces by accident.

Takeaway

Three rules save the weekend on a Kali + Python image:

  1. On a brand-new Python, install heavy C/Rust deps from apt, not pip; bridge with a --system-site-packages venv.
  2. Verify binaries before automating them; name collisions are real (httpx vs httpx-toolkit).
  3. Split apt installs into must-succeed and best-effort, pin sources, and keep the image to what you actually use.

Isolation note: run offensive tooling in a sandbox and bind it to localhost. The next piece covers microVM sandboxing for exactly this.

Top comments (0)