Skip to main content
View Markdown ↗

Copy this page

Select and copy the Markdown below, then paste it into your LLM.

On this page

Configure CI/lock tracing

cilock picks the best tracer the environment allows and records what it could not see. It does not stop a run because a privilege is missing, unless you ask it to. So the common failure is quiet: a build in a locked-down container gets the ptrace fallback and no fanotify, the run passes, and the trace reports gaps nobody reads. This page shows which privileges produce which trace, then gives setups per environment. Read How tracing works first for the terms used here.

What each setup gets

We ran the same small command under cilock run --trace in each setup below and read the signed coverage record. The host was Ubuntu 24.04.5 on kernel 6.8 with AppArmor on, Docker 29.1.3, and cilock built from main on 2026-11-09. macOS was 27.2 on arm64 with cilock 4.9.0.

Where cilock runsTracerfanotifyfilesexec
Root on a Linux host or VMeBPFactivecompletecomplete
Docker, default capabilitiesptraceunavailableincompleteincomplete
Docker, --cap-add BPF --cap-add PERFMONeBPFunavailableincompletecomplete
Docker, BPF, PERFMON and SYS_ADMIN, default AppArmor profileeBPFunavailable (mount denied)incompletecomplete
Docker, BPF, PERFMON and SYS_ADMIN, --security-opt apparmor=unconfinedeBPFactivecompletecomplete
Docker, --privilegedeBPFactivecompletecomplete
Not root, no capabilities (host or container)ptraceunavailableincompleteincomplete
Not root, file capabilities cap_bpf,cap_perfmoneBPFunavailableincompleteincomplete
Not root, file capabilities cap_bpf,cap_perfmon,cap_sys_admin,cap_sys_chrooteBPFactiveincompleteincomplete
Not root, file capabilities with cap_sys_admin but not cap_sys_chrootthe run fails
macOS, any usermacOS sandboxn/aincompleteincomplete

The network and products dimensions were incomplete in every run. network-paths-unobserved is present on every Linux trace (see Reading the coverage record). products-outsider-unguarded appeared on every Linux run because nothing in a trace rules out a process outside it writing the same file after the build's last write.

The two non-root eBPF rows are incomplete because of probe-misses-unmeasured: cilock could not read how many probe hits the kernel skipped, and that loss could be of any type. Running cilock as uid 0, including root inside a container, avoided it in our runs.

The short version: run cilock as root with CAP_BPF, CAP_PERFMON and CAP_SYS_ADMIN, and let it mount. cilock takes those privileges away from the build it traces; "The build's own privileges" below explains how.

What each part needs

eBPF tracer

  • Kernel: Linux 5.11 or later with BTF type information (/sys/kernel/btf/vmlinux). The tracer's programs use BPF task storage, added in 5.11. On an older kernel the eBPF probe can pass while the tracer still fails to load; auto then falls back to ptrace and says why.
  • Capabilities: CAP_BPF and CAP_PERFMON, or root.
  • Seccomp: the container's profile must allow bpf and perf_event_open. Docker 29.1's default profile allowed them once the capabilities were added.
  • Not needed: kernel.perf_event_paranoid and kernel.unprivileged_bpf_disabled do not matter when cilock holds the capabilities. Our host had them at 4 and 2, and a non-root cilock with cap_bpf,cap_perfmon still ran the eBPF tracer.

If the eBPF probe fails, auto falls back to ptrace and prints the reason, for example BPF_MAP_CREATE failed: operation not permitted. Set CILOCK_TRACE_MODE=ebpf or --capture-mode trace:ebpf to make that a hard failure before the build starts.

fanotify

fanotify needs more than one capability, because cilock gives the build a mount namespace of its own and marks only the build's mounts. That way a slow fanotify handler can stall the build, never the host.

  • CAP_SYS_ADMIN, for fanotify_init and for unshare(CLONE_NEWNS).
  • Permission to remount / as a slave mount inside that namespace. Docker's default AppArmor profile denies it, and the warning reads make build mounts slave: permission denied. Run the container with --security-opt apparmor=unconfined, or a profile that allows the mount.
  • CAP_SYS_CHROOT, so cilock can move its own thread back out of the build's namespace with setns. Root in a Docker container has it by default. A non-root cilock with file capabilities needs it granted explicitly. When CAP_SYS_ADMIN is present and CAP_SYS_CHROOT is not, fanotify starts and the run then fails with cilock's thread could not be restored: re-enter mount namespace: operation not permitted.

Under the default --hardening standard, missing fanotify is a warning and a fanotify-unavailable gap. Under --hardening strict or CILOCK_FANOTIFY=1 the run fails with fanotify required but unavailable.

