Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 16 of 16

Kali Docker in CI/CD: Defensive Validation

Difficulty: Intermediate ~16 min Part 16/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress16 / 16
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/api and benign public endpoints like example.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:

  1. 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.
  2. HTTP — does the endpoint return the status you expect (a 200, or a deliberate 301)? Catches a crashed process, a bad route, or a health check wired to the wrong path.
  3. TLS — is the certificate present, trusted, and not about to expire? Catches the classic 2 a.m. outage where a cert silently lapsed.
  4. 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 narrow nmap -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: validate places this job after deploy in the pipeline order, so it runs against a live deployment.
  • image: pulls the reusable toolbox, so no per-job installation of dig/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 to main so 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 pipefail is 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 echo only 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, only 80,443 are 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:

  1. docker network create labnet
  2. docker run -d --name web --network labnet nginx — the “deployed” service.
  3. docker run --rm --network labnet kali-toolbox check-http.sh http://web/ 200--rm deletes the container on exit; expect exit 0.
  4. 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, and echo $? on the host confirms it. That non-zero is exactly what fails a real pipeline.
  5. Try check-ports.sh web "80" (pass) versus check-ports.sh web "22" (fails, because 80 is 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:

  1. 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.
  2. Check the name, not just the wire. If getent fails but the IP works, you have a DNS regression — exactly what check-dns.sh exists to catch. See DNS troubleshooting from a Kali Docker container for the 127.0.0.11 embedded-resolver path.
  3. Distinguish “unreachable” from “not exposed.” A refused connection on 443 might mean the port genuinely isn’t published. Prove which with a narrow port check — nmap service discovery shows how to read open vs filtered vs closed so you don’t blame the network for a missing listener.
  4. 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:

  1. 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.
  2. Wait for readiness, don’t sleep blindly. Replace a fixed sleep 30 with a bounded poll: retry check-http.sh until it passes or a timeout elapses, e.g. loop up to ~20 times with a short pause and exit 1 if it never comes ready. This distinguishes “slow to start” (eventually passes) from “actually broken” (never passes).
  3. Gate on a real health endpoint. Point the HTTP check at a /healthz route that returns 200 only when dependencies (DB, cache) are truly ready, so a green check means usable, not just listening.
  4. 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.

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 pipefail and 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.

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