Skip to content
DevOps AI ToolKit
Newsletter

Kali Linux on Docker · Part 13 of 16

Packet Capture With tcpdump in Kali Docker

Difficulty: Intermediate ~18 min Part 13/16
Prerequisites: Kali Linux fundamentalsBasic Docker knowledge
Series progress13 / 16
Series curriculum (16 lessons)

When something on your network is misbehaving — a connection that hangs, a TLS handshake that fails, a service that swears it sent a response nobody received — the ground truth is always on the wire. tcpdump lets you read that wire. Running it from a disposable Kali container keeps the capture tooling reproducible and off your host, but it comes with two things you must understand first: which network the container can actually see, and what privilege packet capture really needs.

This lesson shows you how packet capture works inside a container’s network namespace, how to grant only the one capability tcpdump requires (not --privileged), and how to save a capture to the host so you can read it back later.

The two ideas that make container capture make sense

Before a single packet, hold two concepts in your head.

1. A container has its own network namespace. When you run a normal container, Docker gives it a private network stack — its own interfaces, its own routing table, its own view of “the network.” Inside that container tcpdump can only see traffic that actually crosses its interfaces. It cannot see your host’s Wi-Fi or your host’s other containers unless they share the same network. This is the number-one surprise for people new to container capture: you run tcpdump, you see almost nothing, and you assume it is broken. It is not — you are simply looking at a different, isolated network stack than the one carrying the traffic you care about.

2. Packet capture is a privileged operation. Reading raw frames off an interface requires the CAP_NET_RAW Linux capability. A default container drops it, so tcpdump fails with a permission error. The wrong fix is to reach for --privileged; the right fix is to add back the one capability capture needs. We will get to exactly why below.

💡 Note — “Namespace” and “capability” are the two levers here. The namespace decides what traffic is visible; the capability decides whether you are allowed to read it. Almost every capture problem is one of these two.

Which interfaces can the container see?

Start Kali and list the interfaces from inside it:

docker run --rm -it --name kali kali-devops-toolbox
  • --rm removes the container when you exit, keeping the environment disposable.
  • -i keeps STDIN open and -t allocates a TTY, so you get an interactive shell.
  • --name kali gives it a stable name for later reference.
  • kali-devops-toolbox is the custom image built earlier in this series; substitute kali-rolling if you have not built it yet.

Inside the container, ask tcpdump what it can capture on:

tcpdump -D
  • tcpdump -D lists the interfaces available inside this namespace. On a default bridge container you will typically see eth0 (the container’s link to the Docker network) and lo (loopback) — and nothing else. There is no wlan0, no host interface. That short list is your visibility boundary.

This is the moment to internalize expected-vs-observed thinking: you expect to capture the traffic between your lab services, so you must make sure that traffic actually crosses eth0 of the container you are running tcpdump in.

🛠️ DevOps Perspective — This is the same model you hit with a Kubernetes sidecar: a debug container sees the pod’s network namespace, not the node’s. Getting comfortable with “what can this namespace see?” in Docker is exactly the reflex you need when you kubectl debug a pod later.

Grant the right privilege — NOT —privileged

Try to capture and, on a default container, you will hit this:

tcpdump: eth0: You don't have permission to capture on that device
(socket: Operation not permitted)

The container dropped CAP_NET_RAW, so the kernel refuses to open a raw capture socket. You will find plenty of forum answers that say “just add --privileged.” Do not.

--privileged does not grant a capability — it grants all of them, disables the seccomp and AppArmor filters that normally sandbox the container, and exposes host devices. It effectively removes most of the isolation that made running an offensive toolkit in a container safe in the first place. A privileged Kali container is close to a root shell on your host.

Grant only what capture needs instead:

docker run --rm -it \
  --cap-add=NET_RAW \
  --name kali \
  kali-devops-toolbox
  • --cap-add=NET_RAW adds back the single capability that lets tcpdump open a raw socket and read frames off eth0. Everything else the container still lacks — this is least privilege in one flag.
  • The rest of the flags are unchanged from before.

If you also need to change interface state — for example put an interface into promiscuous mode or bring one up — add NET_ADMIN as well:

docker run --rm -it \
  --cap-add=NET_RAW \
  --cap-add=NET_ADMIN \
  --name kali \
  kali-devops-toolbox
  • --cap-add=NET_ADMIN grants network administration rights (interface config, promiscuous mode). Add it only if plain NET_RAW proves insufficient — start narrow, widen only when a real error tells you to.

