Kali Linux on Docker · Part 15 of 16
Build a Kali Linux DevOps Toolbox With Docker
Series curriculum (16 lessons)
You have spent this series learning the pieces one at a time: running a Kali container, building a custom image, mounting volumes, wiring up Docker networking, scanning with nmap, and locking down container security. This capstone pulls all of it into one thing you can actually keep — a small, repository-style project you clone into any environment and use to answer the same three questions every DevOps engineer asks when something breaks: Is DNS resolving? Is the service responding? Is TLS healthy? By the end you will have a reproducible Kali DevOps toolbox that lives in Git, builds anywhere Docker runs, and carries a handful of safe, explained diagnostic scripts.
🛠️ DevOps Perspective — The value here is not any single command. It is that the whole environment — tools, versions, and scripts — is captured as code. Anyone on your team gets an identical toolbox from one
docker compose build, and a laptop reimage never loses your setup again.
🔐 Security Note — Only scan, inspect, or test systems you own or have explicit permission to assess. Every script in this toolbox takes an explicit target argument so it only ever touches an endpoint you named on purpose. Point it at your own lab services (
web,api) or benign public endpoints likeexample.com— never a third party you have no authorization to test.
The project layout
A reusable toolbox is a repository, not a container you built once and forgot how. Structure it so a teammate can read the tree and understand the whole thing in ten seconds:
kali-devops-toolbox/
│
├── Dockerfile
├── compose.yaml
├── README.md
│
├── scripts/
│ ├── dns-check.sh
│ ├── http-check.sh
│ ├── tls-check.sh
│ └── port-check.sh
│
└── workspace/
Dockerfile— the recipe that bakes Kali plus exactly the tools you need into a small, reproducible image.compose.yaml— one command to build and run the toolbox with consistent volumes and networking, so nobody has to remember a longdocker runline.README.md— how to build, how to run, and the one rule about authorized targets. Future-you will thank present-you.scripts/— the diagnostics. Each is small, single-purpose, and requires an explicit target, so the toolbox is safe by construction.workspace/— a mounted directory where captures, output, and scratch files land on your host, surviving container restarts.
Create the directory and initialize it as a Git repository so the whole thing is versioned from the start:
mkdir -p kali-devops-toolbox/scripts kali-devops-toolbox/workspace
cd kali-devops-toolbox
git init
mkdir -p creates the nested directories in one call and does not error if a path already exists. git init turns the folder into a repository — the toolbox is now code you can commit, share, and roll back.
The Dockerfile
The image bakes in only the tools these scripts need. Smaller surface, faster builds, fewer things to patch.
FROM kalilinux/kali-rolling
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
curl \
dnsutils \
iproute2 \
iputils-ping \
jq \
ncat \
nmap \
openssl \
tcpdump \
traceroute \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /toolbox
COPY scripts/ /toolbox/scripts/
RUN chmod +x /toolbox/scripts/*.sh
CMD ["/bin/bash"]
Walking it line by line:
FROM kalilinux/kali-rolling— start from the official Kali base image. Everything below is layered on top of it.RUN apt-get update && apt-get install -y --no-install-recommends ...— install the tools.--no-install-recommendsskips optional extras so the image stays lean, and-yanswers yes to prompts so the build is non-interactive.dnsutilsprovidesdig,ncatprovidesnc, and the rest give you curl, nmap, openssl, and tcpdump.&& apt-get clean && rm -rf /var/lib/apt/lists/*— delete the package cache in the same layer it was created. Because Docker layers are additive, cleaning up in a laterRUNwould not shrink the image — the cache would still exist in the earlier layer. Chaining it here actually reduces the final size.WORKDIR /toolbox— set the working directory; it is created if missing, and later commands run relative to it.COPY scripts/ /toolbox/scripts/— bake the diagnostic scripts into the image so the container is self-contained even without a mounted volume.RUN chmod +x /toolbox/scripts/*.sh— make them executable inside the image.CMD ["/bin/bash"]— the default command when the container starts with no other instruction: drop you into a shell.
💡 Note — Because
COPY scripts/sits below theapt-getlayer, editing a script only invalidates the cache from theCOPYdown. The expensive install layer is reused, so rebuilds after a script tweak take seconds, not minutes. Ordering your Dockerfile from least-to-most frequently changed is the single biggest build-speed win.
The compose file
compose.yaml turns “remember this long docker run invocation” into docker compose run. It captures the image, the mounted workspace, and a sensible security posture in one file everyone shares.
services:
toolbox:
build: .
image: kali-devops-toolbox
container_name: kali-devops-toolbox
volumes:
- ./workspace:/workspace
- ./scripts:/toolbox/scripts:ro
working_dir: /toolbox
cap_drop:
- ALL
cap_add:
- NET_RAW
security_opt:
- no-new-privileges:true
stdin_open: true
tty: true
command: /bin/bash
build: .— build from the Dockerfile in this directory;image:names the result so it is easy to reuse.volumes:— mount the host./workspaceat/workspaceso output persists on your machine, and mount./scriptsread-only (:ro) so the running container can execute them but never modify them. Editing a script on the host is picked up instantly, no rebuild needed.cap_drop: ALLthencap_add: NET_RAW— this is least privilege in action. Drop every Linux capability, then add back onlyNET_RAW, which nmap and tcpdump need for raw packets. This is the correct answer instead of--privileged, which would hand the container nearly every host capability at once. Grant the one thing that is needed, nothing more.security_opt: no-new-privileges:true— prevents any process in the container from gaining more privileges than it started with (e.g. via setuid binaries), a cheap and strong hardening flag.stdin_open: true/tty: true— the compose equivalents of-iand-t, keeping STDIN open and allocating a TTY so you get an interactive shell.
🔐 Security Note — Notice what is not here: no
--privileged, no host networking, and no/var/run/docker.sockmount.--privilegedremoves almost all container isolation; mounting the Docker socket effectively grants root on the host. A troubleshooting toolbox never needs either.cap_drop: ALL+ a singlecap_add: NET_RAWgives packet capture its one legitimate capability while keeping every other door shut.
Build and drop into a shell:
docker compose build
docker compose run --rm toolbox
docker compose build builds the image using the Dockerfile. docker compose run --rm toolbox starts an interactive container from the toolbox service; --rm deletes the container when you exit so you never accumulate dead containers.
The scripts
Every script follows the same defensive pattern, and understanding that pattern is more important than any single tool. Two lines do the heavy lifting.
set -euo pipefail makes Bash fail fast instead of limping along:
-e— exit immediately if any command returns a non-zero status, so a failed step stops the script rather than the next line running on bad state.-u— treat an unset variable as an error, catching typos and missing arguments instead of silently expanding to an empty string.-o pipefail— make a pipeline fail if any command in it fails, not just the last one. Without this,dig ... | grep ...would report success even ifdigblew up.
TARGET="${1:?Usage: $0 hostname}" is a usage guard. The ${1:?message} form means “expand to the first argument, but if it is unset or empty, print message and exit non-zero.” Combined with -u, this guarantees a script can never run against an empty or accidental target — it forces you to name what you are checking. That is exactly the property you want in a security-adjacent tool: it only ever touches an endpoint you explicitly typed.
scripts/dns-check.sh — is the name resolving?
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:?Usage: $0 hostname}"
dig +short "$TARGET"
dig +short "$TARGET" queries DNS for the hostname and prints just the resolved records — no verbose header, just the answer. This is the first question when a service is unreachable: does the name even resolve, and to the address you expect? An empty result means DNS is your problem, not the service.
scripts/http-check.sh — is the service responding?
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:?Usage: $0 URL}"
curl -fsSIL "$TARGET"
The curl flags each earn their place: -f makes curl exit non-zero on an HTTP error (like a 500) instead of printing the error body and reporting success — which pairs perfectly with set -e. -s silences the progress meter. -S keeps error messages visible even with -s. -I fetches only the response headers (a HEAD request), so you see status and metadata without downloading a body. -L follows redirects so a 301/302 does not read as a dead endpoint. The result tells you the HTTP status, server, and redirect chain at a glance.
scripts/tls-check.sh — is the certificate healthy?
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:?Usage: $0 host:port (e.g. example.com:443)}"
echo | openssl s_client -connect "$TARGET" -servername "${TARGET%%:*}" 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
This one has two stages. openssl s_client -connect "$TARGET" opens a TLS connection to the authorized host:port you specified; -servername "${TARGET%%:*}" sends SNI using the hostname portion (${TARGET%%:*} strips everything from the first colon onward), which matters for servers hosting multiple certificates. The leading echo | feeds an immediate EOF so s_client does not hang waiting for input, and 2>/dev/null discards its chatty handshake log. The output is piped into openssl x509 -noout -subject -issuer -dates, which parses the presented certificate and prints just the subject (who it is for), issuer (who signed it), and validity dates (notBefore/notAfter). This is how you confirm a cert is the right one and not about to expire — the single most common “it worked yesterday” outage cause.
scripts/port-check.sh — is the port open?
#!/usr/bin/env bash
set -euo pipefail
TARGET="${1:?Usage: $0 host}"
PORTS="${2:-22,80,443}"
nmap -Pn -p "$PORTS" "$TARGET"
nmap -Pn -p "$PORTS" "$TARGET" scans the specified ports on the authorized host. -Pn skips host discovery and treats the target as up — useful when ICMP is filtered, which it usually is in cloud environments. -p "$PORTS" limits the scan to the ports you care about; PORTS="${2:-22,80,443}" uses a second argument if given, otherwise defaults to SSH/HTTP/HTTPS via the ${2:-default} fallback form. This answers “is the service listening where I think it is, or is a firewall or security group in the way?” If you prefer a dependency-free single-port check, nc -zv "$TARGET" 443 (from ncat) does the same for one port: -z scans without sending data, -v reports the result.
🔐 Security Note —
nmapandtcpdumpneed theNET_RAWcapability the compose file grants — that is the whole reason it is there, and nothing broader. Point these scripts only at hosts in your own lab or ones you are explicitly authorized to assess. A port scan against a stranger’s infrastructure can be a legal problem, not a curiosity.
Try It Yourself
Build the toolbox and run each check against benign, public endpoints you are allowed to test:
🧪 Try It — From the project root:
docker compose build docker compose run --rm toolbox ./scripts/dns-check.sh example.com docker compose run --rm toolbox ./scripts/http-check.sh https://example.com docker compose run --rm toolbox ./scripts/tls-check.sh example.com:443 docker compose run --rm toolbox ./scripts/port-check.sh example.comEach
docker compose run --rm toolbox <command>starts a fresh container, runs one script, and removes the container on exit. Expected state: DNS returns an address, HTTP returns200, TLS shows a valid issuer and futurenotAfterdate, and the port scan reports443/tcp open. Anything else is a lead to follow.
Common Problems
Symptom: dns-check.sh returns nothing. The name did not resolve. Diagnostic steps: confirm the container has working DNS with docker compose run --rm toolbox cat /etc/resolv.conf, then try a name you know resolves (example.com). If a public name works but your internal service does not, the container is using the wrong resolver — you likely need a user-defined Docker network so containers resolve each other by name (see the networking lessons below).
Symptom: port-check.sh reports filtered or the container exits with a capability error. filtered means a firewall or cloud security group is dropping packets — that is a finding, not a script bug. If instead nmap complains it cannot send raw packets, the NET_RAW capability is missing; confirm cap_add: NET_RAW is present in compose.yaml, and resist the urge to “fix” it with --privileged.
Symptom: tls-check.sh prints nothing or hangs. A hang usually means you connected to a plain-HTTP port with s_client; check you used :443, not :80. Empty output means the handshake failed — rerun without 2>/dev/null to see the real error (expired cert, wrong SNI, or a host that does not speak TLS on that port).
Symptom: “permission denied” running a script. The file lost its executable bit on the host. Fix with chmod +x scripts/*.sh. Because ./scripts is mounted read-only into the container, always set permissions on the host side.
How the toolbox becomes a reusable component
The payoff of packaging this as code is that the same artifact drops into wildly different contexts without change:
- Developer workstations — clone the repo,
docker compose build, and every engineer has the identical toolbox regardless of their host OS. No “works on my machine” over tool versions; the image pins them. A laptop reimage is agit cloneaway from a full recovery. - Incident troubleshooting — during an outage you do not want to be
apt-get install-ing tools on a stressed box. Spin up the disposable toolbox, run the DNS/HTTP/TLS/port checks to localize the fault, then throw the container away. It leaves no trace on the host and touches nothing you did not point it at. - CI/CD jobs — reference the image as a job step to validate infrastructure as part of a pipeline: confirm a freshly deployed service resolves, responds, and presents a valid certificate before the pipeline marks the deploy healthy. Because the scripts exit non-zero on failure (thanks to
set -eandcurl -f), they double as pass/fail gates with no extra glue. - Ephemeral cloud instances — the whole toolbox is one image plus a few kilobytes of scripts, so it pulls fast onto a throwaway EC2/VM, does its job, and disappears with the instance. Nothing persistent to install, nothing to clean up.
- Kubernetes debugging workflows — run the image as a short-lived debug pod (
kubectl run kali-toolbox --rm -it --image=kali-devops-toolbox -- bash) inside the cluster to check service DNS, in-cluster connectivity, and TLS between pods from the network’s actual vantage point. When you are done, the pod is gone. - Infrastructure validation — point the scripts at your own endpoints after a Terraform apply or config change to assert reality matches intent: the load balancer answers, the cert rotated correctly, the expected ports are open and no more. This is the “Expected State vs Observed State” discipline turned into a repeatable, versioned check.
🏭 Why This Matters in Production — In production, the difference between a five-minute incident and a five-hour one is often how fast you can localize a fault. A toolbox that is already code — reproducible, disposable, and safe by construction — means anyone on call can run the same trusted checks and get the same answers, instead of improvising with whatever happens to be installed on the nearest box. Reproducibility is not a nicety; it is what makes troubleshooting a team capability rather than tribal knowledge.
You Built a Kali DevOps Toolbox
Step back and look at how much you now understand. Over this series you have gone from “what is a Kali container” to shipping a versioned, hardened, reusable diagnostic environment. You can now:
- Run and dispose of containers as lightweight, isolated environments instead of installing tools on your host.
- Build custom images with a Dockerfile, ordering layers for cache efficiency and cleaning up package caches in-layer for a small footprint.
- Use volumes to persist output on the host and mount scripts read-only into a container.
- Reason about Docker networking and DNS resolution, including why user-defined networks let containers find each other by name.
- Wire everything together with compose so a whole environment builds and runs from a single file.
- Discover services with nmap, resolve names with dig, check services with curl, inspect certificates with openssl, and capture packets with tcpdump — each with the flags understood, not copy-pasted.
- Apply container permissions and security as least privilege:
cap_drop: ALLplus a singlecap_add: NET_RAW,no-new-privileges, read-only mounts, and a firm no to--privilegedand the Docker socket. - Turn all of it into reusable automation — small, guarded scripts that fail fast and only ever touch targets you name.
That is a real, portable capability, not a pile of one-off commands.
Continue Your Kali Linux Journey
There is one bonus lesson ahead: Kali Security Testing in CI/CD, which takes this toolbox and wires it into a pipeline so infrastructure validation and security checks run automatically on every deploy. When you are ready to explore other paths, head back to the Kali Linux hub for the full curriculum.
To revisit the building blocks this capstone assembled:
- Build a Custom Kali Linux Docker Image — the Dockerfile foundations.
- Docker Compose Security Lab — multi-service labs with compose.
- Container Security Best Practices — least privilege, capabilities, and hardening.
- Nmap Service Discovery — the scanning techniques behind
port-check.sh. - Your First DevOps Security Lab — where the hands-on journey began.
You may also want the broader Docker Academy and the client-side Docker Compose Generator to extend your compose.yaml further.
What You Learned
- A reusable Kali DevOps toolbox is a versioned repository — Dockerfile,
compose.yaml, scripts, and a mounted workspace — not a container you built once and forgot. - The
set -euo pipefail+${1:?...}pattern makes scripts fail fast and refuse to run without an explicit target, which is exactly what keeps a diagnostic tool safe. - Four small scripts answer the core troubleshooting questions: DNS resolves (
dig), the service responds (curl), TLS is healthy (openssl s_client), and the port is open (nmap/nc). - Least privilege —
cap_drop: ALLplus a singlecap_add: NET_RAW,no-new-privileges, and read-only mounts — replaces--privilegedand never touches the Docker socket. - The same image drops unchanged into workstations, incident response, CI/CD, ephemeral cloud instances, Kubernetes debugging, and infrastructure validation, turning troubleshooting into a repeatable team capability.
Recommended Reading
- View Book on Amazon Affiliate link
Kali Linux Penetration Testing Bible
A comprehensive reference for structured security-testing workflows with Kali.
- View Book on Amazon Affiliate link
Mastering Hacking With Kali Linux
A practical guide to security-testing techniques with Kali Linux.
- View Book on Amazon Affiliate link
Kali Linux Hacking
An introduction to security-testing concepts using Kali Linux.
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