Verifying zero dependencies
fail2zig’s architectural claim is empirical. Either the binary is
statically linked, programs the firewall via netlink on the default
backend, and carries no dynamic libraries — or it does not. Subprocesses
occur only in optional paths: the journald log source (journalctl) and
the ipset / iptables fallback backends. A deployment on the default
nftables backend with no source = journald jail spawns nothing at all.
Operators running a root-privileged security daemon
deserve to check that claim rather than take it on faith.
This guide walks through the verification every reviewer should be able to run on their own system before deploying fail2zig in production. Each check takes a few seconds and requires no special tooling beyond what ships with any modern Linux distribution.
1. The binary is statically linked
A static binary has no dynamic library dependencies loaded at runtime. Its full code is inside the ELF image itself.
ldd /usr/local/bin/fail2zigExpected output:
statically linkedIf ldd lists shared libraries, the binary was built without the
-Dtarget=x86_64-linux-musl flag, or the build picked up glibc
accidentally. Static linking is a release-build property, not an
architectural guarantee — debug builds are dynamic by default.
A second sanity check for musl-linked release binaries:
file /usr/local/bin/fail2zigLook for statically linked in the output, and ELF 64-bit LSB executable with no interpreter field.
2. The default ban path execs nothing
On the default nftables backend with file-based log sources, fail2zig
spawns no subprocess during normal operation — no fork, no execve, no
/bin/sh. The firewall is programmed over netlink in-process.
sudo strace -f -e trace=execve -p $(pgrep fail2zig)Expected on that setup: no output for the lifetime of the session. Watch for a minute or two under normal ban traffic.
Two optional paths legitimately exec a helper, and only these:
- a jail with
source = journaldspawnsjournalctlon its poll timer; - a jail using the
ipsetoriptablesfallback backend spawns that CLI (argv, never/bin/sh) to program its set.
Any execve of something else — nft, sh, bash, a downloader — is a
zero-dependency violation. If you see one, file an advisory via
SECURITY.md.
Press Ctrl-C to stop the trace when you have watched long enough to
be satisfied.
3. Inspect the actual syscall mix
A more complete view shows what fail2zig is doing. Expect to see log watching, event-loop wakeups, and netlink traffic — nothing that resembles a subprocess or a filesystem traversal.
sudo strace -c -p $(pgrep fail2zig) -- sleep 30The -c summary (after 30 seconds of sampling) will list the
syscalls the daemon actually uses. Expected entries include:
| Syscall | Why fail2zig uses it |
|---|---|
epoll_wait | Event loop waiting for watcher / socket activity. |
read | Reading new bytes from log files. |
inotify_add_watch, inotify_read | Watching log files for changes. |
recvmsg, sendmsg | Netlink traffic to the kernel firewall. |
accept4 | Accepting fail2zig-client IPC connections. |
timerfd_settime | Driving ban expiry timers. |
write | Writing operational logs to stderr / journal. |
Entries that would be red flags: execve, fork, clone of a new
process (thread creation is fine), openat against /usr/sbin/* or
/usr/bin/*, connect to anything outside of the IPC socket and
netlink.
4. What talks to nftables at runtime
fail2zig reaches the kernel via netlink, which shows up as an open
socket on an AF_NETLINK address family.
sudo lsof -p $(pgrep fail2zig) | grep -i netlinkExpected output:
fail2zig ... NETLINK ... connection to "netfilter"This is the socket fail2zig uses to install its scaffold, add
entries to the banned_ipv4 / banned_ipv6 sets, and drain ACK
replies. There should be no process spawning a nft binary on the
side. See netlink interop for the
message-level detail.
5. Confirm the kernel state matches claimed state
The last piece is verifying that bans the daemon reports are actually installed in the kernel, not just remembered in daemon memory.
# List bans the daemon thinks are active.
sudo fail2zig-client list
# Query the kernel directly.
sudo nft list set inet fail2zig banned_ipv4The two lists should agree. If the daemon claims a ban the kernel does not know about, that is a reconciliation bug — file it via the public issue tracker.
6. Reproduce the release binary yourself
For an auditor, the strongest verification is building the binary from source and comparing checksums. fail2zig publishes reproducible builds.
git clone https://github.com/ul0gic/fail2zig
cd fail2zig
git checkout v0.2.2 # or your target version
zig build -Doptimize=ReleaseSafe \
-Dtarget=x86_64-linux-musl
sha256sum zig-out/bin/fail2zig
curl -sL https://github.com/ul0gic/fail2zig/releases/download/v0.2.2/SHA256SUMS | grep fail2zigThe two sha256sum values should match, byte-for-byte. A mismatch
means the release artifact does not correspond to the public source
tree at that tag — worth escalating.
7. Release provenance
Releases are not signed with a standalone tool. Instead, each binary carries a
SLSA build-provenance attestation produced by the release workflow, and a
SHA256SUMS manifest is published alongside the assets. Verify the attestation
with the GitHub CLI:
gh attestation verify fail2zig-v0.2.2-x86_64-linux-musl \
--repo ul0gic/fail2zigA successful verification proves the binary was built by the project’s release
workflow from the tagged source. The verify-release CI job independently
re-downloads the published artifacts and re-checks them against SHA256SUMS.
When any of these checks fails
If the daemon is not statically linked, has dynamic libraries visible in
ldd, spawns a subprocess other than journalctl / ipset / iptables,
or fails to reproduce against the published source tree, something is
wrong. These are not tuning issues. They are integrity violations.
- Shared libraries in
ldd: the binary was not built with the musl target. Rebuild or download the musl release. execveof anything other thanjournalctl/ipset/iptables: a regression. The journald source spawnsjournalctl; the optional ipset and iptables backends spawn their CLI. Anexecveof/bin/shornftis an integrity violation — file a security advisory via SECURITY.md.sha256summismatch: the binary you have is not the published binary. Discard it.gh attestation verifyfails: the binary’s provenance does not match the project’s release workflow. Discard it and escalate.
Related reading
- Zero runtime dependencies — the architectural principle these checks verify.
- Trusted computing base — why the absence of subprocesses and dynamic libraries matters.
- Netlink interop — what the
NETLINKsocket you see inlsofis actually doing.