Skip to content

feat(privops): authorize-before-dispatch for per-user-ownable verbs#208

Merged
click0 merged 1 commit into
mainfrom
claude/analyze-test-coverage-nCOJW
May 25, 2026
Merged

feat(privops): authorize-before-dispatch for per-user-ownable verbs#208
click0 merged 1 commit into
mainfrom
claude/analyze-test-coverage-nCOJW

Conversation

@click0
Copy link
Copy Markdown
Owner

@click0 click0 commented May 24, 2026

Follow-up to #207 (trust-model docs, already merged). Closes the cleanly-ownable part of the privops isolation gap that doc identified.

What

The privops libnv transport carries a real peer uid (getpeereid) but ran verbs purely on request arguments — the uid only fed the audit tail. A hostile operator in the privops group could name another operator's ZFS dataset or RCTL umbrella.

New pure module lib/privops_authz_pure.{h,cpp}: an authorize-before-dispatch decision, checked against the caller's PerUserEnvPure::composeForUid env, for the verbs that carry a robust, request-borne ownership signal:

  • attach_zfs / detach_zfsdataset must lie within the caller's per-user ZFS prefix <master>/<uid>;
  • set_loginclass_rctl / clear_loginclass_rctlloginclass must be the caller's crate-<uid>.

A foreign target is denied 403 before the handler runs (fail closed). Wired into dispatchPrivOpFromMap only; the HTTP/admin path (uid==0) is unchanged and host-wide by design. Per-user config registered at startup via setPerUserAuthzConfig (mirrors the existing setUmbrellaConfig pattern). The trust-model docs are updated to mark this part of the gap closed.

Deliberately deferred

  • host-global verbs (iface / pf / ipfw / nat / epair) — cannot be pool-scoped; host-wide by design.
  • jid-scoped verbs (set_rctl, signal_jail, create_jail, set_jail_cpuset, devfs, query_jail_rctl) — no request-borne owner; need a jid→owner registry (record operator uid at create_jail, check on each jid-keyed verb). Natural follow-up PR if wanted.

Verification

  • make test-unit (kyua + libatf) locally on Linux: 1329/1329 passed, incl. 12 new privops_authz_pure_test cases (prefix-ownership boundaries incl. slash-anchoring, loginclass match, fail-closed empties, ungated verb classes).
  • Daemon wiring (privops_handlers.cpp, main.cpp) compiles on FreeBSD only (libnv); uses portable facilities + the existing startup-global pattern. Please confirm FreeBSD CI.

Heads-up for reviewers

  • Should the HTTP privops path stay host-wide admin (it does here), or eventually carry pool/uid too?
  • Want the jid→owner registry as the next PR to gate the jid-scoped verbs?

https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK


Generated by Claude Code

The privops libnv transport carries a real peer uid (getpeereid) but
ran verbs purely on request arguments — the uid only fed the audit
tail. A hostile operator in the privops group could name another
operator's ZFS dataset or RCTL umbrella (see docs/trust-model.md).

Adds lib/privops_authz_pure.{h,cpp}: a pure authorize-before-dispatch
decision for the verbs that carry a robust, request-borne ownership
signal, checked against the caller's PerUserEnvPure::composeForUid env:

  - attach_zfs / detach_zfs : dataset must lie within the caller's
    per-user ZFS prefix <master>/<uid>;
  - set_loginclass_rctl / clear_loginclass_rctl : loginclass must be
    the caller's crate-<uid> umbrella.

A foreign target is denied 403 before the handler runs (fail closed).
Wired into dispatchPrivOpFromMap (libnv only); the HTTP/admin path
(uid==0) is unchanged and stays host-wide by design. Host-global verbs
(iface/pf/ipfw/nat/epair) cannot be pool-scoped and pass through;
jid-scoped verbs (set_rctl, signal_jail, create_jail, ...) carry no
request-borne owner and are deferred pending a jid->owner registry —
documented in trust-model.{md,uk.md} as the remaining gap.

Per-user config registered at startup via setPerUserAuthzConfig
(mirrors the setUmbrellaConfig pattern). New unit test
privops_authz_pure_test (12 cases) covers prefix ownership boundaries,
loginclass match, fail-closed empties, and the ungated verb classes.

https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
@click0 click0 merged commit 7aa6fab into main May 25, 2026
2 checks passed
@click0 click0 deleted the claude/analyze-test-coverage-nCOJW branch May 25, 2026 06:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants