Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 14 of 16

Running Kali Containers Securely

Difficulty: Intermediate ~18 min Part 14/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress14 / 16
Series curriculum (16 lessons)

A Kali container is a powerful thing to run on your workstation or in CI, and power cuts both ways: the same broad access that lets Kali sniff packets or scan a subnet can, if you hand it out carelessly, turn a container escape into a host compromise. This lesson is a DevSecOps lesson dressed up as a Docker lesson. The goal is simple: give each container exactly the privileges it needs to do its job, and not one bit more.

We will contrast the blunt instrument (--privileged) with the scalpel (--cap-add), lock filesystems and resources down, and go through the container-security truths that every DevOps engineer eventually learns — ideally before an incident rather than during one.

Containers are not VMs

Start here, because every security decision that follows depends on it. A virtual machine runs its own kernel on virtualized hardware; the hypervisor is a hard wall between guest and host. A container is not that. Every container on a host — Kali included — shares the host’s kernel. Isolation comes from Linux namespaces (what a process can see) and cgroups (what a process can use), plus capabilities and seccomp filters that limit what it can do. These are kernel features, not a separate machine.

The practical consequence: the boundary between a container and your host is a set of kernel-enforced restrictions, and anything that weakens those restrictions moves you closer to “this process is just running on my host as root.” That is the lens for the rest of this lesson.

🔐 Security Note — “root inside the container” is not a harmless sandbox identity. By default it maps to real UID 0 on the host kernel (unless you have enabled user namespace remapping). If a process breaks out of the namespace — through a kernel bug, an over-broad capability, or a careless bind mount — it breaks out as root. Treat container-root with the same respect you treat host-root.

The core idea: least privilege

Least privilege means a process gets only the permissions required for its task. It matters because privileges are the blast radius of a compromise. A container that can only open network sockets can, at worst, misuse the network. A container running --privileged can load kernel modules, access every host device, and reconfigure the host — so a single exploited process in it can own the machine.

You will not always know in advance the exact minimum, and that is fine. The discipline is: start with nothing extra, add one specific thing when a command genuinely fails without it, and verify. Below is the toolkit for doing that.

--cap-add vs --privileged

This is the single most important distinction in the lesson. Linux splits root’s monolithic power into ~40 discrete capabilities (packet capture, changing file ownership, binding low ports, loading modules, and so on). Docker already drops most of them by default and keeps a conservative set. You then adjust from that baseline.

  • --cap-add NET_RAW grants one capability on top of the default set — here, the ability to craft/read raw packets (what tcpdump, ping, and nmap -sS need). Everything else stays dropped.
  • --privileged is not “add all capabilities.” It is far broader: it adds all capabilities and disables the default seccomp filter, disables the AppArmor/SELinux profile, and exposes all host devices under /dev. It essentially removes the guardrails that make a container a container.
--cap-add NET_RAW (scalpel)--privileged (sledgehammer)
Capabilities grantedExactly the one(s) you nameAll of them
Default seccomp filterStill enforcedDisabled
AppArmor / SELinux profileStill appliedDisabled
Host devices (/dev)Not exposedAll exposed
Blast radius if exploitedNarrow, scopedEffectively host root
When to useAlmost alwaysAlmost never

For a Kali packet-capture container you want the first row, not the second:

docker run --rm -it \
  --cap-add NET_RAW \
  --cap-add NET_ADMIN \
  kali-devops-toolbox tcpdump -i eth0
  • --rm removes the container on exit, keeping it disposable.
  • -i keeps STDIN open and -t allocates a TTY for an interactive shell.
  • --cap-add NET_RAW allows crafting and reading raw packets (needed by tcpdump).
  • --cap-add NET_ADMIN allows interface/routing changes (needed for promiscuous mode and some scan types).
  • kali-devops-toolbox is the custom Kali image built earlier in this series; substitute kali-rolling if you have not built it.

You can also go the other direction and drop everything, then add back only what you need — the most explicit form of least privilege:

docker run --rm -it \
  --cap-drop ALL \
  --cap-add NET_RAW \
  kali-devops-toolbox
  • --cap-drop ALL removes every capability, including the ones Docker keeps by default.
  • --cap-add NET_RAW then re-adds just the one this workload needs.

⛔ Production Warning — If a tutorial anywhere tells you to “just add --privileged to make it work,” stop. That is almost always a lazy substitute for finding the one capability that was actually missing. Reach for --cap-add and add capabilities one at a time until the command works. --privileged belongs to a tiny set of legitimate cases (building images inside containers, certain nested-Docker or device-driver scenarios) and should be a deliberate, documented decision — never a reflex.

Run as non-root when feasible

