Summary
Since #81 ("a missing policy on either side denies, never inherits"), pool.effectivePolicy returns no evaluator unless a pool policy exists for the claim's key. A claim of a promoted template has no pool by construction, so armEgressProxy never binds the vsock proxy and the guest's 127.0.0.1:3128 relay is default-denied — regardless of the tenant's egress block.
There is no configuration that restores it: a promoted template cannot be declared as a pool (Promote/DeleteTemplate refuse keys a configured pool owns via pooledHash, and the refill loop would buildGolden a template name that is not a cocoon image forever), and docs/sdk.md states there is no warm pool for promoted templates.
Net effect: on v0.1.6 every promoted-template claim is offline, even for a tenant whose policy allows egress. That removes the only route a none-lane guest has to reach an operator-allowed host.
Reproduction (v0.1.6 = 3d70391, node with tenants but no pools)
Config: no pools; tenant bake with egress: {allow: [{host: "*"}]}; tenant instar with a single allowed host.
- Claim
ghcr.io/cocoonstack/sandbox/rt:24.04 (none lane, large) as tenant bake, run apt-get update with http_proxy=http://127.0.0.1:3128 in the guest → Connection failed [IP: 127.0.0.1 3128] (45/45 attempts).
- Same node, same config, sandboxd 218eee1 → the relay works and the same commands succeed (45/45).
Read of the code path at 3d70391: pool/egress.go effectivePolicy → if poolPol == nil { return nil, false }; armEgressProxy → if !ok { return nil }; the guest-side silkd net_egress.rs then sees the per-connection vsock dial refused ("default-deny").
Why this matters
docs/egress.md documents the policy as pool ∩ tenant and says a tenant "must now declare one" after upgrading — but for promoted templates the pool side can never be declared, so the intersection is empty by construction rather than by operator choice. Instar's whole guest→LLM path (per docs/deploy.md there: the tenant egress allow-list names the proxy/gateway host) depends on tenant-scoped egress for promoted-template claims. #81's security goal (no single-sided inheritance of a pool's secrets into a tenant) does not require this: a promoted-template claim has no pool secrets to inherit.
Suggested direction (for discussion)
Keep #81's intersection semantics for pooled keys, and let a promoted-template claim (no configured pool for its key) take the tenant policy alone — the tenant layer is then the only layer that exists, exactly like root takes the pool layer alone today. Root claims of promoted templates would remain default-deny unless a policy is attached some other way (or an explicit node-level default is added).
Filed from an instar testbed window; the testbed stays on 218eee1 until this is settled.
Summary
Since #81 ("a missing policy on either side denies, never inherits"),
pool.effectivePolicyreturns no evaluator unless a pool policy exists for the claim's key. A claim of a promoted template has no pool by construction, soarmEgressProxynever binds the vsock proxy and the guest's127.0.0.1:3128relay is default-denied — regardless of the tenant'segressblock.There is no configuration that restores it: a promoted template cannot be declared as a pool (
Promote/DeleteTemplaterefuse keys a configured pool owns viapooledHash, and the refill loop wouldbuildGoldena template name that is not a cocoon image forever), anddocs/sdk.mdstates there is no warm pool for promoted templates.Net effect: on v0.1.6 every promoted-template claim is offline, even for a tenant whose policy allows egress. That removes the only route a none-lane guest has to reach an operator-allowed host.
Reproduction (v0.1.6 = 3d70391, node with tenants but no
pools)Config: no
pools; tenantbakewithegress: {allow: [{host: "*"}]}; tenantinstarwith a single allowed host.ghcr.io/cocoonstack/sandbox/rt:24.04(none lane, large) as tenantbake, runapt-get updatewithhttp_proxy=http://127.0.0.1:3128in the guest →Connection failed [IP: 127.0.0.1 3128](45/45 attempts).Read of the code path at 3d70391:
pool/egress.goeffectivePolicy→if poolPol == nil { return nil, false };armEgressProxy→if !ok { return nil }; the guest-sidesilkdnet_egress.rsthen sees the per-connection vsock dial refused ("default-deny").Why this matters
docs/egress.mddocuments the policy aspool ∩ tenantand says a tenant "must now declare one" after upgrading — but for promoted templates the pool side can never be declared, so the intersection is empty by construction rather than by operator choice. Instar's whole guest→LLM path (perdocs/deploy.mdthere: the tenant egress allow-list names the proxy/gateway host) depends on tenant-scoped egress for promoted-template claims. #81's security goal (no single-sided inheritance of a pool's secrets into a tenant) does not require this: a promoted-template claim has no pool secrets to inherit.Suggested direction (for discussion)
Keep #81's intersection semantics for pooled keys, and let a promoted-template claim (no configured pool for its key) take the tenant policy alone — the tenant layer is then the only layer that exists, exactly like root takes the pool layer alone today. Root claims of promoted templates would remain default-deny unless a policy is attached some other way (or an explicit node-level default is added).
Filed from an instar testbed window; the testbed stays on 218eee1 until this is settled.