Auditing a program
Writing a policy by guessing what a program needs is miserable and produces policies that are too wide. Audit mode replaces the guessing with a recording.
bailey audit --save-trace trace.json ./programWhat it does
The program runs while eBPF programs record the filesystem and network access of its process tree. When it exits, the trace is diffed against your current resolved policy, and everything the policy does not already grant is reported:
audit: 34 ungranted access(es) observed
HIGH RISK (review before granting):
[Connect] 93.184.216.34:443 (network egress)
[Read] /home/you/.ssh/id_ed25519 (credential or secret path)
routine (safe to grant):
[Read] /usr/share/mygame/assets.pak
[Write] /home/you/.local/share/mygame/save.datThe split is the point. Routine access is a list of grants to add. High-risk access is a decision, and it is deliberately not presented as a routine addition.
Risk classification
An ungranted access is high risk when it is:
- Network egress. Every outbound destination, always.
- A credential path. Anything containing
.ssh,.gnupg,.aws,.config/gcloud,.password-store, or.mozilla, and files named.netrc,.git-credentials,id_rsa, orid_ed25519. - Outside the target's own directory, unless it is a system path such as
/usr,/etc,/lib, or/dev.
Everything else is routine.
Generating a profile
bailey profile generate --trace trace.json --target ./program > bailey.tomlThe generated profile is deny-by-default and grants exactly the routine findings. High-risk findings are excluded, and the count of what was excluded is reported:
bailey: excluded 2 high-risk access(es); pass --include-high-risk to add themRead the excluded list before reaching for --include-high-risk. A game that connects to a content server and a game that connects to a stranger's server look identical in a trace.
What audit is for
Tightening software you have some reason to trust. You installed it, you believe it is what it claims to be, and you want it to stop having access to your home directory.
Not vetting unknown code. Two reasons. First, a trace shows one run: code that only misbehaves on a certain date, or that detects it is being observed, shows you a clean trace. Second, generating a profile from a hostile program's trace grants exactly what that program did, including its exfiltration. That is why high-risk findings are separated and never included silently.
Audit runs confined
The resolved policy applies during an audit, widened only by read access to the target's own directory. A program audited under the default policy still cannot reach your home directory or the network; the attempts are recorded instead. --unconfined lifts that, and says so when it does.
Requirements and privilege
Audit needs the bailey-bpf-helper binary with CAP_BPF and CAP_PERFMON, and a kernel with BTF. See installation.
The helper does not run the program. The unprivileged main tool spawns it and tells the helper which PID to observe, so the program never runs with elevated privilege. The helper loads a fixed set of eBPF programs and copies bytes; that is its entire job.
What the trace covers
Every way of opening a path, because the recorder attaches below the syscalls at do_filp_open rather than to openat alone. Program executions. Outbound connections, IPv4 and IPv6. Each event carries a timestamp and the path as the kernel resolved it; a relative path is resolved against the accessing process's working directory and marked as such, and one that could not be resolved is listed separately rather than compared against your policy as though it were absolute.
The trace records what it lost. If the kernel could not deliver an event, or the event cap was reached, the count reaches the trace and profile generate refuses to turn it into a profile without --accept-truncated.
How observation is scoped
Where a cgroup can be created for the run, observation is scoped to it. Cgroup membership is inherited at fork by the kernel, so every process the target creates is in scope from its first instruction, and nothing outside the run is recorded.
Where no cgroup is available, the recorder follows the process tree instead, refreshing it from /proc, and the run says so:
bailey: warning: no cgroup for this run, so observation follows the process
tree; a process that is born and reaped between passes can be missed.That fallback is a race: a child that lives a couple of milliseconds usually has its file reads recorded and its exec missed. bailey doctor reports whether this host can give a run a cgroup.