roothelper: pin bridge name + IP + CIDR to a banger-managed shape
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>
This commit is contained in:
parent
4004ce2e7e
commit
182bccf8af
3 changed files with 196 additions and 15 deletions
|
|
@ -71,11 +71,11 @@ validator before the helper touches the host.
|
|||
|
||||
| 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. | Bridge name passes the kernel iface-name rules (1–15 chars, no `/`/`:`/whitespace, not `.`/`..`). Resolver address must parse via `net.ParseIP`. |
|
||||
| `priv.clear_resolver_routing` | `resolvectl revert` on the bridge. | Same iface-name check. |
|
||||
| `priv.ensure_bridge` | Create the configured Linux bridge if missing; assign the bridge IP. | Bridge name must equal `br-fc` or start with `br-fc-` (so a compromised daemon can't drive `ip link` against `eth0` / `docker0` / `lo`). Bridge IP must parse as IPv4. CIDR prefix must be a number in `[8, 32]`. |
|
||||
| `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-*`. Bridge config (name + IP + CIDR) passes the same banger-managed check as `priv.ensure_bridge`, otherwise the new tap could be `master`-attached to an arbitrary host iface. |
|
||||
| `priv.delete_tap` | `ip link del NAME`. | Same prefix check on the tap name. |
|
||||
| `priv.sync_resolver_routing` | `resolvectl dns/domain/default-route` on the configured bridge. | Bridge name must equal `br-fc` or start with `br-fc-` (same banger-managed check). Resolver address must parse via `net.ParseIP`. |
|
||||
| `priv.clear_resolver_routing` | `resolvectl revert` on the bridge. | Same banger-managed bridge-name check. |
|
||||
| `priv.ensure_nat` | `iptables -t nat MASQUERADE` for `(guest_ip, tap)` plus matching FORWARD rules; `enable=false` removes them. | Tap must be banger-prefixed. Guest IP must parse as IPv4. |
|
||||
| `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` and `losetup -d` for a snapshot the helper itself just created. | Every non-empty `dmsnap.Handles` field is checked: DM name `fc-rootfs-*`, DM device `/dev/mapper/fc-rootfs-*`, loops `/dev/loopN`. |
|
||||
|
|
@ -271,11 +271,12 @@ If you install banger as root, you are trusting:
|
|||
`validateLoopDevicePath`, `validateDMRemoveTarget`,
|
||||
`validateDMSnapshotHandles`, `validateRootExecutable`,
|
||||
`validateNotSymlink`, `validateExt4ImagePath`,
|
||||
`validateLinuxIfaceName`, `validateIPv4`, `validateResolverAddr`,
|
||||
and `validateFirecrackerPID`. If any of these are bypassed, the
|
||||
helper would carry out a privileged op against an unmanaged
|
||||
target. They are unit-tested in
|
||||
`internal/roothelper/roothelper_test.go`.
|
||||
`validateLinuxIfaceName`, `validateBangerBridgeName`,
|
||||
`validateNetworkConfig`, `validateCIDRPrefix`, `validateIPv4`,
|
||||
`validateResolverAddr`, `validateSignalName`, and
|
||||
`validateFirecrackerPID`. If any of these are bypassed, the helper
|
||||
would carry out a privileged op against an unmanaged target. 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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue