Installing seccomp filters directly inside a program using libseccomp, python-seccomp, and Go, with per-feature syscall trimming. Including socket family restrictions for network daemons and blocking dangerous syscalls like unshare, mount, and ptrace.

In episode 10 you installed seccomp from the outside: pods, service units, containers. All those approaches share one weakness: they rely on the person running the application. What if an application could lock itself down — restricting the syscalls it's allowed to make without waiting for an admin to install a profile?
Episode 11 covers self-confinement: an application installs its seccomp filter from within its own code. You'll see libseccomp as the foundation, python-seccomp and Go as the practical ways, and real patterns: trimming syscalls per feature, restricting socket families for network daemons, and blocking dangerous syscalls.
Imagine a pilot locking the cockpit door after takeoff. Before takeoff, the crew needs free access — opening config files, connecting to the database, opening ports. After that, the need is gone, and every remaining door only increases risk. The same principle applies to applications: do your initialization first, then lock.
The right pattern is always sequential:
setuid/setgid) if needed.After step 3, unregistered syscalls are rejected — even if the application is successfully exploited, every subsequent step (spawning a shell, reading files, opening sockets) is already pruned. This is defense in depth at the front line: right next to the vulnerable code.
Note
Self-confinement isn't a replacement for the external profiles from episodes 8–10, but a complement. Container profiles prune from the outside; in-application filters prune from the inside. Two layers checking the same dimension from opposite directions — making it twice as hard for an attacker.
libseccomp is the C library that wraps filter creation — seccomp_init creates a context, seccomp_rule_add adds rules, seccomp_load loads into the kernel. Every other language binding is just a wrapper around these three functions:
#include <seccomp.h>
#include <errno.h>
int main(void) {
scmp_filter_ctx ctx = seccomp_init(SCMP_ACT_ERRNO(EPERM));
if (ctx == NULL) return 1;
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(read), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(write), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(openat), 0);
seccomp_rule_add(ctx, SCMP_ACT_ALLOW, SCMP_SYS(exit_group), 0);
seccomp_load(ctx);
seccomp_release(ctx);
return 0;
}Read the flow: init sets the default action (ERRNO(EPERM) for all syscalls), rule_add allows the specific ones, and load sends the filter to the kernel. After load, the rules can't be changed anymore — the lock is already engaged.
In Python, the official binding is python-seccomp, installed with pip install python-seccomp. The same configuration becomes much more concise:
import seccomp
f = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1))
for name in [
"read", "write", "openat", "close", "mmap",
"mprotect", "futex", "exit_group", "rt_sigaction",
]:
f.add_rule(seccomp.ALLOW, name)
f.load()Note a detail that often trips people up: the syscall list must not forget the ones the interpreter itself uses. The Python runtime calls mmap, mprotect, futex, and rt_sigaction for memory allocation and signals — a filter that's too thin kills the interpreter in the first second. Always run your test suite after load().
In Go, the two most relevant packages: github.com/seccomp/libseccomp-golang for building filters directly, and github.com/containers/common/pkg/seccomp which can load Docker-style JSON profiles into a Go process. An example of the first:
package main
import (
"log"
seccomp "github.com/seccomp/libseccomp-golang"
)
func main() {
filter, err := seccomp.NewFilter(seccomp.ActErrno.SetReturnCode(1))
if err != nil {
log.Fatal(err)
}
defer filter.Release()
names := []string{"read", "write", "openat", "close",
"mmap", "mprotect", "futex", "exit_group"}
for _, name := range names {
id, err := seccomp.GetSyscallFromName(name)
if err != nil {
log.Fatal(err)
}
if err := filter.AddRule(id, seccomp.ActAllow); err != nil {
log.Fatal(err)
}
}
if err := filter.Load(); err != nil {
log.Fatal(err)
}
}The strength of GetSyscallFromName is name portability: you write syscall names, not numbers, and the library translates them per architecture. The same code runs on x86_64 and ARM64 unchanged.
A network daemon needs socket, bind, and connect — but not every socket family. A web server shouldn't need AF_PACKET (raw packets) or AF_NETLINK (communication with the kernel). With argument filters, you can allow socket only for certain families. In Python:
import seccomp
f = seccomp.SyscallFilter(defaction=seccomp.ERRNO(1))
f.add_rule(seccomp.ALLOW, "socket", seccomp.Arg(0, seccomp.EQ, 1)) # AF_UNIX
f.add_rule(seccomp.ALLOW, "socket", seccomp.Arg(0, seccomp.EQ, 2)) # AF_INET
f.add_rule(seccomp.ALLOW, "bind")
f.add_rule(seccomp.ALLOW, "connect")
f.add_rule(seccomp.ALLOW, "accept4")
f.add_rule(seccomp.ALLOW, "getsockname")
f.load()seccomp.Arg(0, seccomp.EQ, 1) means: the 0th argument of socket must equal 1 (AF_UNIX). This combination locks the daemon so it can only communicate via Unix sockets and IPv4 — every attempt to open AF_INET6, AF_PACKET, or AF_NETLINK is immediately rejected, even if the application is already compromised. This is episode 5's lock applied to a daemon's real life.
Tip
The same principle applies to every syscall with a restricting argument: openat can be restricted per path, clone per namespace flag, connect per port. Trim syscalls per feature, not just per name — the more specific, the narrower the gap.
There's a class of syscalls that ordinary daemons almost never need, but that are very valuable to attackers. Block them explicitly, even when the daemon runs without privileges:
unshare # creates new namespaces — a sandbox escape path
mount # modifies the host filesystem
ptrace # debugs or injects into other processes
bpf # manipulates kernel BPF programs
perf_event_open # spies on system activity
keyctl # manages kernel keyrings
process_vm_readv # reads another process's memory
process_vm_writev # writes to another process's memoryWhy block "privilege-requiring" syscalls if the daemon has no privileges? Because of defense in depth. An attacker who finds a way to raise privileges still faces the next layer — and this filter cuts it off at the syscall level, long before those privileges become useful. unshare in particular matters: it's raw material for container escape exploits, and blocking it from inside the application cuts that path from the start.
Warning
Loading a filter isn't a reason to stop dropping privileges. Seccomp limits what actions can be called; capabilities and UID drops limit with what rights. The combination of both — not one replacing the other — is what makes a sandbox truly solid.
The secret to a long-lived filter isn't a long list, but mapping features to syscalls. Ask per feature: what's needed to run it?
read/write, mmap, exit_group; block socket, connect, execve.socket (restricted to AF_INET/AF_INET6), bind, accept, sendto, recvfrom; block ptrace, keyctl, unshare.clone, execve, openat that are no longer needed.Each feature produces a line of thinking, not a line of code. The result is a filter that can be explained — and a filter that can be explained is a filter that can be maintained.
In episode 11 you understood self-confinement: an application installs a filter from within its own code with libseccomp as the foundation, python-seccomp and libseccomp-golang as more convenient wrappers, and the per-feature trimming pattern that restricts syscalls to actual needs. You can also restrict a daemon's socket families with argument filters and block dangerous syscalls like unshare, mount, ptrace, bpf, and perf_event_open.
The keys to take home:
In-application filters demand high discipline: the application must know exactly what it does. How does the seccomp mechanism interact with other kernel security layers — capabilities, namespaces, and LSMs? In episode 12, we move into Seccomp & Capabilities Combinations: composing profiles that trim syscalls while sharpening the capability set, so the layered defenses work with each other, not against each other.