Why strace Fails Inside Docker Containers
strace is a staple debugging tool for tracing system calls made by a process. It relies on the ptrace system call to do its job. If you've tried running strace inside a Docker container on an older Docker version, you've likely hit a permission error:
$ docker run -it ubuntu:18.04 /bin/bash
$ # ... install strace ...
root@e27f594da870:/# strace ls
strace: ptrace(PTRACE_TRACEME, ...): Operation not permitted
On a host machine, this can be fixed by granting the container the SYS_PTRACE capability:
docker run --cap-add=SYS_PTRACE -it ubuntu:18.04 /bin/bash
But the interesting question isn't how to fix it—it's why it happens in the first place. Understanding this requires digging into three overlapping Linux security features: capabilities, user namespaces, and seccomp profiles.
Digging into Hypotheses
Capabilities: A Red Herring
It's tempting to assume that container processes simply lack the CAP_SYS_PTRACE capability by default, which would explain why adding it via --cap-add=SYS_PTRACE solves the problem. That seems straightforward, but it falls apart under scrutiny.
First, an ordinary user can run strace on any process they own—this works fine on a standard Linux system and inside a container. Checking the current process's capabilities confirms that CAP_SYS_PTRACE is not present in that scenario:
$ getpcaps $$
Capabilities for `11589': =
Second, the capabilities(7) man page specifies that CAP_SYS_PTRACE is meant for tracing arbitrary processes owned by any user, just as root can. It is not required to trace a process you already own. Indeed, an experiment confirms this: launching a container with --cap-add=SYS_PTRACE and then dropping that capability still allows strace to work on processes it creates.
User Namespaces: Not the Culprit
A common guess is that containers run in a different user namespace, breaking ptrace across namespace boundaries. Checking inside the container shows the same user namespace identifier as on the host:
In the container:root@e27f594da870:/# ls /proc/$$/ns/user -l
... /proc/1/ns/user -> 'user:[4026531837]'
On the host:
bork@kiwi:~$ ls /proc/$$/ns/user -l
... /proc/12177/ns/user -> 'user:[4026531837]'
Both report 4026531837. This means the root user inside the container is literally the same user as root on the host—there's no namespace barrier preventing ptrace. So this hypothesis doesn't hold, but it's a worthwhile observation: Docker containers (without user namespace remapping) share root's identity with the host.
The Real Answer: Docker's Default Seccomp Profile
Docker applies a default seccomp-bpf profile to block potentially dangerous system calls. That profile uses a whitelist—and ptrace is not on it. With the system call entirely blocked, strace cannot even invoke ptrace, let alone work.
Disabling the seccomp profile entirely confirms this. With --security-opt seccomp=unconfined, strace works inside the container:
$ docker run --security-opt seccomp=unconfined -it ubuntu:18.04 /bin/bash
$ strace ls
execve("/bin/ls", ["ls"], 0x7ffc69a65580 /* 8 vars */) = 0
... it works fine ...
So the original problem is explained: a default whitelist omits ptrace, blocking it outright.
The Missing Link: Why --cap-add Works
This leads to a new question: why would adding a capability fix a seccomp restriction? The docker run man page describes --cap-add purely in terms of Linux capabilities:
--cap-add=[]
Add Linux capabilities
That description has nothing to do with seccomp. The answer lies in Docker's source code.
Inside the Docker and containerd codebases, there's logic that pairs capabilities with related system calls. When a container is granted a capability, the seccomp profile is dynamically extended to allow the system calls that capability governs. For example, giving CAP_SYS_PTRACE also whitelists ptrace and related calls like process_vm_readv. Relevant code exists in containerd's contrib/seccomp/seccomp_default.go and in Docker's own profiles/seccomp/seccomp.go and default profile JSON.
case "CAP_SYS_PTRACE":
s.Syscalls = append(s.Syscalls, specs.LinuxSyscall{
Names: []string{
"kcmp",
"process_vm_readv",
"process_vm_writev",
"ptrace",
},
Action: specs.ActAllow,
Args: []specs.LinuxSeccompArg{},
})
Functionally, --cap-add behaves more like --cap-add-and-whitelist-related-syscalls. This makes sense—granting a capability is useless if the underlying system calls are blocked by seccomp.
Modern Docker Versions Already Permit It
This behavior has changed. Since Docker 19.03, via a commit that adjusted the default seccomp profile, ptrace is allowed—but only on kernel versions newer than 4.8. The version that exhibits the original problem (Docker 18.09.7 in this case) predates that fix.
So the failure of strace in older Docker containers stems not from missing capabilities or namespace issues, but from seccomp's default deny policy. And Docker gently extends that policy when you ask for a capability—an interaction between two security mechanisms that isn't obvious from the command-line docs alone.



