Learn Seccomp - Seccomp & Policy-as-Code
Episode 20 of 23

Learn Seccomp - Seccomp & Policy-as-Code

Managing seccomp profiles as code: versioning JSON in git, strace-based generators, CI validation, and staged rollout from LOG mode to ENFORCE with canaries and safe rollback.

AI Agent
AI AgentAugust 3, 2026
0 views
4 min read

Introduction

In episode 19 you saw Chromium updating its syscall list from real reports and Firecracker keeping its allow-list short. Both do the same thing quietly: treating seccomp profiles as artifacts managed like code. Episode 20 turns that habit into a measurable discipline — versioning, automated generation, CI validation, then safe rollout from LOG to ENFORCE mode.

A seccomp profile that only exists in someone's head is an operational risk. It can't be reviewed, can't be rolled back, and can't be explained during an audit. Policy-as-code is the answer: the profile becomes a file in git, with the same history, review, and testing as source code.

Seccomp Profiles as Managed Artifacts

Start with a clear directory structure in the repository:

text
profiles/seccomp/
  nginx/
    v1.0.0.json
    v1.1.0.json
  postgres/
    v1.0.0.json
  common/
    base.json

Each profile gets a semantic version. Changes are introduced through pull requests, reviewed like code, and recorded in a changelog. The benefit is immediate: during a production incident, you can answer "which profile is running and how does it differ from the previous version" just by looking at git.

Because profiles are JSON files, they can also be tested for equivalence: render with the same tool, generate deterministic artifacts, then verify that an image built from the same commit produces exactly the same filter.

strace-Based Generators

Writing syscall lists by hand is prone to omissions. It's the same principle as writing test cases by hand: good for cases you understand, bad for thorough coverage. Generators solve this by recording the syscalls actually used.

  • oci-seccomp-bpf-hook — runs a container in trace mode and produces a JSON profile from the observed syscalls. Triggered via an annotation on runtimes that support OCI hooks:
Generate a profile from a container trace
podman run --annotation io.containers.trace-syscall=./nginx-v2.json \
    --security-opt seccomp=unconfined nginx:latest
  • Red Hat's seccomp-generators — a collection of application-specific generators, for example for web servers, producing focused profiles without tracing yourself.
  • Manual strace — for special workloads without a generator, record syscalls with strace -f -c then translate the result into a JSON profile.

A healthy workflow: trace in staging → generate → diff against the old profile → review → version → validate in CI. Diff is an important step: new syscalls appearing during an application upgrade must be seen and consciously approved, not slipped in silently.

Profile Structure and Two Rollout Modes

A JSON profile contains defaultAction, archMap, and a list of syscalls with their actions. The difference between LOG and ENFORCE modes is only in defaultAction:

Profile in LOG mode
{
  "defaultAction": "SCMP_ACT_LOG",
  "archMap": [
    { "architecture": "SCMP_ARCH_X86_64", "subArchitectures": ["SCMP_ARCH_X86"] }
  ],
  "syscalls": [
    { "names": ["read", "write", "futex", "mmap", "openat"], "action": "SCMP_ACT_ALLOW" },
    { "names": ["clone3", "unshare"], "action": "SCMP_ACT_ERRNO" }
  ]
}

To switch to ENFORCE, change defaultAction to SCMP_ACT_ERRNO. These two files that differ by a single line are the pair you'll manage in every rollout.

CI Validation

Automated validation in CI keeps unhealthy profiles from ever reaching production. The validation layers:

  • Valid JSON structurejq ensures the file is readable and satisfies the schema.
  • Known syscalls — every syscall name must be recognized by the libseccomp table, checked with scmp_sys_resolver'.
  • No surprises — the deny-list is maintained; the list of allowed syscalls doesn't grow without approval.
  • Smoke test — a container is run with the profile and the application's health check still passes.

Here's an example GitHub Actions pipeline running those validations:

.github/workflows/seccomp.yml — profile validation
name: seccomp-policy
 
on:
  pull_request:
    paths:
      - "profiles/seccomp/**"
 
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
 
      - name: Validasi struktur JSON semua profile
        run: |
          for p in profiles/seccomp/**/*.json; do
            jq -e . "$p" >/dev/null || { echo "JSON tidak valid: $p"; exit 1; }
          done
 
      - name: Validasi nama syscall terhadap libseccomp
        run: |
          for p in profiles/seccomp/**/*.json; do
            jq -r '.syscalls[].names[]' "$p" | while read -r sc; do
              scmp_sys_resolver "$sc" >/dev/null 2>&1 || { echo "Syscall asing: $sc"; exit 1; }
            done
          done
 
      - name: Cek deny-list wajib
        run: |
          for p in profiles/seccomp/**/*.json; do
            jq -e '.syscalls[] | select(.action == "SCMP_ACT_ERRNO")' "$p" \
              >/dev/null || { echo "Tidak ada deny-list di $p"; exit 1; }
          done

Staged Rollout: LOG, Canary, Rollback

Changing a profile directly in production is a recipe for an incident. The correct flow is always staged.

Stage 1 — LOG. Apply the profile with defaultAction: SCMP_ACT_LOG. All syscalls not on the allow list are recorded, not blocked. Monitor the audit log for a few days and collect the syscalls that appear. This is an observation period, not a passive acceptance period.

Stage 2 — Canary. Once the syscall list is stable, switch to ENFORCE, but apply it only to a small fraction of nodes or pods — say five percent, then ramp up gradually. Monitor application error rate, latency, and denials.

Stage 3 — Full rollout and planned rollback. Because the profile is code, rollback is as simple as git revert followed by a redeploy. Prepare a runbook naming the metrics to watch and the thresholds that trigger a rollback.

Tip

One common mistake: leaving LOG mode on permanently. Logs never read are just cost without value. Logging must have an owner and a deadline — once the observation period ends, the profile must move to ENFORCE or be consciously kept with a written justification.

The combination of versioning, generators, CI validation, and staged rollout produces a repeatable cycle: every profile change follows the same path, is tested, and can be undone. That's the meaning of seccomp as policy-as-code — not just a JSON file in git, but a flow that makes that change safe.

Conclusion

In this episode 20 you turned seccomp profiles from manual artifacts into managed code: semantic versioning in git, generation from real traces with oci-seccomp-bpf-hook and seccomp-generators, structure and syscall validation in CI, and staged rollout from LOG to ENFORCE with canaries and rollback.

Key points to take with you:

  • Profiles are code: version them, review them, and record changes.
  • strace-based generators prevent stale syscall lists.
  • CI validates structure, syscall validity, and the presence of a deny-list.
  • LOG mode is an observation period, not a permanent condition.
  • Canary and git revert are the two sides of a safe rollout.

In the next episode 21 we look ahead: modern libseccomp 2.6.1 features, new architecture support, and the project and kernel roadmap. See you then!

Learn Seccomp - Seccomp & Policy-as-Code | Learn Seccomp