README gets a top-level Updating section; docs/privileges.md gains
a step-by-step trust-model writeup of `banger update`. The new
scripts/publish-banger-release.sh drives the manual release cut:
build, tar, sha256sum, cosign sign-blob, verify against the embedded
public key, jq-merge into manifest.json, rclone upload to the R2
bucket. Refuses outright if the embedded key is still the placeholder
so we can't accidentally publish an unverifiable release. Also folds
in gofmt drift accumulated across the updater package and a few
sibling files.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A pre-release audit caught three places where the docs misrepresent
the trust model. Each is a claim users would read while auditing
banger and reach the wrong conclusion.
* docs/privileges.md:140, 194 — bridge default was documented as
"banger0" but the code default (model.DefaultBridgeName) is
"br-fc". A user following the manual-removal recipe would `ip
link del banger0` against a non-existent interface.
* docs/privileges.md:192 — uninstall recipe said "stop your VMs
first via `banger vm stop --all`". That flag doesn't exist; vm
stop is a per-name action. Replaced with the actual options:
`banger vm prune` (bulk) or per-VM `banger vm stop <name>`.
* docs/privileges.md:255 and README.md:78-79 — helper unit's
CapabilityBoundingSet was listed as 5 caps; the actual set in
commands_system.go:370 is 11 (we added FOWNER/KILL/MKNOD/SETGID/
SETUID/SYS_CHROOT during Phase B and never updated the docs).
Updated both lists; the "what's NOT included" rationale stays
accurate against the new positive list.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
priv.ensure_bridge / priv.create_tap accepted the daemon's network
config triple (BridgeName, BridgeIP, CIDR) and forwarded it straight
to `ip link` / `ip addr` / `ip link set master`. Argv-style exec
ruled out shell injection, but the kernel happily honours those
commands against any iface a compromised owner-uid daemon names —
including eth0/docker0/lo. Concretely:
* priv.ensure_bridge could `ip link set <iface> up` against any
host interface and `ip addr add` arbitrary IP/CIDR to it.
* priv.create_tap could `ip link set <new-tap> master <iface>`,
bridging the per-VM tap into the host's primary LAN so the
guest sees host-local broadcast traffic.
* priv.sync_resolver_routing / priv.clear_resolver_routing only
enforced "name shaped like a Linux iface" — no banger constraint.
New validators (single chokepoint via validateNetworkConfig):
* validateBangerBridgeName: name must equal "br-fc" or start with
"br-fc-". Stops a compromised daemon from naming any host iface
in these RPCs. Users with a custom bridge keep the prefix.
* validateCIDRPrefix: numeric in [8, 32]. Wider prefixes would
silently widen the bridge subnet beyond what the daemon intends.
* validateNetworkConfig bundles bridge-name + validateIPv4 +
validateCIDRPrefix so every helper RPC that takes the triple
stays in lockstep.
Wired into methodEnsureBridge, methodCreateTap, and the resolver-
routing pair (replacing the older validateLinuxIfaceName-only check
with the stricter banger-bridge check).
docs/privileges.md updated: the helper-RPC table rows now spell out
the banger-managed bridge constraint, and the trust list includes
the new validators.
Tests: TestValidateBangerBridgeName (default + suffixed accepted,
host ifaces / wrong prefix / oversized rejected), TestValidate
CIDRPrefix (boundary + non-numeric + IPv6-style 64 rejected),
TestValidateNetworkConfig (happy path + each-field-bad cases).
Smoke at JOBS=4 still green — banger's defaults sail through the
new gate.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The filesystem-mutations table referred to `~/.config/banger/banger.toml`,
but the daemon reads `~/.config/banger/config.toml` (per
internal/config/config.go and README.md). Bring privileges.md in line.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defence-in-depth pass over every helper method that touches the host
as root. Each fix narrows what a compromised owner-uid daemon could
ask the helper to do; many close concrete file-ownership and DoS
primitives that the previous validators didn't reach.
Path / identifier validation:
* priv.fsck_snapshot now requires /dev/mapper/fc-rootfs-* (was
"is the string non-empty"). e2fsck -fy on /dev/sda1 was the
motivating exploit.
* priv.kill_process and priv.signal_process now read
/proc/<pid>/cmdline and require a "firecracker" substring before
sending the signal. Killing arbitrary host PIDs (sshd, init, …)
is no longer a one-RPC primitive.
* priv.read_ext4_file and priv.write_ext4_files now require the
image path to live under StateDir or be /dev/mapper/fc-rootfs-*.
* priv.cleanup_dm_snapshot validates every non-empty Handles field:
DM name fc-rootfs-*, DM device /dev/mapper/fc-rootfs-*, loops
/dev/loopN.
* priv.remove_dm_snapshot accepts only fc-rootfs-* names or
/dev/mapper/fc-rootfs-* paths.
* priv.ensure_nat now requires a parsable IPv4 address and a
banger-prefixed tap.
* priv.sync_resolver_routing and priv.clear_resolver_routing now
require a Linux iface-name-shaped bridge name (1–15 chars, no
whitespace/'/'/':') and, for sync, a parsable resolver address.
Symlink defence:
* priv.ensure_socket_access now validates the socket path is under
RuntimeDir and not a symlink. The fcproc layer's chown/chmod
moves to unix.Open(O_PATH|O_NOFOLLOW) + Fchownat(AT_EMPTY_PATH)
+ Fchmodat via /proc/self/fd, so even a swap of the leaf into a
symlink between validation and the syscall is refused. The
local-priv (non-root) fallback uses `chown -h`.
* priv.cleanup_jailer_chroot rejects symlinks at both the leaf
(os.Lstat) and intermediate path components (filepath.EvalSymlinks
+ clean-equality). The umount sweep was rewritten from shell
`umount --recursive --lazy` to direct unix.Unmount(MNT_DETACH |
UMOUNT_NOFOLLOW) per child mount, deepest-first; the findmnt
guard remains as the rm-rf safety net. Local-priv mode falls
back to `sudo umount --lazy`.
Binary validation:
* validateRootExecutable now opens with O_PATH|O_NOFOLLOW and
Fstats through the resulting fd. Rejects path-level symlinks and
narrows the TOCTOU window between validation and the SDK's exec
to fork+exec time on a healthy host.
Daemon socket:
* The owner daemon now reads SO_PEERCRED on every accepted
connection and refuses any UID that isn't 0 or the registered
owner. Filesystem perms (0600 + ownerUID) already enforced this;
the check is belt-and-braces in case the socket FD is ever
leaked to a non-owner process.
Docs:
* docs/privileges.md walked end-to-end. Each helper RPC's
Validation gate row reflects what the code actually enforces.
New section "Running outside the system install" calls out the
looser dev-mode trust model (NOPASSWD sudoers, helper hardening
bypassed) so users don't deploy that path on shared hosts.
Trust list updated to include every new validator.
Tests added: validators (DM-loop, DM-remove-target, DM-handles,
ext4-image-path, iface-name, IPv4, resolver-addr, not-symlink,
firecracker-PID, root-executable variants), the daemon's authorize
path (non-unix conn rejection + unix conn happy path), the umount2
ordering contract (deepest-first + --lazy on the sudo branch), and
positive/negative cases for the chown-no-follow fallback.
Verified end-to-end via `make smoke JOBS=4` on a KVM host.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Explain what runs as the owner user vs root, every helper RPC method
and its validation gate, the on-disk paths banger writes, network
mutations, and how install/uninstall work end to end. The aim is to
give a reader enough information to grant or refuse the privileges
banger asks for during system install with their eyes open.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>