Not a bug report — a design question, raised because we hit the wall it
describes and the answer shapes what we build next.
The current position, and it is a defensible one
Managoat.Runner.Adapter:
:network_policy is not [advertised] — the machine is the user's and
so is its network; apply_network_policy/2 refuses rather than
pretending.
def apply_network_policy(%Handle{}, %NetworkPolicy{}), do: {:error, :not_supported}
It goes deeper than the adapter: fountain runner's Backend interface
has no network-policy method, so the wire protocol has nothing to carry one
even if the adapter wanted to. Refusing rather than pretending is clearly
right given that — a silent no-op here would be the worst available
outcome, since a caller would believe a box was sealed when it was not.
Where it bites
We're building managoat/airlock: a
policy names the hosts a job may reach, managoat_broker attaches the
credentials, and the record is the product. The containment story is two
layers — the sandbox's own default-deny egress, and the proxy's rules —
and the first layer is what forces every request through the chokepoint
that logs it. Without it, anything that drops the proxy environment (sudo
stripping it, npm 9) egresses unrecorded.
Airlock picked the local runner precisely because the broker has to be
reachable from the box and a cloud sandbox cannot dial a laptop. So the box
we chose for reachability is the one box that cannot have the policy
applied — Sprites, E2B and Daytona all advertise :network_policy; the
runner is the only adapter that does not.
We're resolving it by moving to Sprites with a tunnel, which is a fine
answer for us and needs nothing from this library. The question is whether
it should stay the answer.
The question
Should the daemon protocol ever carry a seal, or is "the machine is the
user's" the permanent answer?
Arguments we can see for keeping it as-is:
- a runner's machine may be someone's laptop, with a VPN, a corporate
proxy and a firewall the daemon has no business touching;
- egress filtering on the host is genuinely per-platform (pf, nftables,
WFP) in a way create/exec/spawn are not, and a backend that
implements it badly is worse than one that refuses;
- the Firecracker backend is a different story from the process backend,
and one capability atom would have to answer for both.
And for revisiting it:
- the microVM backend already owns a network namespace
(firecracker_net.go), so for that backend the policy is not the
user's machine's — it is the VM's, and refusing looks like an accident of
where the seam was drawn rather than a decision about it;
capabilities/0 is per-adapter, not per-backend, so a runner whose
Firecracker backend could seal cannot say so.
If the answer is "the process backend never, the microVM backend maybe",
that is already useful to know — it would tell us the local path is
permanently one-layer and that the seal belongs with the cloud providers,
which is a thing we'd write down rather than keep rediscovering.
No action needed on our account. Closing this with a paragraph of reasoning
would be a good outcome; it is the kind of thing that belongs in the
adapter's moduledoc next to the sentence that is already there.
Found in
managoat_runner 0.2.1, building managoat/airlock.
Not a bug report — a design question, raised because we hit the wall it
describes and the answer shapes what we build next.
The current position, and it is a defensible one
Managoat.Runner.Adapter:It goes deeper than the adapter:
fountain runner'sBackendinterfacehas no network-policy method, so the wire protocol has nothing to carry one
even if the adapter wanted to. Refusing rather than pretending is clearly
right given that — a silent no-op here would be the worst available
outcome, since a caller would believe a box was sealed when it was not.
Where it bites
We're building
managoat/airlock: apolicy names the hosts a job may reach,
managoat_brokerattaches thecredentials, and the record is the product. The containment story is two
layers — the sandbox's own default-deny egress, and the proxy's rules —
and the first layer is what forces every request through the chokepoint
that logs it. Without it, anything that drops the proxy environment (
sudostripping it, npm 9) egresses unrecorded.
Airlock picked the local runner precisely because the broker has to be
reachable from the box and a cloud sandbox cannot dial a laptop. So the box
we chose for reachability is the one box that cannot have the policy
applied — Sprites, E2B and Daytona all advertise
:network_policy; therunner is the only adapter that does not.
We're resolving it by moving to Sprites with a tunnel, which is a fine
answer for us and needs nothing from this library. The question is whether
it should stay the answer.
The question
Should the daemon protocol ever carry a seal, or is "the machine is the
user's" the permanent answer?
Arguments we can see for keeping it as-is:
proxy and a firewall the daemon has no business touching;
WFP) in a way
create/exec/spawnare not, and a backend thatimplements it badly is worse than one that refuses;
and one capability atom would have to answer for both.
And for revisiting it:
(
firecracker_net.go), so for that backend the policy is not theuser's machine's — it is the VM's, and refusing looks like an accident of
where the seam was drawn rather than a decision about it;
capabilities/0is per-adapter, not per-backend, so a runner whoseFirecracker backend could seal cannot say so.
If the answer is "the process backend never, the microVM backend maybe",
that is already useful to know — it would tell us the local path is
permanently one-layer and that the seal belongs with the cloud providers,
which is a thing we'd write down rather than keep rediscovering.
No action needed on our account. Closing this with a paragraph of reasoning
would be a good outcome; it is the kind of thing that belongs in the
adapter's moduledoc next to the sentence that is already there.
Found in
managoat_runner 0.2.1, building
managoat/airlock.