feat: enforce owner allowlisted bwrap HTTPS egress
Assistant: codex Assistant-Model: gpt-6-astra Assistant-Session: 01a0726e-5232-73f2-aaca-2c05ceb62efb
This commit is contained in:
parent
d69827aaa2
commit
d477c3b5d9
11 changed files with 563 additions and 32 deletions
70
scripts/smoke-bwrap-egress-manager.py
Normal file
70
scripts/smoke-bwrap-egress-manager.py
Normal 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,
|
||||
}
|
||||
)
|
||||
)
|
||||
89
scripts/smoke-bwrap-egress.py
Normal file
89
scripts/smoke-bwrap-egress.py
Normal 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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue