feat(privops): close three cross-tenant authz holes (1.1.17, security)#219
Merged
Conversation
A self-audit of the 1.1.12 -> 1.1.15 per-tenant gate series found three
verbs whose ownership check was incomplete or absent. In the strict
multi-tenant model (rootless + path_master_prefix) these let one
operator reach another tenant's resources — holes in gates documented
as closed.
1. mount_nullfs SOURCE was unchecked. 1.1.14 gated the mount `target`
(must be inside an owned jail) but never `source`. An operator could
bind-mount ANOTHER tenant's path prefix into their own jail and read
it. Now gated cross-tenant: a source inside
path_master_prefix/<other-uid> -> 403 DenyForeignSource. Host paths
(/etc, ...) and GUI runtime sockets (/tmp/.X11-unix, host
Wayland/PulseAudio sockets) fall outside every tenant prefix and stay
allowed — privops is single-trust-domain w.r.t. the host; the gate
only enforces tenant-vs-tenant.
2. configure_iface sat in the host-global Allow arm despite carrying a
jid and jexec-ing ifconfig INSIDE that jail (+ moving a host iface
into its vnet). Naming a foreign jid was cross-tenant. Now gated by
jid against the jid->owner registry (like signal_jail).
3. reclaim_iface_from_vnet was ungated despite naming a jail
(jail_name) and pulling an iface out of its vnet — a foreign name
could steal/DoS another tenant's networking. Now gated by jail_name
(like destroy_jail).
Mechanism: PerUserEnvPure::Env gains pathMasterPrefix (bare tenant root,
no uid) so authz can tell "another tenant's space" from "a host path";
PrivOpsAuthzPure gains Decision::DenyForeignSource, Request::source, a
sourceForeign() helper, and moves the three verbs to their correct
groups; the dispatcher fills req.source ("source") for mount_nullfs and
req.jailName ("jail_name") for reclaim_iface_from_vnet.
Safety: the gate only DENIES on a confirmed-foreign target; own and
registry-unknown targets always Allow (bootstrap concession), so
legitimate crate-run flows (operating on the operator's own registered
jail) are unaffected regardless of operation ordering. Deployments
without path_master_prefix keep the unchanged opt-in behavior.
Tests: privops_authz_pure_test gains own/host/GUI-socket Allow,
foreign-tenant DenyForeignSource, foreign-target precedence,
unconfigured opt-in, configure_iface + reclaim own/foreign/unknown, and
a host-global-verbs-still-allowed regression; per_user_env_pure_test
asserts pathMasterPrefix composition. Verified via a standalone E2E
harness (kyua/atf unavailable in this env); FreeBSD + Linux CI run the
full ATF suite.
docs/trust-model.{md,uk.md} + CHANGELOG [1.1.17] + --version bumped.
https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
A self-audit of the 1.1.12 → 1.1.15 per-tenant gate series (run as a project-wide bug analysis) found three verbs whose ownership check was incomplete or missing. In the strict multi-tenant model (rootless +
path_master_prefix) these let one operator reach another tenant's resources — holes in gates thatdocs/trust-model.mddocumented as closed.The three holes
mount_nullfstargetbut neversource→ operator bind-mounts another tenant's prefix into their own jail and reads itsourcecross-tenant →403 DenyForeignSourceconfigure_ifaceAllowarm despite carrying ajidandjexec-ingifconfiginside that jail (+ moving a host iface into its vnet)jid(registry), likesignal_jailreclaim_iface_from_vnetjail_name) and pulling an iface out of its vnet → steal/DoS a tenant's networkingjail_name, likedestroy_jailSource-gate semantics (the subtle one)
sourceis denied only when it lies inside another tenant's space (path_master_prefix/<other-uid>/…). Host paths (/etc,/usr, …) and GUI runtime sockets (/tmp/.X11-unix, the host Wayland/PulseAudio sockets thatgui.modebinds) fall outside every tenant prefix and stay allowed — privops is a single trust domain w.r.t. the host; this gate enforces tenant-vs-tenant only. A naive "source must be under my prefix" check would have broken GUI mode.Mechanism
PerUserEnvPure::EnvgainspathMasterPrefix(the bare tenant root, no uid) so the authz layer can tell "another tenant's space" from "a host path".PrivOpsAuthzPure: newDecision::DenyForeignSource,Request::source, asourceForeign()helper, and the three verbs moved to their correct gated groups.req.source(fromsource) formount_nullfsandreq.jailName(fromjail_name) forreclaim_iface_from_vnet.Safety / compatibility
The gate only denies on a confirmed-foreign target; own and registry-unknown targets always Allow (the bootstrap concession). So legitimate
crate runflows — which operate on the operator's own freshly-created (and registered) jail — are unaffected regardless of operation ordering. Deployments that never setpath_master_prefixkeep the unchanged opt-in behavior (source gate off).Tests
privops_authz_pure_test: own/host/GUI-socket Allow, foreign-tenantDenyForeignSource, foreign-target precedence, unconfigured opt-in,configure_iface+reclaim_iface_from_vnetown/foreign/unknown, and a host-global-verbs-still-allowed regression.per_user_env_pure_test:pathMasterPrefixcomposition (full / opt-in-empty / trailing-slash-strip).authorize()(kyua/atf are absent in this re-provisioned env) →1.1.17 AUTHZ CHECKS PASSED; FreeBSD + Linux CI run the full ATF suite. Both ATF test files syntax-checked via a shim header.Test plan
g++ -fsyntax-onlyclean on the pure modules.kyua) + FreeBSD lite (gmake crate) run the real ATF cases.Still by design
Genuinely host-global verbs (
teardown_iface,set_iface_up,bridge_*,add_pf_rule,add_ipfw_rule,configure_ipfw_nat,create_epair) remain ungated — shared host state, no tenant-specific target.https://claude.ai/code/session_01X6t6tzVypHye5bDGLxzmZK
Generated by Claude Code