block / block/buzz

Sandboxing buzz-acp workers with systemd `--user`: the filesystem sandbox silently no-ops on Ubuntu 24.04+ (`apparmor_restrict_unprivileged_userns=1`)

Open
#5,670 0 comments 0 reactions 0 assignees View on GitHub
Dominant language
Rust
Stars
32.7k
Forks
4.3k
Avg merge
1d 13h
Merged PRs (30d)
253

Description

## Summary

We run each `buzz-acp` worker as a `systemd --user` template unit with a filesystem sandbox (`ProtectSystem=strict`, `ProtectHome=read-only`, explicit `ReadWritePaths=`). On Ubuntu 24.04+ that sandbox **silently does not apply** — the unit starts normally, reports `active (running)`, and has full read/write access to `$HOME`, including any secrets directory.

No error, no warning, no journal entry from systemd. The only signal is a kernel audit record, which you have to know to go looking for.

This isn't a buzz bug — it's a systemd/AppArmor interaction. We're reporting it because **`buzz-acp` defaults to `bypass-permissions`** (`crates/buzz-acp/src/config.rs`, `BUZZ_ACP_PERMISSION_MODE`), which is documented and deliberate, but it makes the OS-level sandbox the *actual* trust boundary for a fleet of workers. A boundary that fails open and silent is worth a line in the deployment docs.

## Environment

| | |
|---|---|
| OS | Ubuntu 24.04.4 LTS |
| Kernel | 7.0.0-28-generic |
| systemd | 255 (255.4-1ubuntu8.17) |
| `kernel.apparmor_restrict_unprivileged_userns` | `1` (Ubuntu default since 23.10) |
| buzz | v0.5.2 |

## What happens

To apply `ProtectSystem`/`ProtectHome`/`ReadWritePaths`, the user-session `systemd-executor` creates a user namespace and needs `cap_sys_admin` to set up the mounts. Ubuntu's unprivileged-userns restriction transitions that namespace into the capability-stripped `unprivileged_userns` AppArmor profile, and the capability is denied:

```
apparmor="DENIED" operation="capable" class="cap"
profile="unprivileged_userns" ... capability=21 capname="sys_admin"
```

systemd 255 then **degrades silently** rather than failing the unit: the service runs in the host mount namespace with no sandbox at all. We confirmed the degraded state by spawning transient units carrying the exact same properties and probing the boundary from inside — the process could write to paths that `ProtectHome=read-only` and the `ReadWritePaths=` allowlist should have made unreachable.

## Reproduction

```bash
# On Ubuntu 24.04+ with kernel.apparmor_restrict_unprivileged_userns=1
systemd-run --user --pty \
-p ProtectSystem=strict -p ProtectHome=read-only -p PrivateTmp=yes \
/bin/sh -c 'touch "$HOME/sandbox-canary" && echo "SANDBOX DEGRADED: wrote to \$HOME" || echo "sandbox OK"'

# Then check the kernel log for the denial:
sudo dmesg | grep -i 'unprivileged_userns.*sys_admin' | tail -1
```

Expected on a working sandbox: the write fails. Observed on this host: the write succeeds and the unit reports success.

## Fix (host prerequisite)

Ubuntu's documented mechanism for legitimate userns users — the same pattern as the stock Chrome/bwrap carve-outs. It grants **only** `systemd-executor` the right to create user namespaces, while leaving it otherwise unconfined; nothing is loosened for any other binary.

`/etc/apparmor.d/systemd-executor-userns`:

```
abi ,
include

profile systemd-executor-userns /usr/lib/systemd/systemd-executor flags=(unconfined) {
userns,

# Site-specific additions and overrides. See local/README for details.
include if exists
}
```

```bash
sudo install -m 0644 systemd-executor-userns.apparmor /etc/apparmor.d/systemd-executor-userns
sudo apparmor_parser -r /etc/apparmor.d/systemd-executor-userns
systemctl --user restart
```

## Detecting the degraded state

Two things made this manageable for us, both of which might be worth suggesting to anyone sandboxing workers:

**1. A fail-closed canary in the unit.** Probe the most dangerous path from inside and refuse to start if it's writable, so a degraded sandbox never runs at all:

```ini
ExecStartPre=/bin/sh -c 'if touch "%h//.sandbox-canary" 2>/dev/null; then \
rm -f "%h//.sandbox-canary"; \
echo "sandbox DEGRADED (secrets writable)" >&2; exit 1; fi'
```

**2. A verification script** that spawns transient units carrying the properties *parsed out of the real unit file* (so the check cannot drift from what's deployed) and probes the boundary from inside, printing PASS/FAIL per property.

## Suggested action

Happy to contribute whichever is useful:

- a short note in the deployment/hardening docs: if you sandbox `buzz-acp` workers with systemd on Ubuntu 24.04+, install the carve-out and **verify** the sandbox rather than assuming it applied;
- the AppArmor profile + the canary snippet as a reference example;
- the verification script, generalized.

We have all three running in a 9-worker fleet and can open a PR (with DCO sign-off) against whichever location you'd prefer — `docs/`, an example directory, or nothing at all if you'd rather just have the docs line.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.