feat: enforce owner allowlisted bwrap HTTPS egress
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
tegwick 2026-09-05 22:08:18 +02:00
parent d69827aaa2
commit d477c3b5d9
11 changed files with 563 additions and 32 deletions

View file

@ -0,0 +1,70 @@
import importlib.util
import json
import os
import tempfile
from pathlib import Path
from unittest.mock import patch
from sandboxer.core.manager import SandboxManager
from sandboxer.extensions.bwrap import BwrapExtension
from sandboxer.lifecycle.store import SandboxStore
from sandboxer.models import Extension, Profile, SandboxCreateRequest, SandboxExecRequest
spec = importlib.util.spec_from_file_location(
"proof", Path(__file__).with_name("smoke-bwrap-egress.py")
)
proof = importlib.util.module_from_spec(spec)
spec.loader.exec_module(proof)
os.environ["SANDBOXER_NO_STATE_HUB"] = "1"
with tempfile.TemporaryDirectory(prefix="glas-managed-egress-") as directory:
root = Path(directory)
profile = Profile(
id="profile.egress-proof",
version="1",
extension="ext.bwrap",
network={"default": "deny", "egress": ["api.anthropic.com:443"]},
)
ext = Extension(
id="ext.bwrap",
title="proof",
handler="sandboxer.extensions.bwrap:BwrapExtension",
config={"base_dir": str(root / "sandboxes"), "allowed_egress": ["api.anthropic.com:443"]},
)
consumer = {"actor": "agt", "project": "glas-harness", "run_id": "egress-proof"}
with (
patch("sandboxer.core.manager.load_profile", return_value=profile),
patch("sandboxer.core.manager.resolve_extension", return_value=ext),
patch("sandboxer.core.manager.load_extension", return_value=ext),
):
store = SandboxStore(path=root / "sandboxes.json")
mgr = SandboxManager(store=store)
created = mgr.create(
SandboxCreateRequest(profile=profile.id, consumer=consumer), host="localhost"
)
try:
# Different manager and store instances reconstruct the persisted owner handle.
resumed = SandboxManager(store=SandboxStore(path=root / "sandboxes.json"))
result = resumed.execute(
created.sandbox_id,
SandboxExecRequest(
command=["python3", "-c", proof.PROBE], consumer=consumer, timeout_seconds=40
),
)
assert result.exit_code == 0, result.stderr
finally:
SandboxManager(store=SandboxStore(path=root / "sandboxes.json")).destroy(
created.sandbox_id
)
assert not Path(created.inputs["egress_dir"]).exists()
assert not BwrapExtension._pid_alive(int(created.inputs["egress_pid"]))
print(
json.dumps(
{
"sandbox_id": created.sandbox_id,
"manager_restart": True,
"proof": json.loads(result.stdout),
"proxy_removed": True,
"model_call": False,
}
)
)

View file

@ -0,0 +1,89 @@
"""Non-secret HTTPS reachability and denied egress proof."""
import json
import tempfile
from pathlib import Path
from sandboxer.extensions.bwrap import BwrapExtension
from sandboxer.models import Profile
PROBE = r"""
import json, os, socket, ssl, urllib.request, urllib.error
from pathlib import Path
proxy = os.environ['HTTPS_PROXY']
# GET without any credential: TLS and a provider HTTP response suffice.
client = urllib.request.build_opener(urllib.request.ProxyHandler({'https': proxy}))
try:
response = client.open('https://api.anthropic.com/', timeout=20)
status = response.status
response.close()
except urllib.error.HTTPError as error:
status = error.code
assert 100 <= status <= 599
try:
client.open('https://example.com/', timeout=5)
except (urllib.error.URLError, OSError):
denied = True
else:
raise AssertionError('undeclared destination reachable')
try:
socket.create_connection(('1.1.1.1',443),timeout=2)
except OSError:
direct_denied = True
else:
raise AssertionError('direct network reachable')
interfaces = Path('/proc/net/dev').read_text().splitlines()[2:]
assert [line.split(':')[0].strip() for line in interfaces] == ['lo']
print(json.dumps({'provider_http_status':status, 'tls_verified':True,
'undeclared_host_denied':denied, 'direct_network_denied':direct_denied}))
"""
def main():
with tempfile.TemporaryDirectory(prefix="sandboxer-egress-proof-") as directory:
extension = BwrapExtension(
{
"base_dir": directory,
"allowed_egress": ["api.anthropic.com:443"],
"ro_binds": ["/usr", "/bin", "/lib", "/lib64", "/etc/ssl/certs"],
}
)
profile = Profile(
id="profile.egress-proof",
version="1",
extension="ext.bwrap",
network={"default": "deny", "egress": ["api.anthropic.com:443"]},
)
handle = extension.provision(profile, {}, "localhost")
try:
extension.wait_ready(handle)
result = extension.execute(
handle,
["python3", "-c", PROBE],
credential_route_refs=[],
execution_context={},
timeout_seconds=40,
max_output_bytes=4096,
)
finally:
teardown = extension.teardown(handle)
assert result["exit_code"] == 0, result
assert teardown["workspace_removed"] == "True"
assert not Path(handle["egress_dir"]).exists()
assert not extension._pid_alive(int(handle["egress_pid"]))
print(
json.dumps(
{
"sandbox_id": handle["sandbox_id"],
"proof": json.loads(result["stdout"]),
"proxy_removed": True,
"workspace_removed": True,
"model_call": False,
},
indent=2,
)
)
if __name__ == "__main__":
main()