Seccomp Profiles: Filtering Syscalls
Seccomp Profiles: Filtering Syscalls
Every process running inside a container makes system calls (syscalls) to the kernel — opening files, creating network sockets, spawning processes, changing permissions. A typical application uses only a fraction of the 300+ syscalls available on Linux. The syscalls your application never uses are an attack surface: if an attacker exploits a vulnerability, they can only use syscalls that are permitted.
Seccomp (Secure Computing Mode) is a Linux kernel feature that filters which syscalls a process can invoke. Docker and containerd apply a default seccomp profile that blocks known-dangerous syscalls, but you can create custom profiles for finer control.
How seccomp works
A seccomp profile is a JSON file that defines which syscalls are allowed, denied, or handled with specific actions:
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["chmod", "fchmod", "fchmodat"],
"action": "SCMP_ACT_ERRNO"
},
{
"names": ["mount", "umount2"],
"action": "SCMP_ACT_ERRNO"
}
]
}
The actions:
SCMP_ACT_ALLOW: Permit the syscallSCMP_ACT_ERRNO: Block the syscall and return an error (the application gets EPERM)SCMP_ACT_KILL: Kill the process that attempts the syscallSCMP_ACT_LOG: Log the syscall attempt (useful for building profiles)
Building a custom seccomp profile
The most practical approach is to start with the default Docker profile and add restrictions:
# Run a container with syscall logging to build a custom profile
docker run --security-opt seccomp=unconfined \
--security-opt seccomp-log \
myapp:latest 2>&1 | grep "syscall"
# Or use strace to capture syscalls
strace -f -e trace=all python3 app.py 2>&1 | grep "^(" | sort -u
Then create a profile that allows only the syscalls your application actually uses:
{
"defaultAction": "SCMP_ACT_ERRNO",
"syscalls": [
{
"names": [
"accept", "accept4", "access", "arch_prctl", "bind",
"brk", "clock_gettime", "close", "connect", "epoll_create1",
"epoll_ctl", "epoll_wait", "execve", "exit_group", "fstat",
"futex", "getcwd", "getdents64", "getpeername", "getrandom",
"getsockname", "getsockopt", "ioctl", "listen", "lseek",
"madvise", "mmap", "mprotect", "munmap", "newfstatat",
"openat", "poll", "read", "readlink", "recvfrom",
"rt_sigaction", "rt_sigprocmask", "sendto", "setitimer",
"setsockopt", "shutdown", "sigaltstack", "socket", "stat",
"sysinfo", "tgkill", "time", "uname", "wait4", "write",
"writev"
],
"action": "SCMP_ACT_ALLOW"
}
]
}
This profile uses defaultAction: SCMP_ACT_ERRNO (deny by default) and explicitly allows only the syscalls the application needs. Any syscall not in the list returns EPERM.
Applying seccomp profiles
# Docker
docker run --security-opt seccomp=/path/to/profile.json myapp:latest
# Kubernetes — via securityContext (Kubernetes 1.24+)
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
securityContext:
seccompProfile:
type: Localhost
localhostProfile: profiles/myapp.json
containers:
- name: app
image: myapp:latest
The default Docker seccomp profile
Docker's default profile blocks approximately 44 syscalls that are known to be dangerous or unnecessary, including:
mount,umount2— filesystem mountingreboot— system rebootkexec_load— kernel loadingopen_by_handle_at— can bypass file permissionsptrace— process debugging (used in container escapes)
The default profile is a good starting point, but custom profiles provide defense in depth by restricting to only what your specific application needs.
Linux Capabilities and Filesystem Restrictions
Linux Capabilities and Filesystem Restrictions
Linux capabilities split the privileges of the root user into distinct units. Instead of running as root (which has all capabilities), a process can be granted only the specific capabilities it needs. Containers inherit a default set of capabilities from the runtime — most of which are unnecessary for typical applications.
The default capability set and why it's too broad
Docker's default capability set includes 14 capabilities. A typical web application needs almost none of them:
| Capability | What it allows | Needed by web apps? |
|---|---|---|
| CHOWN | Change file ownership | Rarely |
| DAC_OVERRIDE | Bypass file read/write/execute permissions | No |
| FSETID | Set file capabilities | No |
| FOWNER | Bypass permission checks on files owned by the process | No |
| MKNOD | Create device nodes | No |
| NET_RAW | Use raw sockets (ping, packet crafting) | No |
| SETGID | Change group ID | No |
| SETUID | Change user ID | No |
| SETFCAP | Set file capabilities | No |
| SETPCAP | Transfer capabilities | No |
| NET_BIND_SERVICE | Bind to ports < 1024 | Only for port 80/443 |
| SYS_CHROOT | Change root directory | No |
| KILL | Send signals to any process | No |
| AUDIT_WRITE | Write to audit log | No |
Dropping all capabilities
The most secure approach is to drop all capabilities and add back only what's needed:
# Kubernetes pod spec
apiVersion: v1
kind: Pod
metadata:
name: myapp
spec:
containers:
- name: app
image: myapp:latest
securityContext:
capabilities:
drop:
- ALL
add:
- NET_BIND_SERVICE # Only if binding to port 80/443
# Docker
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE myapp:latest
If your application doesn't bind to ports below 1024, you don't even need NET_BIND_SERVICE. Drop everything.
Read-only root filesystem
A read-only root filesystem prevents any process from writing to the container's filesystem. This stops attackers from:
- Downloading and executing malicious binaries
- Modifying application code or configuration
- Writing persistence mechanisms (cron jobs, startup scripts)
- Tampering with or deleting logs
# Kubernetes
securityContext:
readOnlyRootFilesystem: true
# Docker
docker run --read-only --tmpfs /tmp --tmpfs /var/tmp myapp:latest
Applications that need to write files should use:
- emptyDir volumes for temporary files (Kubernetes)
- tmpfs mounts for writable directories (Docker)
- Persistent volumes for data that must survive container restarts
Resource limits
Resource limits prevent a compromised container from consuming all host resources:
# Kubernetes
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
Without limits, a compromised container can:
- Consume all available memory (OOM killer terminates other containers)
- Use all CPU cycles (starving other workloads)
- Fill the disk with logs or downloaded files
- Open thousands of network connections (exhausting file descriptors)
Putting it all together: a hardened pod spec
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
This pod spec implements every runtime hardening control: non-root user, seccomp, no privilege escalation, read-only filesystem, dropped capabilities, and resource limits. An attacker who compromises this container has minimal capabilities, cannot write files, cannot escalate privileges, and cannot consume excessive resources.
Practice Problems
A Python web application uses Flask, connects to a PostgreSQL database, and serves static files. It does not use raw sockets, mount filesystems, or change file ownership. Write a seccomp profile that allows common web app syscalls but blocks dangerous ones.
Solution
```json
{
"defaultAction": "SCMP_ACT_ALLOW",
"syscalls": [
{
"names": ["mount", "umount2", "reboot", "kexec_load", "ptrace", "open_by_handle_at"],
"action": "SCMP_ACT_ERRNO"
},
{
"names": ["socket", "socketpair"],
"action": "SCMP_ACT_ALLOW",
"args": [
{
"index": 0,
"value": 2,
"op": "SCMP_CMP_EQ"
}
]
}
]
}
```
This profile allows most syscalls by default but blocks known-dangerous ones (mount, reboot, ptrace, etc.). The socket restriction ensures only AF_INET (IPv4) sockets are created, blocking AF_PACKET (raw sockets). For a production profile, use strace to capture the exact syscalls the application uses and build an allowlist. A container currently runs as root with all capabilities, a writable filesystem, and no resource limits. Write the Docker run command and Kubernetes pod spec to harden it.
Solution
Docker:
```bash
docker run \
--user 999:999 \
--read-only \
--tmpfs /tmp \
--cap-drop ALL \
--security-opt no-new-privileges \
--memory 256m \
--cpus 0.5 \
myapp:latest
```
Kubernetes:
```yaml
apiVersion: v1
kind: Pod
metadata:
name: hardened-app
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
containers:
- name: app
image: myapp:latest
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests:
memory: "128Mi"
cpu: "250m"
limits:
memory: "256Mi"
cpu: "500m"
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir: {}
``` Quiz
1. A container runs with the default Docker seccomp profile. An attacker exploits a vulnerability and attempts to use the `mount` syscall to mount a malicious filesystem. What happens?
2. Why is dropping all Linux capabilities more secure than keeping the default Docker capability set?
3. A container has a read-only root filesystem but the application needs to write temporary files. What is the correct approach?
Flashcards
Question
What is seccomp and how does it improve container security?
Click to reveal answer
Answer
Seccomp (Secure Computing Mode) is a Linux kernel feature that filters which syscalls a process can invoke. By restricting syscalls to only what the application needs, it limits what an attacker can do even after compromising the container.
Question
What is the most secure approach to Linux capabilities in containers?
Click to reveal answer
Answer
Drop all capabilities (`capabilities.drop: [ALL]`) and add back only what the application specifically needs. Most applications need zero capabilities.
Question
Why use a read-only root filesystem for containers?
Click to reveal answer
Answer
It prevents attackers from writing malicious binaries, modifying configuration, installing persistence mechanisms, or tampering with logs — even after gaining code execution inside the container.
Question
What does `allowPrivilegeEscalation: false` prevent?
Click to reveal answer
Answer
It prevents a process from gaining more privileges than its parent process. This blocks setuid binaries and other privilege escalation techniques inside the container.
Revision Notes
Key Takeaways
- 1. Seccomp profiles restrict syscalls to only what the application needs, limiting attacker capabilities
- 2. Dropping all Linux capabilities follows least privilege — most applications need zero capabilities
- 3. Read-only root filesystems prevent attackers from writing files even after code execution
- 4. Resource limits contain compromises by preventing resource exhaustion attacks
Interview Tips
- • Use a concrete example: 'We built a custom seccomp profile using strace that reduced available syscalls from 300+ to 45, blocking entire attack categories.'
- • Explain the layered approach: 'Seccomp filters syscalls, capabilities limit privileges, read-only filesystem prevents writes, and resource limits contain resource exhaustion — each layer addresses a different attack vector.'
- • Discuss the trade-off: 'Custom seccomp profiles require testing to avoid blocking legitimate syscalls, but the security benefit is significant.'
Cheat Sheet
Container Runtime Hardening Cheat Sheet
Phase: Container and Kubernetes Security | Level: intermediate
Seccomp profiles:
- Filter which syscalls a container process can invoke
- Docker default blocks ~44 dangerous syscalls (mount, reboot, ptrace, etc.)
- Custom profiles: start with default, add application-specific restrictions
- Build with strace or seccomp-log to capture actual syscall usage
Linux capabilities:
- Default Docker set includes 14 capabilities — most are unnecessary
- Drop ALL, add back only what's needed
- Most applications need zero capabilities
- NET_BIND_SERVICE only needed for ports < 1024
Read-only root filesystem:
- Prevents writing binaries, modifying config, installing persistence
- Use tmpfs/emptyDir for writable directories
- Application code and config remain immutable
Resource limits:
- Prevent compromised containers from consuming all host resources
- Set requests (guaranteed) and limits (maximum)
- Memory, CPU, and ephemeral storage
Complete hardened pod spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
seccompProfile:
type: RuntimeDefault
capabilities:
drop: [ALL]
resources:
limits:
memory: "256Mi"
cpu: "500m"
Continue Learning
Before You Start
- No prerequisites listed.