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 runs | Tracer | fanotify | files | exec |
|---|---|---|---|---|
| Root on a Linux host or VM | eBPF | active | complete | complete |
| Docker, default capabilities | ptrace | unavailable | incomplete | incomplete |
Docker, --cap-add BPF --cap-add PERFMON | eBPF | unavailable | incomplete | complete |
Docker, BPF, PERFMON and SYS_ADMIN, default AppArmor profile | eBPF | unavailable (mount denied) | incomplete | complete |
Docker, BPF, PERFMON and SYS_ADMIN, --security-opt apparmor=unconfined | eBPF | active | complete | complete |
Docker, --privileged | eBPF | active | complete | complete |
| Not root, no capabilities (host or container) | ptrace | unavailable | incomplete | incomplete |
Not root, file capabilities cap_bpf,cap_perfmon | eBPF | unavailable | incomplete | incomplete |
Not root, file capabilities cap_bpf,cap_perfmon,cap_sys_admin,cap_sys_chroot | eBPF | active | incomplete | incomplete |
Not root, file capabilities with cap_sys_admin but not cap_sys_chroot | the run fails | |||
| macOS, any user | macOS sandbox | n/a | incomplete | incomplete |
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;autothen falls back to ptrace and says why. - Capabilities:
CAP_BPFandCAP_PERFMON, or root. - Seccomp: the container's profile must allow
bpfandperf_event_open. Docker 29.1's default profile allowed them once the capabilities were added. - Not needed:
kernel.perf_event_paranoidandkernel.unprivileged_bpf_disableddo not matter when cilock holds the capabilities. Our host had them at 4 and 2, and a non-root cilock withcap_bpf,cap_perfmonstill 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, forfanotify_initand forunshare(CLONE_NEWNS).- Permission to remount
/as a slave mount inside that namespace. Docker's default AppArmor profile denies it, and the warning readsmake 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 withsetns. Root in a Docker container has it by default. A non-root cilock with file capabilities needs it granted explicitly. WhenCAP_SYS_ADMINis present andCAP_SYS_CHROOTis not, fanotify starts and the run then fails withcilock'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_scope0 or 1 allows it. At 2 the kernel also requiresCAP_SYS_PTRACEin 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:
drop | When | The build runs as |
|---|---|---|
root-capless | cilock is root, not under sudo (every root container) | uid 0, no capabilities, no_new_privs |
sudo | cilock is root under sudo | the invoking user (SUDO_UID/SUDO_GID) |
user | CILOCK_TRACEE_USER=<uid>:<gid> | that uid and gid, no capabilities |
inherited | CILOCK_TRACEE_USER=inherit | cilock's uid and capabilities |
none | cilock is not root | cilock'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 this | And you get |
|---|---|
--security-opt apparmor=unconfined | eBPF, no fanotify (fanotify-unavailable): exec complete, files incomplete |
SYS_ADMIN too | The same |
BPF and PERFMON too | ptrace: 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):
_meta.host.tracer.backendisebpf. If it isptrace+seccomp,reasonsays why.summary.coverage.gapshas nofanotify-unavailable.summary.coverage.dimensions.files.completeanddimensions.exec.completearetrue.
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
| Symptom | Cause | Fix |
|---|---|---|
stderr: eBPF unavailable (BPF_MAP_CREATE failed: operation not permitted); tracer is ptrace+seccomp | No CAP_BPF/CAP_PERFMON, or seccomp blocks bpf | Add 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 start | Kernel older than 5.11, or no BTF | Use a 5.11+ kernel with /sys/kernel/btf/vmlinux |
Gap fanotify-unavailable, detail unshare mount namespace: operation not permitted | No CAP_SYS_ADMIN | Add SYS_ADMIN |
Gap fanotify-unavailable, detail make build mounts slave: permission denied | AppArmor (or another LSM) denies the mount | --security-opt apparmor=unconfined, or appArmorProfile: Unconfined |
Run fails: re-enter mount namespace: operation not permitted | CAP_SYS_ADMIN without CAP_SYS_CHROOT | Grant CAP_SYS_CHROOT too |
Run fails: fanotify required but unavailable | --hardening strict or CILOCK_FANOTIFY=1 without the fanotify privileges | Grant 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-verity | Set CILOCK_FSVERITY=auto beside --hardening strict |
Run fails: attestation rejected (--require-zero-drops) | Events were lost, often on a heavily loaded host | Re-run; give the build host headroom |
Gap events-dropped, all dimensions incomplete | The ring buffer overflowed under load | Re-run; reduce parallel load on the host |
Gap probe-misses-unmeasured | cilock could not read the kernel's missed-probe counts, as in non-root runs | Run cilock as root |
Gap syscalls-untraced | The ptrace tracer ran | Fix whatever made eBPF unavailable |
Run fails: fork/exec ...: operation not permitted, no command-run attestation | ptrace refused: seccomp, or ptrace_scope 3 | Allow ptrace; or get eBPF working |
CILOCK_TRACE_MODE="..." is not recognized | Typo in the variable | Use auto, ebpf or ptrace |
macOS: sandbox_apply: Operation not permitted | The build uses its own sandbox | Run that build without --trace, or turn off its sandboxing |
Few or no materials, with gap opens-unrecorded | fanotify hashed files the build opened while the tracer recorded no open | Treat the material list as wrong; re-run, and check the tracer and its gaps |
| Few or no materials, no coverage record at all | The run was not traced (no --trace), so materials came from a directory walk | Add --trace |