Hardened, non-root network debugging toolbox for Kubernetes — a low-CVE, Wolfi-based
alternative to nicolaka/netshoot.
Pull it into a running pod or node with kubectl debug and you get the usual network
diagnostics (tcpdump, conntrack, iproute2, mtr, …) — but from a minimal, regularly
rebuilt base, running as a non-root user, and still able to capture packets thanks to file
capabilities.
cgr.dev/chainguard/wolfi-base) — minimal, glibc,
rebuilt frequently, typically low/zero CVE.tcpdump bind-tools iproute2 iputils netcat-openbsd socat conntrack-tools iptables nftables ethtool mtr iperf3 lsof strace curl openssl jq bash. No kubectl
(you run it from your workstation; bundling it + a mounted SA token is a lateral-movement
risk).tcpdump/ping carry file capabilities, so
capture works as non-root with the --profile=netadmin (pods) / --profile=sysadmin
(nodes) debug profile — never root. (Kubernetes has no ambient caps, so file caps are
required.)kubectl debug# published to both registries — pick either:
IMG=jc3rny/network-debug-toolbox:latest # Docker Hub
IMG=ghcr.io/jc3rny/library/network-debug-toolbox:latest # GHCR (no pull rate limits)
# inside a pod (packet capture needs the netadmin profile)
kubectl debug -it <pod> -n <ns> --image=$IMG --target=<container> --profile=netadmin -- bash
# then, inside: tcpdump -i any -nn 'port 8080'
# on a node / host netns (e.g. to see pre-SNAT client IPs before kube-proxy rewrites them)
kubectl debug node/<node> -it --image=$IMG --profile=sysadmin -- \
tcpdump -i any -nn 'tcp port <nodePort>'
# save a pcap for Wireshark (no -t — a TTY corrupts the binary stream)
kubectl debug -i <pod> -n <ns> --image=$IMG --target=<container> --profile=netadmin -- \
tcpdump -i any -w - 'port 8080' > pod.pcap
Non-root note: capture works non-root because the binaries carry file capabilities and you pass
--profile=netadmin/sysadmin. Deep host filesystem access during node debug may still need a root override; non-root + caps covers network capture.
id # uid=65532 (non-root)
getcap "$(command -v tcpdump)" # → cap_net_admin,cap_net_raw=ep
tcpdump -D # lists interfaces (no permission error)
make # build + scan Dockerfile config + scan image (fails on HIGH/CRITICAL)
make verify # full vulnerability report (all severities, never fails)
make verify-caps# assert tcpdump keeps its file caps (non-root capture guard)
make sbom # CycloneDX SBOM
Trivy runs via Docker (no local install needed). Local builds are native (e.g. arm64 on Apple Silicon) for speed.
CI is split into a validation gate and a publish stage (.github/workflows/):
ci.yml — on pull requests: runs the reusable validate.yml gate (build + Trivy
config/image scan + file-caps guard + helm lint). No secrets, no push. Make
validate / build-scan the required status check on main.release.yml — on push to main (→ latest), v* tags (→ semver), and a weekly
rebuild: runs the same gate, then pushes multi-arch (linux/amd64,linux/arm64) image +
chart to both Docker Hub and GHCR.Published artifacts:
| Artifact | Docker Hub | GHCR |
|---|---|---|
| Image | jc3rny/network-debug-toolbox | ghcr.io/jc3rny/library/network-debug-toolbox |
| Chart | jc3rny/network-debug-toolbox-helm | ghcr.io/jc3rny/helm/network-debug-toolbox |
GHCR auth uses the built-in GITHUB_TOKEN (no setup). Docker Hub needs these under
Settings → Secrets and variables → Actions:
| Name | Kind | Purpose |
|---|---|---|
DOCKERHUB_USERNAME | Variable | Docker Hub account the image is pushed under (not sensitive — kept a variable so it stays readable in logs) |
DOCKERHUB_TOKEN | Secret | Docker Hub access token, Read & Write, all repositories |
No PAT is needed — everything runs on the built-in GITHUB_TOKEN.
Images and charts are signed with cosign (keyless/OIDC); verify with
cosign verify <ref> --certificate-identity-regexp '.*' --certificate-oidc-issuer https://token.actions.githubusercontent.com.
Uses Conventional Commits (feat: → minor, fix: → patch). release-please keeps a
release PR that bumps the version + CHANGELOG.md + the chart's version/appVersion.
You merge that PR when you want to cut a release; on merge, the versioned, signed image
and chart are published automatically (release-please calls release.yml in the same run).
Dependabot base-image bumps land as fix(...) so they queue into the next patch release
(action bumps use ci(...) — changelog only). :latest already tracks main, so base-image
patches reach users immediately, independent of cutting a versioned release.
The release PR is created by
GITHUB_TOKEN, so CI does not auto-run on it. Merge it with the admin "merge without waiting for requirements" option, or close/reopen it once to trigger CI. (Dependabot PRs are unaffected — they run CI and auto-merge normally.)
To publish manually instead:
docker login
make publish IMAGE=<user>/network-debug-toolbox TAG=$(date +"%Y%m%d")
make publish forces linux/amd64, runs the full scan + verify-caps gate, pushes, then
attaches the CycloneDX SBOM to the pushed image as an OCI referrer (needs oras —
brew install oras). Pin the Wolfi base to a digest before production use (the Dockerfile
already pins one; refresh it with the command in the Dockerfile header).
helm/ deploys the image as a per-node DaemonSet you exec into for node-level
debugging (hardened, non-root by default — see helm/values.yaml). CI packages it and
pushes it as an OCI artifact to both registries on main/v* (use either):
# GHCR
helm install ndt oci://ghcr.io/jc3rny/helm/network-debug-toolbox \
--namespace netdebug --create-namespace
# Docker Hub
helm install ndt oci://registry-1.docker.io/jc3rny/network-debug-toolbox-helm \
--namespace netdebug --create-namespace
# then exec into the pod on a node and capture
kubectl -n netdebug exec -it ds/ndt-network-debug-toolbox -- \
tcpdump -i any -nn 'tcp port <port>'
hostNetwork/hostPID require the namespace to allow it under Pod Security:
kubectl label ns netdebug pod-security.kubernetes.io/enforce=privileged --overwrite.
scripts/presnat-capture.shHelper to capture pre-SNAT traffic (real client IPs) for a LoadBalancer/NodePort
Service across nodes — it discovers the VIP, service ports and nodePorts, builds the BPF
filter, runs kubectl debug node/... on each node with this image, and (in pcap mode)
merges the per-node captures. See ./presnat-capture.sh -h.
Apache-2.0 — see LICENSE.
Content type
Image
Digest
sha256:8fdfcffff…
Size
33.8 MB
Last updated
about 1 month ago
docker pull jc3rny/network-debug-toolbox