-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA512
Marek,
Following up on the write-access point I parked last time.
The use case that forces it: I run untrusted third-party MCP servers inside
ai-managed qubes to reproduce target environments and run vulnerability PoCs.
The agent driving that can be prompt-injected by the very server it's testing,
so a hallucinating or injected agent destroying data inside the boundary isn't
hypothetical for me - it's the normal operating condition. It works today, but
the boundary is still binary, which is the thing your reply pushed me to fix.
The binary part is the problem. ai-managed currently means full read + root
exec + lifecycle + remove + device + copy; untagged is invisible. Isolating
the untagged set is necessary but not sufficient - an injected agent still has
lifecycle over everything it can see and can pivot out of the qube it was
supposed to stay in. So the fix has to bind somewhere the agent can't reach,
which means dom0.
What I built and tested since: the single ai-managed tag becomes a cumulative
ladder - read floor < exec < net < full - plus an orthogonal copy-in-only sink
(ai-dump: no read, no exec, you can only push data into it). It's your
tag-scoping idiom, just with more rungs. The qrexec-expressible surfaces stay
pure @tag: policy and only change which tag they match; the @adminvm wrappers
read the tier in dom0 and fail closed - a missing resolver denies, it never
grants. The AI can't mutate tags (admin.vm.tag.Set/Remove are denied) and
can't read the tier tags either: a tags read returns only ai-managed, so it
finds its ceiling through refusals, not introspection. That last part needed a
fix I didn't expect - qubesd emits tag events as domain-tag-add:ai-full, so the
tier rode out in the event name and leaked the fleet's authority map the moment
a qube was tiered. The event stream now parses the suffix off and redacts it.
Create paths also strip any tier tag the platform propagates, or you could
clone an ai-full qube into a fresh one and self-escalate.
One ordering choice I'd flag because it's non-obvious: net sits above exec, not
beside it. In an egress-controlled sandbox, rewriting a qube's firewall (open
an exfil path) is higher-trust than running code confined inside one qube - and
in-qube root can't widen the envelope the NetVM enforces anyway, so exec
genuinely is the smaller grant. If you think that's backwards I'd like to hear
why.
This all ships behaviour-neutral: untiered = full, today's boundary, until the
operator tiers the fleet and flips one dom0 flag to least-privilege. The
running instance is still in that compat default - tested, not yet flipped -
which is the honest state, not a deployed least-privilege fleet.
On constraining property values - you noted qrexec can't do it and named two
mechanisms, a wrapper service or a qubesd admin-permission event handler. I'm
keeping wrappers for that (the netvm-only-to-X case) and setting the event
handler aside, with reasons to shoot at: it's a Python package running inside
qubesd - so it lives in the dom0 TCB, which cuts against the dom0-minimalism
this whole thing sells, and it's a second artifact in a different trust domain
from the rest of qubes-mcp, harder to reverse and audit than an
operator-installed wrapper plus a policy line. For something meant to be
third-party, auditable and reversible I think that favours wrappers - until a
parallel qmcp.* namespace gets big enough to flip the trade. The drift risk
runs the other way too (wrappers shelling to surfaces that move under them); if
you think that's the bigger exposure, say so.
Two things I'm less sure about and would value your eye on.
First, identity vs capability. qrexec dispatches on source identity + tags, so
an injection turns a legitimate agent into a confused deputy without its
identity or tags changing - the policy can't see it. But capabilities don't fix
that either: the injected agent holds the same caps it legitimately needs, so it
rides its own authority. What a capability model (or a dom0 nonce broker) buys
is tighter per-call scoping and a place to force out-of-band approval on the
destructive subset - not deputy-immunity. So I lean on least-authority plus
per-call confirmation on the destructive calls (the ask verb, rendered outside
the calling qube's control); within a tier an injected agent still acts freely,
blast radius bounded by the tier, not eliminated. The question I'm stuck on: for
a qrexec-native system, is per-call dom0 approval on the destructive subset
where the complexity should go, or is there a Qubes idiom I'm missing?
Second, your copy-scope question. You asked whether I really meant any copy
between any two ai-managed qubes, or just to/from mcp-control. It's any-to-any
on purpose: I treat the ai-managed set as one trust domain and gate
exfiltration at the single egress qube (the one you landed on in Q4), not at
the copy layer - so intra-sandbox copy doesn't widen the exfil surface. But it's
the loosest line in the file and does nothing against lateral movement once an
agent is injected, so I'm not attached to it. If you'd narrow it to
mcp-control-only and route the rest through ask, I'd take that.
The reviewer-competence point you raised is the one I keep coming back to. A
reviewable-plan gate (I'm leaning toward the Salt/qubesctl stack as executor,
not Ansible - heavy YAML makes the review harder, not easier) only protects an
operator who can actually review the plan. So I'm treating the plan text as a
plain-language catastrophe-catcher and the tier ceiling as the real wall: a
rubber-stamped approval still can't exceed what the tier permits. The
action-gate and the per-agent principal split are designed but not built yet.
The wrapper-vs-extension call and the identity-vs-capability line are the two
I'd most like you to push on.
Thanks,
Alex
F4F9 735B E899 60F4 70E6 ADBE 5312 A67E E8CE 816B
-----BEGIN PGP SIGNATURE-----
iNUEARYKAH0WIQT0+XNb6Jlg9HDmrb5TEqZ+6M6BawUCaj2NHl8UgAAAAAAuAChp
c3N1ZXItZnByQG5vdGF0aW9ucy5vcGVucGdwLmZpZnRoaG9yc2VtYW4ubmV0RjRG
OTczNUJFODk5NjBGNDcwRTZBREJFNTMxMkE2N0VFOENFODE2QgAKCRBTEqZ+6M6B
a5tQAP0cEvDF/DHbpA37LlMnJ7nxZvep8Hcp01aQFbn5DaClOwEAy8I3zdD6eFmk
4LGzk1+LwtAC3j9i0Rb74N6u3nIwAAM=
=8wer
-----END PGP SIGNATURE-----