🔐 Security Note — Never normalize --privileged for packet capture. It removes seccomp/AppArmor confinement, hands the container every capability, and exposes host devices — a container breakout from there is a host compromise. Packet capture needs exactly one capability, CAP_NET_RAW (plus NET_ADMIN only for promiscuous/interface changes). Adding those two is a scalpel; --privileged is a sledgehammer. Least privilege is not a nicety here — it is the whole reason to run offensive tooling in a container rather than on the host. Only capture, scan, or inspect traffic on systems you own or have explicit permission to assess.

Capturing with filters

Now the actual capture. tcpdump’s power is in narrowing the firehose down to the packets you care about with a capture filter (BPF syntax). A few you will use constantly:

tcpdump -i eth0 -n
  • -i eth0 captures on the container’s eth0 interface.
  • -n skips DNS resolution of addresses so output is fast and does not itself generate lookup traffic that pollutes your capture.

Filter by host, port, and protocol:

tcpdump -i eth0 -n host web
tcpdump -i eth0 -n port 80
tcpdump -i eth0 -n 'host web and port 80'
tcpdump -i eth0 -n 'tcp and port 443'
  • host web keeps only packets to or from the host named web (Docker’s embedded DNS resolves that name on a user-defined network).
  • port 80 keeps only packets on TCP/UDP port 80.
  • host web and port 80 combines both — the quotes protect the spaces from the shell.
  • tcp and port 443 narrows to TLS traffic. Good filters are the difference between a readable capture and 40,000 unrelated packets.

A couple more flags worth knowing:

tcpdump -i eth0 -n -c 20 -v port 80
  • -c 20 stops after 20 packets — invaluable so a capture does not run forever.
  • -v increases verbosity (TTL, IP options, etc.); -vv/-vvv add more.

Save to a PCAP file that survives the container

A container is disposable — when it exits, its filesystem is gone, and so is any capture you left inside it. Write the PCAP to a bind-mounted directory so it lands on the host instead. Start Kali with your host’s current directory mapped to /workspace:

docker run --rm -it \
  --cap-add=NET_RAW \
  --network security-lab \
  -v "$(pwd):/workspace" \
  --name kali \
  kali-devops-toolbox
  • --network security-lab attaches Kali to the user-defined lab network so it can actually see the lab traffic (recall: the namespace decides visibility).
  • -v "$(pwd):/workspace" bind-mounts your current host directory into the container at /workspace. Anything written there appears on the host immediately and persists after the container is gone.

Now capture to a file under that mount:

tcpdump -i eth0 -n -w /workspace/capture.pcap 'host web and port 80'
  • -w /workspace/capture.pcap writes raw packets to a PCAP file instead of printing them. Because /workspace is the bind mount, capture.pcap shows up in your host directory as it is written.
  • Press Ctrl+C to stop; tcpdump reports how many packets it captured.

Read the capture back — either from the same container, from a fresh one, or with Wireshark on the host:

tcpdump -r /workspace/capture.pcap -n
tcpdump -r /workspace/capture.pcap -n 'port 80'
  • -r /workspace/capture.pcap reads packets from the file instead of a live interface. Reading a saved file needs no special capability — you can drop --cap-add entirely for offline analysis.
  • You can apply a display filter at read time ('port 80') to re-slice a capture you already have, so it is fine to capture broadly and narrow later.

🏭 Why This Matters in Production — In a real incident you rarely analyze on the box that captured. You grab a short, filtered PCAP on the affected host (or a debug container beside it), copy it out via a mount or docker cp, and analyze offline in Wireshark on your workstation — where you can follow TCP streams, spot retransmits, and see exactly where a handshake died. Capturing to a persisted /workspace/capture.pcap and reading it back with tcpdump -r is that workflow in miniature: capture with least privilege, persist the artifact, analyze somewhere safe.

Host capture vs container capture

Sometimes the container’s eth0 genuinely cannot see what you need — you want all the host’s traffic, or traffic on a network the container is not attached to. You have a few options, in order of preference:

  • Attach the container to the right Docker network (--network). Best first move: put Kali on the same user-defined network as the services you are debugging so their traffic crosses its eth0. Least privilege, full isolation.
  • Capture on the host directly with the host’s own tcpdump. If you truly need the host’s physical interface, running tcpdump on the host (with sudo) is often cleaner and safer than weakening a container to reach host interfaces.
  • --network host makes the container share the host’s network namespace, so it sees every host interface. This is powerful but it removes network isolation — the container is now on the host’s network stack directly.

