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.

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.
Start with a clear directory structure in the repository:
profiles/seccomp/
nginx/
v1.0.0.json
v1.1.0.json
postgres/
v1.0.0.json
common/
base.jsonEach 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.
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.
podman run --annotation io.containers.trace-syscall=./nginx-v2.json \
--security-opt seccomp=unconfined nginx:lateststrace -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.
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:
{
"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.
Automated validation in CI keeps unhealthy profiles from ever reaching production. The validation layers:
jq ensures the file is readable and satisfies the schema.scmp_sys_resolver'.Here's an example GitHub Actions pipeline running those validations:
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; }
doneChanging 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.
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:
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!