Docker 93 🐳 Kernel Resource Auditing: Tracing System Calls inside Containers using strace and eBPF
Container system call tracing is complicated by two facts that are easy to overlook. First, minimal container images—the ones most likely to be deployed in production—often exclude strace and other debugging tools precisely because they increase the attack surface . Second, strace relies on the ptrace system call, which requires the CAP_SYS_PTRACE capability that Docker does not grant by default, and Docker’s default seccomp profile restricts ptrace unless that capability is present . The result is that the obvious approach—docker exec into the container and run strace—fails on both counts. The tools that work are nsenter, which uses the host’s strace inside the container’s namespaces, and eBPF, which traces from the kernel without attaching to the process at all.
Key point: strace attaches to a process and therefore needs CAP_SYS_PTRACE and a seccomp profile that permits ptrace. eBPF programs run in the kernel and do not attach to the process, which makes them usable in environments where ptrace is blocked. The trade-off is that eBPF tracing requires host-level privileges and the tooling must be installed on the host, not inside the container.
Why container system call tracing is difficult
The capability problem. strace uses ptrace to attach to a process and inspect its system calls. Docker’s default seccomp profile blocks ptrace-related system calls unless the container has CAP_SYS_PTRACE . A container started without --cap-add=SYS_PTRACE cannot be traced by strace from inside, and the failure mode is a permission error that does not always explain the cause. The --security-opt seccomp=unconfined flag removes the seccomp restriction but does not grant the capability, so it is not a complete solution by itself.
The missing tool problem. Hardened and distroless images strip strace, curl, and other debugging utilities to reduce the attack surface . The image that is most likely to have a subtle system call issue—a minimal production image—is also the one least likely to have the tool needed to diagnose it. Installing strace inside the running container requires package manager access, which minimal images often lack.
The namespace problem. Even when strace is available, the host and container have different views of the process tree. A strace run on the host without entering the container’s PID namespace sees the container process as a foreign PID, and the system calls it captures are filtered through the host’s perspective. nsenter addresses this by entering the container’s namespaces before running the host’s strace.
The container-awareness problem for eBPF. eBPF programs on the host see system calls from every process on the system, including the host’s own processes and every container. Distinguishing which container a system call belongs to requires reading the PID namespace from the kernel’s task_struct and correlating it with the container’s metadata . The correlation is not automatic; the tool must be designed to do it.
a. strace via nsenter
The nsenter approach uses the host’s strace binary while entering the container’s PID and network namespaces. The container does not need strace installed, and the host’s strace is already configured and available. The -p 1 flag traces PID 1 inside the container’s PID namespace, which is the container’s main process.
# Get the container PID on the host
PID=$(docker inspect --format '{{.State.Pid}}' myapp)
# Enter the container's PID and network namespaces
# Run the host's strace against PID 1 inside the namespace
sudo nsenter -t $PID -p -n -- strace -p 1 -f
The -p flag enters the PID namespace, and -n enters the network namespace. The -f flag follows child processes, which is essential for containers that spawn subprocesses. Without -f, the trace stops at the first fork . The -p 1 inside the namespace refers to the container’s init process, not the host’s PID 1.
Filtering reduces the volume of output. A container making thousands of system calls per second produces a trace that is unreadable without filters. The -e trace= flag accepts syscall classes or individual names.
# Trace only network-related system calls
sudo nsenter -t $PID -p -n -- strace -f -e trace=%network -p 1
# Trace file operations with timing
sudo nsenter -t $PID -p -n -- strace -f -tt -T -e trace=%file -p 1
The -tt flag adds microsecond timestamps, and -T shows the time spent in each system call. Together they identify which call is slow, not just which call is frequent. For a container that starts slowly, this combination shows whether the delay is in connect, read, or a filesystem operation.
A summary mode is useful for a first pass. The -c flag counts system calls and reports the time spent in each, sorted by total time.
# Summarize system call counts and time
sudo nsenter -t $PID -p -n -- strace -c -f -p 1
The summary answers “what is this container doing?” before the detailed trace answers “what is it doing right now?” A container that spends most of its time in futex is blocked on locks; one that spends time in read on a socket is waiting for network data.
b. eBPF Tracing with bpftrace
eBPF tracing runs in the kernel and does not attach to the target process. It does not need CAP_SYS_PTRACE and is not blocked by Docker’s seccomp profile. The bpftrace tool provides a high-level language for writing eBPF programs, and the tracepoint:syscalls:sys_enter_* probes fire when a system call is entered.
A basic execve trace shows every new process started on the host, including those inside containers. The problem is distinguishing container processes from host processes. The PID namespace is the discriminator: processes in a container have a different PID namespace than the host.
# Trace execve and print the PID, command, and arguments
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-6d %-8s", pid, comm); join(args->argv); }'
The output includes host processes and container processes without distinction. To filter to a specific container, the trace must read the PID namespace from the kernel’s task_struct. The curtask built-in in bpftrace provides access to the current task’s structure, and nsproxy->pid_ns_for_children->ns.inum gives the PID namespace ID .
# Trace execve with PID namespace and hostname
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve {
$task = (struct task_struct *)curtask;
$pidns = $task->nsproxy->pid_ns_for_children->ns.inum;
$cname = $task->nsproxy->uts_ns->name.nodename;
printf("pidns=%d host=%s pid=%d comm=%s\n", $pidns, str($cname), pid, comm);
join(args->argv);
}'
The PID namespace ID is a stable identifier for the container. The hostname provides a human-readable label if the container was started with --hostname. Correlating the PID namespace ID with the container’s metadata (name, image, labels) requires a mapping table, which the container runtime maintains and which tools like crictl inspect can query .
The OCI seccomp BPF hook uses this approach to generate seccomp profiles. It attaches an eBPF program to raw_syscalls:sys_enter, filters by PID namespace, writes matching system calls to a perf buffer, and generates a profile when the container exits . This is the same mechanism that container security tools use to build a profile of what a container actually does, rather than what the image maintainer assumed it would do.
Complete Example Session
# ============================================
# PART 1: GET THE CONTAINER PID
# ============================================
PID=$(docker inspect --format '{{.State.Pid}}' myapp)
echo $PID
# ============================================
# PART 2: CHECK IF STRACE IS AVAILABLE
# ============================================
# The container may not have strace
docker exec myapp which strace
# If empty, use nsenter with the host's strace
# ============================================
# PART 3: TRACE WITH NSENTER AND HOST STRACE
# ============================================
# Enter PID and network namespaces, trace PID 1
sudo nsenter -t $PID -p -n -- strace -f -p 1
# ============================================
# PART 4: FILTER TO NETWORK SYSCALLS
# ============================================
sudo nsenter -t $PID -p -n -- strace -f -e trace=%network -p 1
# ============================================
# PART 5: ADD TIMING
# ============================================
sudo nsenter -t $PID -p -n -- strace -f -tt -T -e trace=%file -p 1
# ============================================
# PART 6: SUMMARY MODE
# ============================================
sudo nsenter -t $PID -p -n -- strace -c -f -p 1
# ============================================
# PART 7: SAVE TRACE TO FILE
# ============================================
sudo nsenter -t $PID -p -n -- strace -f -o /tmp/container.trace -p 1
# ============================================
# PART 8: BASIC EBPF EXECVE TRACE
# ============================================
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%-6d %-8s", pid, comm); join(args->argv); }'
# ============================================
# PART 9: EBPF WITH PID NAMESPACE FILTER
# ============================================
sudo bpftrace -e '
tracepoint:syscalls:sys_enter_execve {
$task = (struct task_struct *)curtask;
$pidns = $task->nsproxy->pid_ns_for_children->ns.inum;
printf("pidns=%d pid=%d comm=%s\n", $pidns, pid, comm);
join(args->argv);
}'
# ============================================
# PART 10: CORRELATE PID NAMESPACE WITH CONTAINER
# ============================================
# The PID namespace ID from bpftrace is the kernel's view
# Correlate with container metadata:
# docker inspect --format '{{.State.Pid}}' myapp
# Read /proc/<pid>/ns/pid to get the namespace inode
sudo readlink /proc/$PID/ns/pid
# Compare with the pidns value from bpftrace
The ten parts move from the nsenter approach, which works with the host’s tools and does not require capabilities inside the container, to the eBPF approach, which does not attach to the process and can filter by PID namespace. Part 10 shows the correlation step that connects the kernel’s namespace ID to the container runtime’s metadata.
Quick Reference
strace via nsenter
| Flag | Purpose |
|---|---|
nsenter -t $PID -p -n | Enter PID and network namespaces |
strace -p 1 | Trace container’s init process |
-f | Follow child processes |
-e trace=%network | Filter to network syscalls |
-tt | Microsecond timestamps |
-T | Time spent in each call |
-c | Summary of counts and time |
-o file | Write to file |
eBPF Tracing
| Probe | Fires When |
|---|---|
tracepoint:syscalls:sys_enter_execve | A process calls execve |
tracepoint:syscalls:sys_enter_openat | A file is opened |
tracepoint:syscalls:sys_enter_connect | A socket connects |
raw_syscalls:sys_enter | Any syscall (ID in args) |
Container Distinction
| Method | Discriminator |
|---|---|
| eBPF on host | PID namespace ID (nsproxy->pid_ns_for_children->ns.inum) |
| eBPF on host | UTS hostname (nsproxy->uts_ns->name.nodename) |
| strace via nsenter | Namespace entry itself |
Best Practices
✅ Do This:
# Use nsenter with host strace when the container lacks tools
sudo nsenter -t $PID -p -n -- strace -f -e trace=%network -p 1 # ✅
# Filter syscalls to reduce noise
strace -e trace=%file,%network -p 1 # ✅
# Use eBPF when ptrace is blocked by seccomp
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { ... }' # ✅
# Read PID namespace in eBPF to distinguish containers
$pidns = $task->nsproxy->pid_ns_for_children->ns.inum # ✅
❌ Don’t Do This:
# Assume docker exec strace works in minimal images
docker exec myapp strace -p 1 # may fail: no strace, no capability # ❌
# Use --security-opt seccomp=unconfined without CAP_SYS_PTRACE
# Seccomp allows ptrace but capability is still missing # ❌
# Trace without filtering in a busy container
strace -p 1 # thousands of lines per second # ❌
# Assume eBPF output is container-scoped by default
# Host processes appear in the same trace # ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
strace permission denied | CAP_SYS_PTRACE not granted | Use nsenter with host strace |
strace not found in container | Minimal/distroless image | Use nsenter with host binary |
| Trace output overwhelming | No syscall filter | -e trace=%network or %file |
| eBPF output includes host processes | No PID namespace filter | Read nsproxy->pid_ns_for_children |
| Cannot correlate pidns to container | No mapping table | Use /proc/<pid>/ns/pid to match |
Real-World Examples
1. Get Container PID
docker inspect --format '{{.State.Pid}}' myapp
2. Trace via nsenter
sudo nsenter -t $PID -p -n -- strace -f -p 1
3. Filter Network Calls
sudo nsenter -t $PID -p -n -- strace -f -e trace=%network -p 1
4. Timing File Operations
sudo nsenter -t $PID -p -n -- strace -f -tt -T -e trace=%file -p 1
5. Summary Mode
sudo nsenter -t $PID -p -n -- strace -c -f -p 1
6. eBPF execve Trace
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%d %s", pid, comm); join(args->argv); }'
7. eBPF with PID Namespace
sudo bpftrace -e 'tracepoint:syscalls:sys_enter_execve { $p=(struct task_struct*)curtask; printf("ns=%d ", $p->nsproxy->pid_ns_for_children->ns.inum); join(args->argv); }'
8. Read Namespace Inode
sudo readlink /proc/$PID/ns/pid
9. Trace with CAP_SYS_PTRACE
docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined myapp
# Then docker exec strace -p 1 works, but this weakens isolation
10. OCI Seccomp Hook Approach
# eBPF traces syscalls by PID namespace, generates profile on exit
# Requires CAP_SYS_ADMIN and host-level tooling
Visual
strace via nsenter
┌──────────────────────────────────────────────────────────────┐
│ HOST │
│ │
│ ┌─────────────────┐ │
│ │ nsenter │ Enter PID + network namespaces │
│ │ -t $PID -p -n │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Host's strace │ Runs against PID 1 inside namespace │
│ │ -p 1 -f │ No container tooling needed │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Container's │ System calls visible from │
│ │ syscall stream │ container's PID perspective │
│ └─────────────────┘ │
│ │
│ CAP_SYS_PTRACE is not required because the container │
│ process is traced from the host's namespace. │
│ │
└──────────────────────────────────────────────────────────────┘
eBPF Tracing with Namespace Filter
┌──────────────────────────────────────────────────────────────┐
│ KERNEL │
│ │
│ ┌─────────────────┐ │
│ │ eBPF program │ Fires on sys_enter_execve │
│ │ │ Reads task_struct from curtask │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ nsproxy-> │ PID namespace ID │
│ │ pid_ns_for_ │ │
│ │ children->ns │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Filter by │ Only container's namespace │
│ │ pidns value │ │
│ └────────┬────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Output │ pidns, pid, comm, args │
│ └─────────────────┘ │
│ │
│ No ptrace. No CAP_SYS_PTRACE. │
│ Host processes filtered out by namespace. │
│ │
└──────────────────────────────────────────────────────────────┘
Why docker exec strace Fails
┌──────────────────────────────────────────────────────────────┐
│ docker exec myapp strace -p 1 │
│ │
│ ┌─────────────────┐ │
│ │ Check 1: │────► FAIL: strace not installed │
│ │ Is strace in │ in minimal image │
│ │ the image? │ │
│ └────────┬────────┘ │
│ │ (if installed) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ Check 2: │────► FAIL: ptrace blocked by │
│ │ CAP_SYS_PTRACE? │ default seccomp │
│ └────────┬────────┘ │
│ │ (if granted) │
│ ▼ │
│ ┌─────────────────┐ │
│ │ strace works │ But container isolation weakened │
│ └─────────────────┘ │
│ │
│ nsenter avoids both failures by using the host's tools. │
│ │
└──────────────────────────────────────────────────────────────┘
PID Namespace Correlation
┌──────────────────────────────────────────────────────────────┐
│ eBPF OUTPUT CONTAINER METADATA │
│ │
│ pidns = 4026532533 docker inspect myapp │
│ pid = 42 .State.Pid = 31889 │
│ comm = node │
│ │
│ ┌─────────────────┐ │
│ │ readlink │ │
│ │ /proc/31889/ │────► pid:[4026532533] │
│ │ ns/pid │ │
│ └─────────────────┘ │
│ │
│ The namespace inode matches the pidns from eBPF. │
│ This correlates kernel-level traces to container names. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| strace capability | Requires CAP_SYS_PTRACE; blocked by default seccomp |
| strace in minimal images | Often removed |
| nsenter workaround | Enter namespaces, use host’s strace |
| eBPF advantage | No ptrace, no process attachment |
| eBPF container filter | PID namespace from task_struct |
| eBPF host requirement | CAP_SYS_ADMIN / root on host |
| Correlation | /proc/<pid>/ns/pid matches eBPF pidns |
Key takeaways:
straceinside a container usually fails for two independent reasons. The image may not havestrace, and Docker’s default seccomp profile blocksptraceunlessCAP_SYS_PTRACEis granted .nsenterwith the host’sstracebypasses both.nsenteruses the host’s tools inside the container’s namespaces. The-pflag enters the PID namespace, and-nenters the network namespace. Thestrace -p 1traces the container’s init process. No capabilities are needed inside the container.- eBPF does not attach to the process. It runs in the kernel and fires on tracepoints. It is not blocked by seccomp and does not require
CAP_SYS_PTRACE. The trade-off is that it requires host-level privileges and produces output for all processes on the system unless filtered. - The PID namespace is the discriminator for container traces. The
nsproxy->pid_ns_for_children->ns.inumfield intask_structidentifies which container a system call belongs to. Correlating it with/proc/<pid>/ns/pidlinks the kernel trace to the container’s metadata . - Filtering is mandatory for useful traces. A busy container generates thousands of system calls per second.
-e trace=%networkor%filereduces the output to the calls relevant to the problem.-csummarizes before detailed tracing begins.
Remember: Container system call tracing is a choice between two constraints. strace gives detailed, process-scoped output but requires capabilities and tools that containers often do not have. eBPF gives kernel-level visibility without attaching to the process but produces host-wide output that must be filtered by namespace. The nsenter approach works for ad-hoc debugging when the host has strace and the container is running. The eBPF approach works for continuous monitoring or for environments where ptrace is blocked. Neither is universally better; the choice depends on what the container image provides and what the security policy allows.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!