security: agent signing keys can only be supplied via BUZZ_PRIVATE_KEY (no file, keyring or systemd-credential source)
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
## Summary
`BUZZ_PRIVATE_KEY` is the **only** way to give an agent identity its signing key. Both entry points take it as a clap arg with an env fallback and nothing else:
- `crates/buzz-cli/src/lib.rs:86` — `#[arg(long, env = "BUZZ_PRIVATE_KEY", hide_env_values = true)] private_key: Option`
- `crates/buzz-acp/src/config.rs:243` — `#[arg(long, env = "BUZZ_PRIVATE_KEY", hide_env_values = true)] pub private_key: String`
A search of the tree finds no file-based, keyring-based or systemd-credential source: no `BUZZ_PRIVATE_KEY_FILE`, no `CREDENTIALS_DIRECTORY` / `LoadCredential` handling anywhere in the repo.
This is complementary to #2883, not a duplicate: that issue is about not handing the raw key to *model-controlled* processes (MCP config, shell children). This one is about how the harness itself receives the key in the first place — and its "additional threat-model note" explicitly leaves this to a separate issue.
## Why it matters on a multi-agent host
The natural way to run several agent identities on one VPS is a `.env` per agent, exported into each unit or shell. That makes the key a plain string in a shared namespace, with two consequences:
- **Any process that can read another agent's `.env` can sign as that agent.** Nothing in the protocol can distinguish it: the signature is valid, and a NIP-OA auth tag attests *who owns* an agent, not *which* agent is speaking. We hit exactly this — a session on our host published relay events signed as a different agent because that agent's key was still exported in the environment it inherited. Recovering that it happened at all required reading the events afterwards.
- **`--private-key` is worse than the env var and looks equivalent.** As a flag the key lands in `/proc//cmdline`, world-readable to every user on the box; `hide_env_values = true` only affects `--help` output. Nothing warns about the difference at the point of use.
Neither of these is exotic — one host, several agents, one operator is the shape of every self-hosted deployment we've seen discussed in the issue tracker.
## Proposal (small, additive)
1. Accept `BUZZ_PRIVATE_KEY_FILE=/path/to/key` (hex or `nsec`, trailing newline trimmed) in both `buzz-cli` and `buzz-acp`, taking precedence over `BUZZ_PRIVATE_KEY`. The file can then be `0600 root:agent-x`, so OS permissions — not shell hygiene — decide who can sign.
2. Optionally honor systemd's `CREDENTIALS_DIRECTORY` (`LoadCredential=buzz-key:/etc/buzz/agent-x.key`), which is the idiomatic form of the same thing for the unit-file deployments the docs already recommend.
3. Keep `BUZZ_PRIVATE_KEY` working unchanged as the fallback; note in `--help` that `--private-key` exposes the key through `ps`.
Precedence would be: `--private-key` → `BUZZ_PRIVATE_KEY_FILE` → `BUZZ_PRIVATE_KEY` → credential directory, or whatever order maintainers prefer — the ordering is the only real design question here.
## Acceptance criteria
- Starting `buzz-acp` and `buzz` with only `BUZZ_PRIVATE_KEY_FILE` set authenticates and publishes normally.
- A malformed/unreadable file fails at startup with a clear error and never falls through to an unauthenticated session.
- The key never appears in the process's own argv when supplied by file.
- Tests cover hex and `nsec` file contents, trailing whitespace, and the precedence order.
Happy to send the PR if maintainers confirm the shape and the precedence order they want.
Contributor guide
Assessment
This issue has not been assessed yet.