Kali Linux on Docker · Part 14 of 16
Running Kali Containers Securely
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_RAWgrants one capability on top of the default set — here, the ability to craft/read raw packets (whattcpdump,ping, andnmap -sSneed). Everything else stays dropped.--privilegedis 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 granted | Exactly the one(s) you name | All of them |
| Default seccomp filter | Still enforced | Disabled |
| AppArmor / SELinux profile | Still applied | Disabled |
Host devices (/dev) | Not exposed | All exposed |
| Blast radius if exploited | Narrow, scoped | Effectively host root |
| When to use | Almost always | Almost 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
--rmremoves the container on exit, keeping it disposable.-ikeeps STDIN open and-tallocates a TTY for an interactive shell.--cap-add NET_RAWallows crafting and reading raw packets (needed bytcpdump).--cap-add NET_ADMINallows interface/routing changes (needed for promiscuous mode and some scan types).kali-devops-toolboxis the custom Kali image built earlier in this series; substitutekali-rollingif 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 ALLremoves every capability, including the ones Docker keeps by default.--cap-add NET_RAWthen re-adds just the one this workload needs.
⛔ Production Warning — If a tutorial anywhere tells you to “just add
--privilegedto make it work,” stop. That is almost always a lazy substitute for finding the one capability that was actually missing. Reach for--cap-addand add capabilities one at a time until the command works.--privilegedbelongs 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:1000runs 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"]
FROMsets the base image;RUNexecutes build steps in a single layer (chaining with&&keeps the image small and the cache coherent).apt-get install --no-install-recommendsavoids pulling optional extras;apt-get cleanandrm -rf /var/lib/apt/lists/*delete the package cache so it does not bloat the image layer.useradd ... analystcreates an unprivileged user;USER analystmakes every subsequent instruction and the running container default to that user instead of root.WORKDIRsets the working directory;CMDis 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-addwhile 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-onlymounts the container’s root filesystem read-only; writes to it fail.--tmpfs /tmpmounts 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./capturesdirectory into the container at/captures.- The trailing
:romakes 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 Note — Do not mount
/var/run/docker.sockinto 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;:roon 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 nonegives the container no network interface (beyond loopback) — ideal when a task only needs to process local files.--network security-labattaches 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 to127.0.0.1on 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 hostonly 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 mountedSecret.
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-rollingimage, 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:latesttag. 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 upgradepicks 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 512mcaps the container at 512 MB of RAM; exceed it and the kernel OOM-kills the container instead of the host.--cpus 1.5limits it to 1.5 CPU cores’ worth of compute, so a busy scan cannot monopolize the host.--pids-limit 200caps 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-privilegessets the kernelno_new_privsbit, 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 withseccomp=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 mounteddocker.sockare 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 intosecurityContext(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
--privilegedfor the same task.
Only inspect or test containers and services you own or have explicit permission to assess.
- 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). - 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. - Add the rest of the hardening: run the “Putting it together” command above (drop
--network security-laband the volume if you have not set them up) and confirm your tools still work. - From inside a
--read-onlycontainer, try to write to the root filesystem:touch /oopsshould fail, whiletouch /tmp/ok(the tmpfs) succeeds. - Confirm the resource cap:
docker inspect -f '{{.HostConfig.Memory}}' <container>should report536870912(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-addand 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>returnstrue. - Fix: raise
--memoryto 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 ALLfirst when you can). - Run as a non-root
--userwhenever the task allows; bakeUSERinto your Dockerfiles. -
--read-onlyroot filesystem plus a--tmpfsfor only the paths that must be writable. - Bind mounts are narrow and
:roby 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
-evariables. - Minimal packages (
--no-install-recommends), from trusted/pinned images, rebuilt regularly for patches. -
--security-opt no-new-privileges; keep default seccomp/AppArmor on (neverunconfined). -
--memory,--cpus, and--pids-limitset so one container cannot starve the host. - Logs shipped somewhere durable before the container is disposed.
Where to go next
- Apply these controls to a real task in capturing packets with tcpdump — the classic
NET_RAWuse case. - Fold the hardening into a reusable image in building a DevOps security toolbox.
- Revisit isolation fundamentals in Docker networking for Kali.
- Audit your own images against these rules with the Docker Production Readiness Auditor.
- Browse related how-tos in the DevOps guides library.
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-addgrants one specific capability while--privilegedremoves 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-onlywith--tmpfs,:romounts, isolated networking, file-based secrets, and--security-opt no-new-privileges. --memory,--cpus, and--pids-limitcap 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 hostand broad bind mounts for the same isolation-destroying reason.
Recommended Reading
- View Book on Amazon Affiliate link
Mastering Kali Linux for Advanced Penetration Testing
An advanced deep-dive into Kali for experienced security testers.
- View Book on Amazon Affiliate link
Kali Linux Penetration Testing Bible
A comprehensive reference for structured security-testing workflows with Kali.
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