Many Kali tools genuinely need elevated privileges (raw sockets for scanning and capture). But plenty of work — running a script, using an HTTP client, parsing output — does not. When a task does not need root, do not run as root.

docker run --rm -it --user 1000:1000 kali-devops-toolbox bash
  • --user 1000:1000 runs the process as UID 1000 / GID 1000 instead of root, so even a compromised process starts with an unprivileged identity.

For images you build, bake the non-root default into the Dockerfile so nobody has to remember the flag:

FROM kalilinux/kali-rolling
RUN apt-get update \
 && apt-get install -y --no-install-recommends curl jq \
 && apt-get clean \
 && rm -rf /var/lib/apt/lists/* \
 && useradd --create-home --uid 1000 analyst
USER analyst
WORKDIR /home/analyst
CMD ["bash"]
  • FROM sets the base image; RUN executes build steps in a single layer (chaining with && keeps the image small and the cache coherent).
  • apt-get install --no-install-recommends avoids pulling optional extras; apt-get clean and rm -rf /var/lib/apt/lists/* delete the package cache so it does not bloat the image layer.
  • useradd ... analyst creates an unprivileged user; USER analyst makes every subsequent instruction and the running container default to that user instead of root.
  • WORKDIR sets the working directory; CMD is the default command.

🛠️ DevOps Perspective — Non-root by default is a habit that pays off far beyond Kali. Web apps, sidecars, and CI runners rarely need root; running them as root is pure downside. When you do need a capability, add it explicitly with --cap-add while still running as a non-root user — the two are independent controls.

Read-only root filesystem

If a process does not need to write to its own filesystem, don’t let it. A read-only root filesystem means malware or a hijacked process cannot drop persistent tools, tamper with binaries, or rewrite config.

docker run --rm -it \
  --read-only \
  --tmpfs /tmp \
  kali-devops-toolbox
  • --read-only mounts the container’s root filesystem read-only; writes to it fail.
  • --tmpfs /tmp mounts a small in-memory, writable scratch area at /tmp (wiped when the container stops) for tools that need a temp directory — you carve out just the writable paths you actually need instead of leaving the whole filesystem writable.

Volume permissions

Bind mounts and volumes are where isolation quietly leaks. A mount hands the container a window into the host filesystem; how big and how writable that window is, is up to you.

docker run --rm -it \
  -v "$PWD/captures:/captures:ro" \
  kali-devops-toolbox
  • -v "$PWD/captures:/captures" mounts the host’s ./captures directory into the container at /captures.
  • The trailing :ro makes the mount read-only inside the container, so a compromised process cannot modify host files through it. Use :rw (the default) only when the container genuinely must write back, and mount the narrowest directory possible — never your home directory or /.

🔐 Security Note — Bind-mounting broad host paths (-v /:/host, or your whole home directory) hands the container read/write access to the host filesystem and largely defeats isolation. Mount the specific directory a task needs, prefer :ro, and never mount sensitive host paths (/etc, ~/.ssh, ~/.aws, ~/.kube) into a container you are using to poke at things.

Never mount the Docker socket

This one gets its own section because it is the most common self-inflicted container escape.

🔐 Security NoteDo not mount /var/run/docker.sock into a container. The Docker daemon runs as root on the host, and the socket is its unauthenticated control API. Any process that can reach that socket can start a new container that bind-mounts the host’s / and runs as root — i.e., it can read every file on the host, add users, and execute arbitrary commands as host root. Mounting the socket is functionally equivalent to giving the container root on your machine. There is no “read-only socket” that fixes this; :ro on the socket does not help, because the danger is the API calls it permits, not writes to the socket file. If you think you need it (Docker-in-Docker for CI, dynamic container orchestration), reach for safer patterns — a rootless/sysbox runtime, a scoped API proxy, or the host’s own orchestrator — and treat socket access as the near-root grant it is. This lesson deliberately shows no example that mounts it.

Network isolation

Give a container the least network reach it needs. The default bridge already isolates containers from the host’s network stack; two settings sit at the extremes:

# No network at all — good for offline analysis of a captured file
docker run --rm -it --network none kali-devops-toolbox

# Attach to an isolated user-defined lab network — good for scanning lab services
docker run --rm -it --network security-lab kali-devops-toolbox
  • --network none gives the container no network interface (beyond loopback) — ideal when a task only needs to process local files.
  • --network security-lab attaches it to an isolated user-defined network alongside your lab targets (see Docker networking for Kali).

🔐 Security Note — Avoid --network host. Host networking removes the network namespace entirely: the container shares the host’s interfaces, ports, and loopback directly. That erases a whole layer of isolation (a service bound to 127.0.0.1 on the host suddenly reachable from the container, port conflicts, and the container able to sniff host traffic). Use a user-defined bridge network for labs; reach for --network host only for a deliberate, understood reason.

Secrets

Two ways to handle secrets are wrong, and both are common. Never bake secrets into an image (they persist in the layer history for anyone who pulls it), and prefer not to pass them as -e environment variables (they show up in docker inspect, in the process environment, and often in logs). Mount secrets as files at runtime, or use your orchestrator’s secret store:

docker run --rm -it \
  -v "$PWD/api-token:/run/secrets/api-token:ro" \
  kali-devops-toolbox
  • The secret lives in a file on the host and is mounted read-only at a well-known path; the app reads it from there.
  • Nothing sensitive is embedded in the image or exposed in the environment listing. In Compose, use the secrets: block; in Kubernetes, a mounted Secret.

Minimal packages

Every package you install is more attack surface and another thing to patch. Install only the tools a given image actually needs (--no-install-recommends keeps optional extras out), and build purpose-specific images rather than one bloated everything-image. A smaller image also pulls faster in CI and has fewer CVEs to triage.

Image provenance and updates

You are running someone else’s software as (container) root, so where it came from matters.

  • Provenance: pull from official/trusted sources — the official kalilinux/kali-rolling image, official tool images, or images you build yourself from a Dockerfile you can read. Pin to specific digests (image@sha256:...) for reproducible, tamper-evident builds rather than trusting a mutable :latest tag. Scan images with a vulnerability scanner before relying on them.
  • Updates: an image is a point-in-time snapshot; the day after you build it, new CVEs exist. Rebuild regularly so apt-get update && apt-get upgrade picks up patched packages, and re-pull base images rather than running a months-old snapshot indefinitely.

Logging

You cannot investigate what you did not record. Containers write logs to stdout/stderr by default, captured by Docker’s logging driver and readable with docker logs <container>. For anything long-lived, ship those logs somewhere durable (a central log stack) so a container that is later --rm’d does not take its evidence with it, and so you can answer “what did this container do?” after the fact. Cap on-disk log growth with the driver’s max-size/max-file options to avoid filling the host disk.

Resource limits

Limits are a security control, not just a performance knob: they cap the damage a runaway, misconfigured, or malicious container can do to the host and its neighbors (a container with no memory limit can OOM the whole machine; one with no CPU limit can starve everything else).

docker run --rm -it \
  --memory 512m \
  --cpus 1.5 \
  --pids-limit 200 \
  kali-devops-toolbox
  • --memory 512m caps the container at 512 MB of RAM; exceed it and the kernel OOM-kills the container instead of the host.
  • --cpus 1.5 limits it to 1.5 CPU cores’ worth of compute, so a busy scan cannot monopolize the host.
  • --pids-limit 200 caps the number of processes, blunting fork-bomb-style resource exhaustion.

Hardening with --security-opt

--security-opt tunes the kernel security modules layered on top of namespaces. The most useful one for least privilege:

docker run --rm -it \
  --security-opt no-new-privileges \
  kali-devops-toolbox
  • --security-opt no-new-privileges sets the kernel no_new_privs bit, which prevents a process from gaining privileges later via setuid binaries or file capabilities — even if one slips into the image. It is a cheap, high-value safety net.
  • Other forms include --security-opt seccomp=<profile.json> (apply a custom syscall filter — do not disable seccomp with seccomp=unconfined) and --security-opt apparmor=<profile> (apply an AppArmor profile). Keep the defaults on unless you have a profile that is stricter, never looser.

Putting it together

A locked-down Kali run for a lab task looks like this — least privilege on every axis at once:

docker run --rm -it \
  --user 1000:1000 \
  --cap-drop ALL \
  --cap-add NET_RAW \
  --security-opt no-new-privileges \
  --read-only --tmpfs /tmp \
  --memory 512m --cpus 1.5 --pids-limit 200 \
  --network security-lab \
  -v "$PWD/captures:/captures:ro" \
  kali-devops-toolbox

Read it top to bottom: non-root user, no capabilities except the one this task needs, no privilege escalation, read-only filesystem with a scratch /tmp, bounded resources, an isolated network, and a narrow read-only mount. No --privileged, no host networking, no host mounts, no Docker socket.

🏭 Why This Matters in Production — These are not “Kali quirks”; they are the exact controls a platform team enforces on every workload. In production, --privileged, --network host, broad bind mounts, and a mounted docker.sock are the findings that show up in an audit and the paths attackers actually use to turn one compromised container into a compromised node. Kubernetes bakes the same ideas into securityContext (runAsNonRoot, readOnlyRootFilesystem, capabilities.drop, allowPrivilegeEscalation: false) and Pod Security Standards. Practicing least privilege on your disposable Kali containers builds the muscle memory that keeps a real cluster out of the incident channel. Our Docker Production Readiness Auditor checks many of these settings automatically.

Try It Yourself

🧪 Try It — Prove that a narrow capability beats --privileged for the same task.

Only inspect or test containers and services you own or have explicit permission to assess.

  1. Try packet capture with no extra privileges and watch it fail: docker run --rm -it kali-devops-toolbox tcpdump -i eth0 (expect a permissions error).
  2. Now grant just the missing capability: docker run --rm -it --cap-add NET_RAW kali-devops-toolbox tcpdump -c 3 -i eth0. It works — with one capability, not all of them.
  3. Add the rest of the hardening: run the “Putting it together” command above (drop --network security-lab and the volume if you have not set them up) and confirm your tools still work.
  4. From inside a --read-only container, try to write to the root filesystem: touch /oops should fail, while touch /tmp/ok (the tmpfs) succeeds.
  5. Confirm the resource cap: docker inspect -f '{{.HostConfig.Memory}}' <container> should report 536870912 (512 MB).

Expected state: step 1 fails, step 2 succeeds with a single --cap-add, and the hardened container runs your tools without ever needing --privileged. If observed state differs, see Common Problems.

Common Problems

Symptom: a tool fails with Operation not permitted even though you run as root in the container.

Root-in-container is still constrained by the capability set. The tool needs a capability that is currently dropped.

  • Diagnose: check what the tool actually needs (packet tools → NET_RAW/NET_ADMIN; binding port <1024 → NET_BIND_SERVICE). Run once with the suspected --cap-add and see if the error clears.
  • Fix: add that one capability with --cap-add. Do not jump to --privileged — that “fixes” it only by removing every guardrail. Add capabilities one at a time until the command works, then stop.

Symptom: the container exits immediately or a tool errors writing to disk.

A --read-only filesystem is doing its job, but the tool needs a writable path you have not provided.

  • Diagnose: rerun without --read-only; if it works, the tool writes somewhere on the root filesystem.
  • Fix: identify the path (often /tmp, /var/run, or a cache dir) and add a --tmpfs <path> or a dedicated volume for just that path — keep the rest read-only.

Symptom: the container is OOM-killed (exit code 137).

It hit the --memory limit.

  • Diagnose: docker inspect -f '{{.State.OOMKilled}}' <container> returns true.
  • Fix: raise --memory to a sane value for the workload, or reduce what the tool loads. Do not remove the limit entirely — an unbounded container can take the host down with it.

🔎 Troubleshooting Tip — When a hardened run misbehaves, remove your restrictions one at a time to find the single one responsible, then add back a narrow accommodation for it (one capability, one tmpfs path, a slightly higher limit). Resist the urge to strip all hardening at once — that just hides which control you actually needed to adjust.

Best-practices checklist

  • No --privileged — use narrow --cap-add (and --cap-drop ALL first when you can).
  • Run as a non-root --user whenever the task allows; bake USER into your Dockerfiles.
  • --read-only root filesystem plus a --tmpfs for only the paths that must be writable.
  • Bind mounts are narrow and :ro by default; never mount /, home, or credential directories.
  • Never mount /var/run/docker.sock.
  • Prefer user-defined bridge networks or --network none; avoid --network host.
  • Secrets come in as mounted files or via a secret store — never in the image or -e variables.
  • Minimal packages (--no-install-recommends), from trusted/pinned images, rebuilt regularly for patches.
  • --security-opt no-new-privileges; keep default seccomp/AppArmor on (never unconfined).
  • --memory, --cpus, and --pids-limit set so one container cannot starve the host.
  • Logs shipped somewhere durable before the container is disposed.

Where to go next

What You Learned

  • Containers share the host kernel and are not VMs; container-root has real host implications, so least privilege is a security control, not a nicety.
  • --cap-add grants one specific capability while --privileged removes seccomp, AppArmor, and device isolation wholesale — reach for the scalpel, almost never the sledgehammer.
  • How to harden a run end to end: non-root --user, --cap-drop ALL + narrow --cap-add, --read-only with --tmpfs, :ro mounts, isolated networking, file-based secrets, and --security-opt no-new-privileges.
  • --memory, --cpus, and --pids-limit cap a container’s blast radius on the host; minimal packages, trusted/pinned images, regular rebuilds, and durable logging round out the posture.
  • Never mount /var/run/docker.sock — it grants near-root power over the host — and avoid --network host and broad bind mounts for the same isolation-destroying reason.

Affiliate Disclosure: Some links on this page are affiliate links. If you purchase through one of these links, DevOps AI Toolkit may earn a commission at no additional cost to you. See our affiliate disclosure.

← Back to Kali Linux on Docker Back to Kali Linux

Related on DevOps AI Toolkit