On kernels before 5.3 there is no pidfd_open, so fanotify events from the build's process group can arrive that cilock cannot tie to the build. They are counted in fanotify-pgid-leader-unbound and their files are not hashed for the build.

ptrace tracer

  • Capabilities: none for the usual case. cilock starts the build as its own child under PTRACE_TRACEME.
  • Yama: kernel.yama.ptrace_scope 0 or 1 allows it. At 2 the kernel also requires CAP_SYS_PTRACE in cilock. At 3 ptrace is disabled entirely.
  • Seccomp: the profile must allow ptrace. Docker's default profile does on kernels 4.8 and later.
  • Kernel: kernels before 5.3, such as RHEL 8's 4.18, are supported. cilock tracks syscall entry and exit itself there, because those kernels do not report it.

When ptrace itself is refused, cilock does not fall back to an untraced run. The run fails with fork/exec <command>: operation not permitted and a non-zero exit. The envelope it writes has no command-run attestation, so a policy that requires one refuses it. We checked this with a Docker seccomp profile that denies ptrace.

Even with every privilege, a ptrace trace carries syscalls-untraced, which marks all four dimensions incomplete. Use it when eBPF is impossible, not as the target.

macOS

No root, no entitlement and no install beyond cilock. /usr/bin/sandbox-exec and /usr/bin/log must be present. Sandboxes do not nest: a build that runs sandbox-exec itself, such as Bazel's darwin sandbox, fails under --trace with sandbox_apply: Operation not permitted. cilock refuses to sign rather than record an untraced run as traced.

What the tracer does not need

cilock's tracer does not read /proc/<pid>/map_files, so it does not need CAP_CHECKPOINT_RESTORE, which the kernel requires for that directory from 5.9 on (or CAP_SYS_ADMIN before). The Docker run with BPF, PERFMON and SYS_ADMIN and no CHECKPOINT_RESTORE got complete file coverage.

The build's own privileges

cilock keeps the capabilities it needs and starts the build without them. The rule it applied is recorded as _meta.host.tracee.drop:

dropWhenThe build runs as
root-caplesscilock is root, not under sudo (every root container)uid 0, no capabilities, no_new_privs
sudocilock is root under sudothe invoking user (SUDO_UID/SUDO_GID)
userCILOCK_TRACEE_USER=<uid>:<gid>that uid and gid, no capabilities
inheritedCILOCK_TRACEE_USER=inheritcilock's uid and capabilities
nonecilock is not rootcilock's own user

A build step that needs a capability itself breaks under root-capless. Use CILOCK_TRACEE_USER=inherit only if you accept that the build could then interfere with the tracer; the attestation says so, and a policy can refuse it.

Setups

Each setup says whether we ran it. "Verified" means we ran it as written, or with only the image and command changed, and read the coverage record. "Not run" setups follow from the verified ones, and you should check your first trace with the jq command before relying on them.

Docker (verified)

docker run --rm \
  --cap-add BPF --cap-add PERFMON --cap-add SYS_ADMIN \
  --security-opt apparmor=unconfined \
  -v "$PWD:/src" -w /src your-build-image \
  cilock run --trace --step build -o build.att.json -- make

This gave eBPF with fanotify active and complete files and exec coverage. --privileged gives the same trace, with far more access than cilock needs.

Drop thisAnd you get
--security-opt apparmor=unconfinedeBPF, no fanotify (fanotify-unavailable): exec complete, files incomplete
SYS_ADMIN tooThe same
BPF and PERFMON tooptrace: nothing complete

On a host that uses SELinux instead of AppArmor we have not tested the mount; if fanotify reports a permission error there, the container's SELinux label is the first thing to check.

Kubernetes (not run)

The container that runs cilock needs the same three capabilities, uid 0, and an AppArmor profile that allows the mount:

containers:
  - name: build
    image: your-build-image
    securityContext:
      runAsUser: 0
      capabilities:
        add: ["BPF", "PERFMON", "SYS_ADMIN"]
      appArmorProfile:
        type: Unconfined

securityContext.appArmorProfile needs Kubernetes 1.30 or later. On older clusters use the annotation container.apparmor.security.beta.kubernetes.io/build: unconfined on the pod. The Pod Security Standards baseline and restricted levels do not allow these capabilities, so the namespace needs the privileged level. If the pod uses seccompProfile: RuntimeDefault and cilock still reports BPF_MAP_CREATE failed: operation not permitted with the capabilities added, the runtime's profile is blocking bpf.

Actions Runner Controller. cilock runs wherever the workflow step runs. With the default runner mode the step runs in the runner container, so put the securityContext above on that container in the runner scale set's pod template. With containerMode: kubernetes, a job that sets container: runs in a separate job pod, and that pod needs the settings instead.

GitHub Actions, hosted runners (not run)

