Lab 03 — Syscall Policy Synthesizer

Difficulty: 3/5 | Runs locally: yes

Pairs with the Phase 01 WARMUP Chapter 8 (the syscall boundary) and foreshadows the Phase 06 seccomp compiler lab.

Why This Lab Exists (Purpose & Goal)

The system-call interface is the security boundary of an operating system — everything dangerous a process can do, it does through a syscall. If you can restrict which syscalls a process may make, you shrink what a compromised process can do even when its code is fully attacker-controlled. The goal of this lab is to learn how a least-privilege syscall allowlist is actually built: by observing the calls a program makes across many legitimate paths, then turning those observations into a candidate policy — without ever blindly trusting the observation as if it were proof.

The Concept, In the Weeds

The naive approach — "trace the program once, allow whatever it called" — is dangerously incomplete, and this lab is designed to make you feel why:

  • One trace under-counts. A single happy-path run misses the syscalls used on error paths, in locale handling, in DNS resolution, in thread startup, in the allocator's slow path, and after a dependency upgrade. An allowlist built from one trace will break the service in production the first time it hits an unobserved path.
  • Observation is a starting point, not authorization. Just because a program did call ptrace once does not mean it should be allowed to — it might be debug code that should never ship. So the synthesizer produces candidate additions for human review and separately flags forbidden calls (ptrace, mount, kernel-module administration, raw networking) that signal something is wrong, but it never auto-approves.
  • seccomp is not semantic authorization. Allowing openat does not decide which file; the path is a pointer seccomp can't dereference (you'll prove this in Phase 06). So the allowlist controls the verbs, while filesystem/LSM policy controls the targets.

Why This Matters for Protecting the Company

Syscall confinement is what turns a remote code execution into a contained, low-value foothold instead of a host takeover. It is the workhorse behind container runtimes, browser sandboxes, and serverless microVM jailers. Knowing how to generate and review a tight policy — rather than shipping the permissive default or, worse, disabling it because it "broke something" — is directly how you reduce the blast radius of the next bug in a service the company runs. The "observe, then review, never auto-approve" discipline is also how you avoid the opposite failure: a policy so tight it causes outages and gets ripped out.

How This Is Used on the Job

This is the on-ramp to writing real seccomp profiles and systemd SystemCallFilter directives (Phase 05/06). In production you'd collect observations with strace -f, seccomp audit logs, or libseccomp's logging action, across startup/steady-state/error/shutdown, then synthesize and review the allowlist, then regression-test it after every dependency bump (the sibling Phase 06 seccomp-regression lab).

Attack / Failure Fixtures

ptrace (read/write another process's memory — sandbox escape), mount (filesystem manipulation), kernel-module administration, and raw networking — each a syscall whose appearance in a normal web service is a signal, not a default to allow.

Build It

Implement the synthesizer: compare observed syscalls from multiple legitimate execution paths against an approved baseline; emit candidate additions and forbidden calls separately; never auto-approve.

LAB_MODULE=solution pytest -q
# On Linux: collect observations with `strace` from OWNED binaries across multiple paths.

Validation — What You Should Be Able to Do Now

  • Explain why an allowlist built from a single trace breaks in production, and which paths you must exercise to build a complete one.
  • Articulate why "the program called it" is not "the program may call it," and design a human-review step into policy synthesis.
  • Explain why seccomp controls verbs but not targets, and what layer enforces the target.

The Broader Perspective

You practiced a pattern that recurs everywhere in security automation: observe behavior to propose a policy, but keep a human in the loop for authorization. The same shape appears in detection engineering (a candidate detection from observed telemetry, reviewed before it pages anyone), in least-privilege IAM (proposed roles from access logs, reviewed before grant), and in AI agents (proposed actions from a model, decided by deterministic policy). Automation accelerates the proposal; judgment owns the grant. Confusing the two is how both over-permissioned and outage-causing policies get shipped.

Interview Angle

  • "How do you build a seccomp allowlist for a service?" — Observe its real syscalls across startup, steady state, error, locale, DNS, and shutdown; synthesize a default-deny allowlist; review it (don't auto-approve); pin the architecture; regression-test it after upgrades. Targets (paths, destinations) are enforced by the filesystem/LSM/network, not seccomp.

References