Kali Linux on Docker · Part 16 of 16
Kali Docker in CI/CD: Defensive Validation
Series curriculum (16 lessons)
A Kali image in CI/CD is not an attack rig — it is a fast, disposable validation stage. After your pipeline deploys a service, a short-lived Kali container asks the same questions a careful engineer would ask by hand: does the name resolve, does the endpoint answer, is the certificate valid, and are only the expected ports exposed? If any answer is wrong, the container exits non-zero and the pipeline fails before real users hit the regression. In this lesson you will wire the toolbox image you built earlier into a pipeline stage that validates your own deployment and gates the release on the result.
Only scan, inspect, or test systems you own or have explicit permission to assess. Everything here targets the service your pipeline just deployed, using lab names like
web/apiand benign public endpoints likeexample.com.
The Core Idea: Validation, Not Attack
The distinction that governs this entire lesson is intent and authorization. The exact same tool — dig, curl, or nmap — is a validation step when you point it at infrastructure your pipeline owns and just deployed, and something else entirely when pointed at a third party. This lesson is squarely the former: a DevSecOps quality gate that treats “is the TLS cert about to expire?” the same way a unit test treats “does this function return the right value?”
🔐 Security Note — Every check here runs against the deployment artifact your own pipeline produced, on infrastructure you control, with authorization implicit in owning the pipeline. That authorization is the line that separates defensive validation from unauthorized scanning. Do not repurpose this stage to reach any host your organization does not own. If your checks ever need to target an address outside your deployment, stop and get explicit, written permission first.
⛔ Production Warning — This is not an automated attack pipeline and must never become one. Do not schedule aggressive, high-rate, or exploit-style scans against production from CI — even against your own systems, a hostile scan profile can trip rate limits, WAFs, and IDS, and page on-call for a self-inflicted “incident.” Keep the checks lightweight, read-only, and idempotent: resolve a name, request an endpoint, read a certificate, confirm a port list. Nothing here mutates state or probes for vulnerabilities.
What “Defensive Validation” Actually Checks
Four cheap, deterministic questions catch a surprising share of real deploy regressions:
- DNS — does the service name resolve to an address at all? A deploy that renames a service or lands on the wrong network fails here first.
- HTTP — does the endpoint return the status you expect (a
200, or a deliberate301)? Catches a crashed process, a bad route, or a health check wired to the wrong path. - TLS — is the certificate present, trusted, and not about to expire? Catches the classic 2 a.m. outage where a cert silently lapsed.
- Expected exposure — are only the ports you intended actually open? Catches a debug port, a database, or an admin interface accidentally published to the world.
Each maps to a tool already in the toolbox: dig/getent for DNS, curl for HTTP, openssl s_client for TLS, and a narrow nmap for the port list.
🏭 Why This Matters in Production — DNS, TLS, and exposure regressions share a nasty trait: the deploy “succeeds,” the app boots, and everything looks green — until a user in a different network hits an unresolved name, or a browser rejects an expired cert, or a scanner finds the database port you forgot to close. A 20-second validation stage catches all three inside the pipeline, where a fix is a code change, instead of in production, where it is an incident.
The Validation Pipeline
Conceptually, validation is one stage that runs after deploy and before you call the release good:
Build
↓
Deploy
↓
Kali Validation Container
↓
DNS Test
↓
HTTP Test
↓
TLS Test
↓
Expected Exposure Test
↓
Pass / Fail
The container is disposable — it starts, runs the four checks against the freshly deployed service, and is thrown away. That is exactly the property that made a Kali Docker image attractive in the first place: a reproducible, isolated, portable environment you can spin up identically on any runner and delete when it is done.
Reusing the Toolbox Image
You do not build a bespoke image here — you reuse the purpose-built toolbox from Build a DevOps troubleshooting toolbox. That image already bundles dnsutils, curl, openssl, and nmap on a slim Kali base, plus small check scripts. The whole point of building it once was so that CI/CD, your laptop, and an incident responder all run the identical toolset.
Assume the toolbox exposes four small scripts (each exits non-zero on failure) that you can call by name:
check-dns.sh <name>— resolves<name>, fails if it does not resolve.check-http.sh <url> <expected-code>— requests<url>, fails if the status code differs.check-tls.sh <host> <port> <min-days>— reads the cert, fails if it is invalid or expires within<min-days>.check-ports.sh <host> <allowed-csv>— scans a small port set, fails if any unexpected port is open.
💡 Note — If you have not built the toolbox yet, these are thin wrappers around one command each:
getent hosts,curl -s -o /dev/null -w '%{http_code}',openssl s_client … | openssl x509 -checkend, and a narrownmap -p <list> --open. The lesson linked above assembles them into the image; here we just call them from CI.
Each script following the same “exit 0 on pass, non-zero on fail” contract is what lets the pipeline gate on them — a runner treats any non-zero exit from a job step as a failed job.
A Minimal CI Job
Here is a generic GitLab CI stage that runs after deploy. It uses the toolbox image as the job image, so the check scripts and their tools are already on PATH. TARGET is the service the previous stage just deployed — an internal DNS name your runner can reach, not a public third party.
validate-deployment:
stage: validate
image: registry.example.com/devops/kali-toolbox:latest
variables:
TARGET: "web.internal" # the service this pipeline just deployed
URL: "https://web.internal/healthz"
EXPECT_CODE: "200"
TLS_MIN_DAYS: "14" # fail if the cert expires within two weeks
ALLOWED_PORTS: "80,443" # everything else being open is a regression
script:
- check-dns.sh "$TARGET"
- check-http.sh "$URL" "$EXPECT_CODE"
- check-tls.sh "$TARGET" 443 "$TLS_MIN_DAYS"
- check-ports.sh "$TARGET" "$ALLOWED_PORTS"
rules:
- if: '$CI_COMMIT_BRANCH == "main"'
What each part does and why it gates the release:
stage: validateplaces this job afterdeployin the pipeline order, so it runs against a live deployment.image:pulls the reusable toolbox, so no per-job installation ofdig/curl/openssl/nmap— reproducibility comes from the image, not from apt commands scattered across jobs.- The
variables:block keeps every expectation (URL, expected status, cert lead time, allowed ports) in one readable place, so updating a check is a config edit, not a script rewrite. - Each
script:line runs one check script. If any script exits non-zero, GitLab marks the job failed and the pipeline fails — that is the entire gating mechanism. Because the steps run in order, a DNS failure short-circuits before the later checks even try. rules:scopes the gate tomainso you validate real deployments, not every feature branch (adjust to your flow).
The same idea works in any runner as a plain shell job — the check scripts and their exit codes are the contract, not GitLab specifically:
#!/usr/bin/env bash
set -euo pipefail # -e: exit on first failure; -u: error on unset vars; -o pipefail: fail if any pipe stage fails
TARGET="web.internal"
check-dns.sh "$TARGET"
check-http.sh "https://$TARGET/healthz" 200
check-tls.sh "$TARGET" 443 14
check-ports.sh "$TARGET" "80,443"
echo "Deployment validation passed."
set -euo pipefailis what makes exit codes matter: the first failing check aborts the script with its non-zero status, which the CI runner reports as a failed job.- The final
echoonly prints if all four checks passed — a clear “green” signal in the job log.
🛠️ DevOps Perspective — Think of this as “Expected State vs Observed State” encoded as a gate. You declare the expected state up front — this name resolves, this endpoint returns
200, this cert has ≥14 days, only80,443are open — and the container reports the observed state. Any gap fails the build. That framing is why these checks belong next to your unit tests, not in a runbook someone remembers to run manually.
Try It Yourself
You can rehearse the whole gate locally with Docker before it ever touches CI — same image, same scripts, same exit-code contract.
🧪 Try It — Stand up a lab target and validate it from the toolbox container:
docker network create labnetdocker run -d --name web --network labnet nginx— the “deployed” service.docker run --rm --network labnet kali-toolbox check-http.sh http://web/ 200—--rmdeletes the container on exit; expect exit0.- Now force a failure:
docker run --rm --network labnet kali-toolbox check-http.sh http://web/ 500— the expected code no longer matches, the script exits non-zero, andecho $?on the host confirms it. That non-zero is exactly what fails a real pipeline.- Try
check-ports.sh web "80"(pass) versuscheck-ports.sh web "22"(fails, because80is open but not in the allow-list) to see the exposure gate in both directions.
Running the identical image locally and in CI is the reproducibility payoff — a check that passes on your laptop passes on the runner for the same reasons.
Common Problems
The validation container can’t reach the service
Symptom: every check fails instantly — check-dns.sh reports the name won’t resolve, or check-http.sh returns a connection error rather than a status code.
Diagnostic steps:
- Confirm network reachability first. The validation container must be on a network path to the target. In Docker, that means the same user-defined network; in CI, it means the runner can actually route to the internal name. From the container,
getent hosts "$TARGET"— no answer means this is a network/DNS problem, not a service problem. - Check the name, not just the wire. If
getentfails but the IP works, you have a DNS regression — exactly whatcheck-dns.shexists to catch. See DNS troubleshooting from a Kali Docker container for the127.0.0.11embedded-resolver path. - Distinguish “unreachable” from “not exposed.” A refused connection on
443might mean the port genuinely isn’t published. Prove which with a narrow port check — nmap service discovery shows how to readopenvsfilteredvsclosedso you don’t blame the network for a missing listener. - Watch for split-horizon names. An internal name the runner resolves may differ from the public one. Validate against the address the deployment actually serves, and never “fix” it by pointing the check at a public third party.
False failures from readiness timing
Symptom: the checks fail on the first run right after deploy but pass on a manual retry — the classic “it’s flaky” report.
Diagnostic steps:
- Recognize the race. Your validation stage started before the service finished coming up. The deploy “completed” (the container is running) but the app inside is still initializing — so DNS may resolve while HTTP still refuses.
- Wait for readiness, don’t sleep blindly. Replace a fixed
sleep 30with a bounded poll: retrycheck-http.shuntil it passes or a timeout elapses, e.g. loop up to ~20 times with a short pause andexit 1if it never comes ready. This distinguishes “slow to start” (eventually passes) from “actually broken” (never passes). - Gate on a real health endpoint. Point the HTTP check at a
/healthzroute that returns200only when dependencies (DB, cache) are truly ready, so a green check means usable, not just listening. - Give TLS a small margin. Freshly issued certs and clock skew on runners can make an “expires in N days” check flap near the boundary. A sensible
TLS_MIN_DAYS(say 14) and NTP-synced runners keep the cert check deterministic.
🔎 Troubleshooting Tip — When a check flaps, ask “does it always fail, or only right after deploy?” Always-fails is a real regression the gate correctly caught. Only-right-after-deploy is almost always readiness timing — fix the wait, not the check. Loosening a check to make flakiness go away just disarms the gate.
Related Lessons
- Build a DevOps troubleshooting toolbox — assemble the reusable image and check scripts this pipeline invokes.
- nmap service discovery — how the expected-exposure check reads open vs filtered vs closed ports.
- TLS certificate troubleshooting — the
opensslmechanics behind the certificate-validity gate. - Docker Academy — deeper hands-on Docker and DevSecOps labs.
- DevOps guides — cross-cutting troubleshooting and deployment-validation walkthroughs.
What You Learned
- A Kali image in CI/CD is a defensive validation stage, not an attack tool — it checks the deployment your own pipeline just produced, with authorization implicit in owning that pipeline.
- Four cheap, deterministic checks — DNS, HTTP status, TLS validity, and expected port exposure — catch a large share of “green deploy, broken service” regressions before users do.
- The gate works entirely through exit codes: a check script exiting non-zero fails the job and the pipeline;
set -euo pipefailand per-line scripts make that automatic. - Reusing the purpose-built toolbox image gives you a reproducible, disposable validation environment that behaves identically on your laptop and on the runner.
- Keep the checks lightweight and read-only — this is a DevSecOps quality gate, never an aggressive automated attack pipeline.
- The two failure modes to expect are unreachable-target (network/DNS path) and false failures from readiness timing (poll for health instead of sleeping), and telling them apart keeps the gate trustworthy.
Recommended Reading
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