Hosted Ubuntu runners are VMs, and the runner user has passwordless sudo. Run cilock under sudo and it gets the same privileges as the root-on-a-host row above, while the build drops back to the runner user:

- name: Build
  run: |
    sudo -E "$(command -v cilock)" run --trace --step build \
      -o "$RUNNER_TEMP/build.att.json" -- make

-E keeps the GITHUB_* and ACTIONS_ID_TOKEN_* variables that attestors and keyless signing read. $(command -v cilock) avoids sudo resetting PATH.

cilock-action with trace: true runs cilock as the runner user without sudo. That is the not-root, no-capabilities row: ptrace and no fanotify. Use the CLI under sudo as above when you need complete coverage. For a job with container:, add the Docker options from the Docker setup to container.options.

GitLab runners (not run)

For the Docker executor, set the capabilities and the AppArmor override in the runner's config.toml:

[runners.docker]
  cap_add = ["BPF", "PERFMON", "SYS_ADMIN"]
  security_opt = ["apparmor=unconfined"]

The job image must run as root. For the Kubernetes executor, apply the settings from the Kubernetes setup to the build container. On a shell executor, run cilock with sudo as on a GitHub hosted runner.

Not root (verified)

When cilock cannot run as root, grant it file capabilities:

sudo setcap cap_bpf,cap_perfmon,cap_sys_admin,cap_sys_chroot+ep "$(command -v cilock)"

This gave eBPF with fanotify active. files and exec were still incomplete because of probe-misses-unmeasured, which root avoids. Do not grant cap_sys_admin without cap_sys_chroot: the run fails. With only cap_bpf,cap_perfmon you get eBPF without fanotify. File capabilities are lost when the binary is replaced, so reapply them after every upgrade.

macOS (verified)

cilock run --trace --step build -o build.att.json -- make

No setup. A macOS trace is never complete in any dimension; see the gaps listed on How tracing works.

Check your trace

After a first run in a new environment, read three things from the attestation (the jq command prints them):

  1. _meta.host.tracer.backend is ebpf. If it is ptrace+seccomp, reason says why.
  2. summary.coverage.gaps has no fanotify-unavailable.
  3. summary.coverage.dimensions.files.complete and dimensions.exec.complete are true.

Then turn that into policy, so a later change to the runner cannot thin the trace without failing verification. Require coverage in policy has the rules.

Troubleshooting

SymptomCauseFix
stderr: eBPF unavailable (BPF_MAP_CREATE failed: operation not permitted); tracer is ptrace+seccompNo CAP_BPF/CAP_PERFMON, or seccomp blocks bpfAdd BPF and PERFMON, or run as root; check the seccomp profile
tracer is ptrace+seccomp and reason says the probe passed but the tracer could not startKernel older than 5.11, or no BTFUse a 5.11+ kernel with /sys/kernel/btf/vmlinux
Gap fanotify-unavailable, detail unshare mount namespace: operation not permittedNo CAP_SYS_ADMINAdd SYS_ADMIN
Gap fanotify-unavailable, detail make build mounts slave: permission deniedAppArmor (or another LSM) denies the mount--security-opt apparmor=unconfined, or appArmorProfile: Unconfined
Run fails: re-enter mount namespace: operation not permittedCAP_SYS_ADMIN without CAP_SYS_CHROOTGrant CAP_SYS_CHROOT too
Run fails: fanotify required but unavailable--hardening strict or CILOCK_FANOTIFY=1 without the fanotify privilegesGrant them, or use --hardening standard and enforce coverage in policy
Run fails: fs-verity required but unsupported on workspace FS--hardening strict on a filesystem without fs-veritySet CILOCK_FSVERITY=auto beside --hardening strict
Run fails: attestation rejected (--require-zero-drops)Events were lost, often on a heavily loaded hostRe-run; give the build host headroom
Gap events-dropped, all dimensions incompleteThe ring buffer overflowed under loadRe-run; reduce parallel load on the host
Gap probe-misses-unmeasuredcilock could not read the kernel's missed-probe counts, as in non-root runsRun cilock as root
Gap syscalls-untracedThe ptrace tracer ranFix whatever made eBPF unavailable
Run fails: fork/exec ...: operation not permitted, no command-run attestationptrace refused: seccomp, or ptrace_scope 3Allow ptrace; or get eBPF working
CILOCK_TRACE_MODE="..." is not recognizedTypo in the variableUse auto, ebpf or ptrace
macOS: sandbox_apply: Operation not permittedThe build uses its own sandboxRun that build without --trace, or turn off its sandboxing
Few or no materials, with gap opens-unrecordedfanotify hashed files the build opened while the tracer recorded no openTreat the material list as wrong; re-run, and check the tracer and its gaps
Few or no materials, no coverage record at allThe run was not traced (no --trace), so materials came from a directory walkAdd --trace