docs: add the privilege model document
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>
This commit is contained in:
parent
47d83ce4d7
commit
b0b1300314
1 changed files with 250 additions and 0 deletions
250
docs/privileges.md
Normal file
250
docs/privileges.md
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
# Privileges
|
||||
|
||||
This document describes exactly what banger does with the privileges it
|
||||
asks for, what runs where, and how to undo it. The aim is to give a
|
||||
reader enough information to grant — or refuse — the privileges with
|
||||
their eyes open.
|
||||
|
||||
## Two services, two trust boundaries
|
||||
|
||||
`banger system install` lays down two systemd units:
|
||||
|
||||
| Unit | User | Socket | Purpose |
|
||||
|---|---|---|---|
|
||||
| `bangerd.service` | owner user (chosen at install) | `/run/banger/bangerd.sock` (0700, owner) | Orchestration: VM/image lifecycle, store, RPC to the CLI. |
|
||||
| `bangerd-root.service` | `root` | `/run/banger-root/bangerd-root.sock` (0600, root) | Narrow root helper: bridge/tap, DM snapshots, NAT, Firecracker launch. |
|
||||
|
||||
The owner daemon does all the business logic. It never runs as root.
|
||||
The root helper runs as root but only accepts a fixed list of operations
|
||||
and rejects every input that isn't a banger-managed path or name.
|
||||
|
||||
The CLI (`banger ...`) talks to the owner daemon. The owner daemon
|
||||
talks to the root helper for the handful of things only root can do.
|
||||
Users and CI scripts never call the root helper directly.
|
||||
|
||||
### Why two daemons
|
||||
|
||||
Before this split the owner daemon shelled `sudo` for every device or
|
||||
network operation. That meant the user's `sudo` config gated daily
|
||||
work, and an attacker who compromised the owner daemon inherited
|
||||
arbitrary `sudo` reach. After the split, the owner daemon has no
|
||||
ambient root. The only way for it to make a privileged change is to
|
||||
ask the helper, and the helper only honours requests that fit a
|
||||
specific shape.
|
||||
|
||||
## Authentication
|
||||
|
||||
The root helper:
|
||||
|
||||
- Listens on a Unix socket at `/run/banger-root/bangerd-root.sock`,
|
||||
mode 0600, owned by root, in a runtime dir at 0711 root.
|
||||
- Reads `SO_PEERCRED` on every accepted connection and rejects any
|
||||
caller whose UID is not 0 or the owner UID recorded in
|
||||
`/etc/banger/install.toml`. The match is by UID, not username.
|
||||
- Decodes one JSON request per connection and dispatches it through a
|
||||
named-method switch. Unknown methods return `unknown_method`.
|
||||
|
||||
The owner daemon:
|
||||
|
||||
- Listens on `/run/banger/bangerd.sock`, mode 0700, owned by the
|
||||
install-time owner user. Other host users cannot connect.
|
||||
- Resolves the helper socket path from the install metadata and
|
||||
retries with backoff if the helper hasn't started yet.
|
||||
|
||||
There is no network listener. Every banger control surface is a Unix
|
||||
socket on the local host.
|
||||
|
||||
## What the root helper will do, exactly
|
||||
|
||||
The helper exposes 17 RPC methods. Each is shaped so the owner daemon
|
||||
can name a banger-managed object but cannot pass an arbitrary host
|
||||
path or interface name. Code lives in
|
||||
`internal/roothelper/roothelper.go`.
|
||||
|
||||
| Method | Effect | Validation gate |
|
||||
|---|---|---|
|
||||
| `priv.ensure_bridge` | Create the configured Linux bridge if missing; assign the bridge IP. | Bridge name and IP come from owner config; helper does not allow caller to pick `lo` etc. |
|
||||
| `priv.create_tap` | `ip link add tap NAME tuntap` and add to bridge, owned by the owner user. | Tap name must match `tap-fc-*` or `tap-pool-*`. |
|
||||
| `priv.delete_tap` | `ip link del NAME`. | Same prefix check. |
|
||||
| `priv.sync_resolver_routing` | `resolvectl dns/domain/default-route` on the configured bridge. | No-op if `resolvectl` is missing. Bridge name comes from owner config. |
|
||||
| `priv.clear_resolver_routing` | `resolvectl revert` on the bridge. | Same. |
|
||||
| `priv.ensure_nat` | `iptables -t nat MASQUERADE` for `(guest_ip, tap)` plus matching FORWARD rules; `enable=false` removes them. | Tap and IP come from VM record; helper does not run arbitrary iptables. |
|
||||
| `priv.create_dm_snapshot` | Create a `dmsetup` device-mapper snapshot from `rootfs.ext4` with COW backing file. | Both paths must be inside `/var/lib/banger`; DM name must start with `fc-rootfs-`. |
|
||||
| `priv.cleanup_dm_snapshot` | `dmsetup remove` for a snapshot the helper itself just created. | Acts on the typed `dmsnap.Handles` returned by create. |
|
||||
| `priv.remove_dm_snapshot` | `dmsetup remove` by target name. | Name must start with `fc-rootfs-`. |
|
||||
| `priv.fsck_snapshot` | `e2fsck -fy` against the DM device. | Tolerates exit 1 (filesystem cleaned). |
|
||||
| `priv.read_ext4_file` | Read a file from inside an ext4 image via `debugfs cat`. | Path is inside the image; image path is not validated against the state dir today (the helper trusts the daemon for image paths because images can sit anywhere the owner registers). |
|
||||
| `priv.write_ext4_files` | Batch write files into an ext4 image, root:root, mode-controlled. | Same. |
|
||||
| `priv.resolve_firecracker_binary` | Stat and return the firecracker binary path. | Resolved path must be a regular file, executable, root-owned, not group/world-writable. |
|
||||
| `priv.launch_firecracker` | Start the firecracker process for a VM. | Socket and vsock paths must be inside `/run/banger`. Log/metrics/kernel paths must be inside `/var/lib/banger`. Tap name must be banger-prefixed. Drives must be inside the state dir or be a `/dev/mapper/fc-rootfs-*` device. Binary must pass the same root-owned-executable check. |
|
||||
| `priv.ensure_socket_access` | `chown` and `chmod 0660` on a firecracker API or vsock socket so the owner user can talk to it. | Helper does not chown arbitrary paths; this is invoked only after the helper itself just created the socket via firecracker. |
|
||||
| `priv.find_firecracker_pid` / `priv.kill_process` / `priv.signal_process` / `priv.process_running` | Look up a firecracker PID by API socket path; signal or stat the resulting process. | Fixed-shape requests; path validation happens at launch time, and PID lookups are filtered to processes whose cmdline mentions the requested API socket. |
|
||||
|
||||
Anything outside this list returns `unknown_method` and is logged. The
|
||||
helper does not run a shell, does not exec helper scripts, and does
|
||||
not accept commands as strings.
|
||||
|
||||
## Filesystem mutations
|
||||
|
||||
Path used | Owner | What is created or changed
|
||||
---|---|---
|
||||
`/etc/banger/install.toml` | root, 0644 | Written once by `banger system install`. Holds owner UID/GID/home, install timestamp, version. Read by both daemons at startup.
|
||||
`/etc/systemd/system/bangerd.service` | root, 0644 | Owner-daemon unit. Contents are deterministic; see below.
|
||||
`/etc/systemd/system/bangerd-root.service` | root, 0644 | Root-helper unit.
|
||||
`/usr/local/bin/banger` | root, 0755 | Copy of the build output.
|
||||
`/usr/local/bin/bangerd` | root, 0755 | Same binary, second name.
|
||||
`/usr/local/lib/banger/banger-vsock-agent` | root, 0755 | Companion agent injected into guests at image-pull time.
|
||||
`/var/lib/banger/...` | owner (via systemd `StateDirectory=banger`), 0700 | Image artifacts, VM dirs, work disks, kernels, OCI cache, SSH key + known_hosts.
|
||||
`/var/cache/banger/...` | owner, 0700 | Bundle and OCI download cache.
|
||||
`/run/banger/...` | owner, 0700 | Owner daemon socket and per-VM firecracker API + vsock sockets.
|
||||
`/run/banger-root/...` | root, 0711 | Root-helper socket dir; the socket itself is 0600.
|
||||
`~/.config/banger/banger.toml` | owner | Optional user config. Read by the owner daemon at startup.
|
||||
|
||||
Outside these directories, banger does not write to the host filesystem
|
||||
during normal operation. The two exceptions are file-sync (the user
|
||||
explicitly opts in to copying paths from their home into a guest, which
|
||||
the owner daemon validates is inside the owner home before reading)
|
||||
and the install/uninstall actions above.
|
||||
|
||||
### Why the owner home is locked down
|
||||
|
||||
The `[[file_sync]]` config lets users mirror host files into guests.
|
||||
banger refuses to follow paths that escape the owner home, including
|
||||
through symlinks:
|
||||
|
||||
- `ResolveFileSyncHostPath` (`internal/config/config.go`) expands a
|
||||
leading `~/` and rejects any candidate that resolves outside the
|
||||
configured `OwnerHomeDir`.
|
||||
- `ResolveExistingFileSyncHostPath` re-checks after `EvalSymlinks` so
|
||||
a symlink inside `~/.aws` that points at `/etc/shadow` cannot leak
|
||||
out.
|
||||
|
||||
This means an installed banger never reads outside the owner home in
|
||||
the file-sync path, even if the owner edits config to try.
|
||||
|
||||
## Network mutations
|
||||
|
||||
For each running VM banger creates:
|
||||
|
||||
- One bridge (default `banger0`, configurable). Created on first VM
|
||||
start, never deleted automatically.
|
||||
- One tap interface named `tap-fc-<vm_id>`. Created on VM start,
|
||||
deleted on VM stop or crash recovery.
|
||||
- One iptables MASQUERADE rule per VM, only when `--nat` was passed.
|
||||
Removed by the symmetric `EnsureNAT(enable=false)` call at stop.
|
||||
- Optionally, `resolvectl` routing entries that send `*.vm` lookups to
|
||||
banger's in-process DNS server on the bridge. Reverted at stop.
|
||||
|
||||
Banger does not touch UFW, firewalld, or other rule managers. It only
|
||||
edits the iptables tables it created the rules in.
|
||||
|
||||
## Cleanup and uninstall
|
||||
|
||||
Per-VM cleanup happens at:
|
||||
|
||||
- `banger vm stop <name>` — stops firecracker, removes the per-VM tap,
|
||||
drops the NAT rule, removes the DM snapshot, removes per-VM
|
||||
sockets, leaves the work disk.
|
||||
- `banger vm delete <name>` — same as stop, plus deletes the per-VM
|
||||
state directory under `/var/lib/banger/vms/<id>` (work disk,
|
||||
metadata).
|
||||
- `banger vm prune` — bulk version.
|
||||
- Crash recovery: on daemon start, `reconcile` runs the same teardown
|
||||
for any VM whose firecracker process is no longer alive.
|
||||
|
||||
System-level uninstall:
|
||||
|
||||
```
|
||||
sudo banger system uninstall # remove services, units, binaries
|
||||
sudo banger system uninstall --purge # also remove /var/lib/banger,
|
||||
# /var/cache/banger, /run/banger
|
||||
```
|
||||
|
||||
Without `--purge`, the state dirs survive so a reinstall can pick up
|
||||
where the previous one left off. With `--purge`, banger leaves no
|
||||
files behind under `/var/lib`, `/var/cache`, or `/run`.
|
||||
|
||||
What `uninstall` does, in order:
|
||||
|
||||
1. `systemctl disable --now bangerd.service bangerd-root.service`.
|
||||
2. Remove `/etc/systemd/system/bangerd.service` and `bangerd-root.service`.
|
||||
3. Remove `/etc/banger/install.toml` and `/etc/banger/`.
|
||||
4. `systemctl daemon-reload`.
|
||||
5. Remove `/usr/local/bin/banger`, `/usr/local/bin/bangerd`,
|
||||
`/usr/local/lib/banger/`.
|
||||
6. With `--purge` only: remove the system state, cache, and runtime
|
||||
dirs.
|
||||
|
||||
What `uninstall` does NOT do automatically:
|
||||
|
||||
- It does not delete the bridge or any iptables rules. Stop your VMs
|
||||
first (`banger vm stop --all`) so the per-VM teardown drops them.
|
||||
The bridge itself is intentionally persistent — a future reinstall
|
||||
reuses it. To remove it manually: `sudo ip link del banger0`.
|
||||
- It does not undo `resolvectl` routing on a bridge that no longer
|
||||
exists; the entries are harmless if the bridge is gone.
|
||||
- It does not remove the owner user, the owner's home, or anything
|
||||
the user wrote into a guest from inside the guest.
|
||||
|
||||
## Hardening of the systemd units
|
||||
|
||||
The two units ship with restrictive defaults; they are written by
|
||||
banger at install time and the contents are deterministic.
|
||||
|
||||
Owner daemon (`bangerd.service`):
|
||||
|
||||
- `User=` is the install-time owner; never `root`.
|
||||
- `NoNewPrivileges=yes`.
|
||||
- `ProtectSystem=strict` — system directories are read-only.
|
||||
- `ProtectHome=read-only` — owner home is read-only to the daemon
|
||||
unit. The daemon writes only to `StateDirectory`, `CacheDirectory`,
|
||||
`RuntimeDirectory`, plus owner config that the user edits.
|
||||
- `ProtectControlGroups`, `ProtectKernelLogs`, `ProtectKernelModules`,
|
||||
`ProtectClock`, `ProtectHostname`, `RestrictSUIDSGID`,
|
||||
`LockPersonality`.
|
||||
- `RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK AF_VSOCK`.
|
||||
- No `AmbientCapabilities`.
|
||||
|
||||
Root helper (`bangerd-root.service`):
|
||||
|
||||
- Same hardening as above, plus `ProtectHome=yes` (no host-home
|
||||
visibility at all from the helper).
|
||||
- `CapabilityBoundingSet=CAP_CHOWN CAP_DAC_OVERRIDE CAP_NET_ADMIN CAP_NET_RAW CAP_SYS_ADMIN`.
|
||||
Only the capabilities required for tap/bridge, iptables, dmsetup,
|
||||
loop devices, and Firecracker. No `CAP_SYS_BOOT`, no `CAP_SYS_PTRACE`,
|
||||
no `CAP_SYS_MODULE`, no `CAP_NET_BIND_SERVICE`.
|
||||
- `ReadWritePaths=/var/lib/banger`.
|
||||
|
||||
## What this leaves you trusting
|
||||
|
||||
If you install banger as root, you are trusting:
|
||||
|
||||
1. The two binaries banger drops under `/usr/local/bin` and the
|
||||
companion agent under `/usr/local/lib/banger`. These should match
|
||||
the build artifacts you reviewed.
|
||||
2. The path validators in
|
||||
`internal/roothelper/roothelper.go:validateManagedPath`,
|
||||
`validateTapName`, `validateDMName`, and `validateRootExecutable`
|
||||
to be tight. If those are bypassed, the helper would carry out a
|
||||
privileged op against an unmanaged path. They are unit-tested in
|
||||
`internal/roothelper/roothelper_test.go`.
|
||||
3. The Firecracker binary banger executes. The helper refuses to launch
|
||||
anything that isn't a regular, executable, root-owned, not
|
||||
world-writable file — but the binary's own behaviour is your
|
||||
responsibility.
|
||||
4. Your own owner-user account. The owner can ask the helper to
|
||||
create taps, run firecracker, and edit ext4 images under
|
||||
`/var/lib/banger`. Anyone with the owner's UID can do those
|
||||
things; treat that account as semi-privileged.
|
||||
|
||||
What you do **not** have to trust:
|
||||
|
||||
- The CLI process. It only talks Unix-socket RPC.
|
||||
- Other host users. The helper socket is 0600 root and the owner
|
||||
socket is 0700 owner.
|
||||
- The contents of the user's home, except the file paths that
|
||||
`[[file_sync]]` explicitly names — and even those are clamped to
|
||||
the owner home.
|
||||
- The guest. Guests cannot reach the helper or the owner daemon; the
|
||||
only host endpoint a guest sees is the in-process DNS server on the
|
||||
bridge IP and the bridge itself for outbound NAT.
|
||||
Loading…
Add table
Add a link
Reference in a new issue