⛔ Production Warning — Reach for --network host (or worse, --privileged) only as a last resort, and never on a shared or production host. Sharing the host network namespace means the container can bind host ports and sniff host interfaces; combined with an offensive toolkit that is a lot of blast radius. If you only need lab traffic, a user-defined network plus --cap-add=NET_RAW gives you everything with none of the exposure.

Try It Yourself

🧪 Try It — Capture live lab traffic, persist it to the host, and read it back — all with a single capability.

  1. Create a lab network and a benign target:
    • docker network create security-lab
    • docker run --rm -d --network security-lab --name web nginx
  2. Launch Kali with capture privilege, the lab network, and a host mount:
    • docker run --rm -it --cap-add=NET_RAW --network security-lab -v "$(pwd):/workspace" --name kali kali-devops-toolbox
  3. Confirm visibility inside Kali: tcpdump -D should list eth0.
  4. Start a capture to the mount, filtered to the target:
    • tcpdump -i eth0 -n -w /workspace/capture.pcap 'host web and port 80'
  5. In a second terminal, generate traffic: docker exec kali curl -s -o /dev/null http://web (or curl -I http://web from inside a second Kali shell).
  6. Back in the capture terminal, press Ctrl+C. It should report a non-zero packet count.
  7. Read it back offline: tcpdump -r /workspace/capture.pcap -n. You should see the HTTP request and response packets.
  8. Confirm persistence: exit Kali, then ls capture.pcap on the host — the file is still there. Open it in Wireshark if you like.
  9. Clean up: docker stop web && docker network rm security-lab.

Expected state: tcpdump -D shows eth0, the capture records packets, and capture.pcap survives on the host. If observed state differs, work through Common Problems.

Common Problems

Symptom: tcpdump: eth0: You don't have permission to capture on that device (socket: Operation not permitted).

The container lacks CAP_NET_RAW, so the kernel refuses the raw socket.

  • Diagnose: you started the container without --cap-add=NET_RAW. Confirm with docker inspect -f '{{.HostConfig.CapAdd}}' kali — an empty list means capture is not permitted.
  • Fix: restart the container with --cap-add=NET_RAW. Do not “fix” it with --privileged — that removes isolation you do not need to remove to capture packets. If you additionally need promiscuous mode, add --cap-add=NET_ADMIN.

Symptom: capture runs but shows no packets (or only ARP/broadcast noise).

Privilege is fine; visibility is the problem — the traffic you want is not crossing this namespace’s eth0.

  • Diagnose: tcpdump -D shows eth0, but the target service is on a different network. Check with docker inspect -f '{{json .NetworkSettings.Networks}}' kali and confirm it lists the same network as the target.
  • Fix: attach Kali to the target’s network (--network security-lab), then re-run the capture. Remember the container cannot see host or unrelated-container traffic by default — that isolation is deliberate.

Symptom: capture worked, but the PCAP file is gone after the container exits.

You wrote it inside the container’s ephemeral filesystem, not the bind mount.

  • Diagnose: the container was started without -v "$(pwd):/workspace", or you wrote to a path outside /workspace.
  • Fix: start with the mount and write to -w /workspace/capture.pcap. Verify mid-capture that the file already appears on the host with ls capture.pcap.

🔎 Troubleshooting Tip — Split “no packets” into its two possible causes the same way you split DNS from reachability. Permission failures throw an explicit “Operation not permitted” — fix with NET_RAW. Visibility failures capture cleanly but show nothing relevant — fix the network the container is on. The error text tells you which half you are in.

Where to go next

What You Learned

  • A container has its own network namespace: tcpdump -D shows only the interfaces (eth0, lo) inside it, so what you can capture is bounded by which Docker network the container is attached to.
  • Packet capture needs the CAP_NET_RAW capability; grant it with --cap-add=NET_RAW (plus NET_ADMIN only for promiscuous/interface changes) rather than --privileged, which strips most container isolation.
  • How to filter captures by interface, host, port, and protocol, and how to cap a run with -c.
  • How to write a capture to /workspace/capture.pcap over a bind mount so it persists on the host, then read it back offline with tcpdump -r — which needs no special capability.
  • When host capture (--network host or the host’s own tcpdump) is appropriate, and why a user-defined network plus NET_RAW is the least-privilege default.

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