3.4

sbx policy log and the proxy

Every connection leaves through a host proxy, which answers a blocked HTTPS request with its own certificate and writes one policy log row per host with the matching rule.

The agent says that https://example.com timed out. It did not time out: the proxy answered it in 90 bytes. Every connection from a sandbox passes a proxy on the host. sbx policy log keeps one row per host with the decision, the proxy path, and the rule.

When you finish this section, you can read a policy log row, name its proxy path, and tell a blocked host from a slow one.

Two proxies and one CA

Inside m101-demo, the environment carries the proxy address and its certificate:

the proxy lines in the environment of m101-demotextFull log
HTTPS_PROXY=http://gateway.docker.internal:3128
HTTP_PROXY=http://gateway.docker.internal:3128
…
NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
NODE_USE_ENV_PROXY=1
NO_PROXY=localhost,127.0.0.1,::1,gateway.docker.internal
…
PROXY_CA_CERT_B64=<base64>
…
REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
…
SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
Every line that is not about the proxy or its CA is cut. The CA value is masked as <base64>.

The address is gateway.docker.internal:3128, not the host.docker.internal:3128 of the legacy plugin (conflict C26). The docs describe two paths. Agents send HTTP and HTTPS through a forward proxy, and other TCP traffic is forwarded transparently. Both paths enforce the network access policies.

The first prototype was only an environment variable, and an agent bypassed it with no_proxy, as Kevin Wittek said on 2026-01-14. The capture sent a request to gateway.docker.internal:18080, a name in NO_PROXY, so curl skipped the forward proxy and opened a plain TCP connection. The transparent path caught that connection, and the log recorded it as transparent and blocked (the capture).

TLS interception: the proxy's own certificate authority, carried as PROXY_CA_CERT_B64 and merged into /etc/ssl/certs/ca-certificates.crt. The capture shows where it is used:

a blocked HTTPS request, with and without headerstextFull log
$ sbx exec m101-policy sh -c 'curl -sS --max-time 10 -I https://example.com; echo exit=$?'
HTTP/1.1 200 OK

HTTP/1.1 403 Forbidden
Content-Length: 90
Content-Type: text/plain

exit=0
[exit 0]

$ sbx exec m101-policy sh -c 'curl -sS --max-time 10 https://example.com; echo; echo exit=$?'
Approval required for example.com:443.

Review and respond with:
  sbx policy approval ls

exit=0
[exit 0]

HTTP/1.1 200 OK is the proxy accepting the CONNECT. The 403 Forbidden that follows arrived inside the TLS session, under a certificate the sandbox trusts, and its 90-byte body is the approval message. After sbx policy allow network --sandbox m101-policy example.com, the same request got HTTP/1.0 200 Connection established and then HTTP/2 200 with server: cloudflare (the capture).

The log classed that one as forward-bypass, a tunnel without inspection and without credential injection. So the ruling for C26 reads: the proxy terminates TLS only when it has to speak. That is a blocked host or a host with a bound credential, and it tunnels the rest. The v0.47.0 notes name both paths: a CONNECT tunnel without interception, and a late handshake check in the transparent proxy.

Reading policy log

policy log: shows which hosts the proxy allowed or blocked, with the matching rule, the proxy type, and the request count. It takes [SANDBOX], --json, --limit, and --type, and the help admits that filesystem logs are not supported yet (conflict C28). The JSON has two arrays:

the log of m101-policy after the deny rulejsonFull log
{
  "blocked_hosts": [
    {
      "host": "example.com:443",
      "vm_name": "m101-policy",
      "proxy_type": "forward",
      "rule": "denied: rule \"local:<uuid>\" matched op(action=net:connect:tcp, resource=net:domain:example.com:443)",
      "last_seen": "<ts>",
      "since": "<ts>",
      "count_since": "<n>",
      "reason": "Denied by local rule"
    },
…
  "allowed_hosts": [
…
    {
      "host": "example.com:443",
      "vm_name": "m101-policy",
      "proxy_type": "forward-bypass",
      "rule": "",
      "last_seen": "<ts>",
      "since": "<ts>",
      "count_since": "<n>"
    },
The second blocked row, the download.docker.com row, and the ports.ubuntu.com row are cut.

Each row has host, vm_name, proxy_type, rule, reason, last_seen, since, and count_since. A blocked row names the operation, op(action=net:connect:tcp, resource=net:domain:example.com:443), and either no applicable policies or the rule that matched. An allowed row in this capture carries an empty rule, so the log says that a host passed, and check network says why. PROXY takes five values, forward, forward-bypass, transparent, network, and browser-open. Figure 3.4 animates both requests.

The 403 body names sbx policy approval ls. The docs describe approval ls, inspect, and respond. Under balanced and deny-all, a request that matches no rule waits for your approval instead of being denied at once. The v0.47.0 help tree has no sbx policy approval command, while sbx policy ls --created-via approval exists (conflict C16).

What a host rule cannot see

A deny covers more than TCP (conflict C27). A 2026-05-26 post said UDP and ICMP are blocked and cannot be allowed. Since v0.33.0 a sandboxed process cannot resolve a domain that policy denies, loopback names excepted, and outgoing ICMP stays blocked. Since v0.45.0 UDP follows policy behind the experimental setting feature.udp-egress, and DNS resolution stops when no rule permits it. The ruling: a deny covers TCP, UDP, and the name lookup itself, and ICMP cannot be allowed. The guest's /etc/resolv.conf is a read-only bind from the host.

A host allow permits every request to that host, with any method, path, and body. The balanced preset allows github.com:443 and **.github.com:443 (the capture). Docker staff said on 2026-04-07 and on 2026-08-10 that an issue body or a gist on an allowed host is not blocked.

The product's answer is the HTTP rule. A kit's network-policy@2 entry can deny hosts: [api.github.com] with methods: [DELETE]. A network allow is a ceiling that HTTP rules carve into, and neither check network nor policy log evaluates a method or a path. Figure 3.5 puts the two rules side by side.

Upstream proxies are a separate setting: proxy, proxy.sandbox, proxy.daemon, and the no_proxy family, with SOCKS5 since v0.35.0. The settings table lists the keys.

Fig. 3.4one blocked and one allowed request to example.comsequence
One blocked and one allowed request to example.com Five lifelines: curl inside m101-policy, the forward proxy on gateway.docker.internal:3128, sandboxd with its authorizer, the policy store, and example.com. The first CONNECT is checked by sandboxd, finds no applicable policy, and the proxy answers 403 Forbidden inside the TLS session with a 90-byte approval message, then logs the host as blocked with proxy type forward. An allow rule for example.com is written in the sandbox scope. The second CONNECT is allowed as forward-bypass, the proxy opens a tunnel without inspection, example.com answers HTTP/2 200, and the host is logged as allowed. curl inside proxy :3128 sandboxd policy store example.com 1 CONNECT example.com:443 HTTPS_PROXY in the environment 2 op(net:connect:tcp, example.com:443) 3 match rules for sandbox:m101-policy 4 no applicable policies 5 No matching allow rule (default deny) 6 HTTP/1.1 403 Forbidden, 90 bytes inside TLS, under the proxy CA 7 log: blocked example.com:443 forward 8 policy allow network --sandbox m101-policy rule <uuid> example.com [tcp] 9 CONNECT example.com:443 10 allowed, forward-bypass 11 TLS tunnel, no inspection HTTP/1.0 200 Connection established 12 HTTP/2 200, server: cloudflare 13 log: allowed example.com:443 forward-bypass
The first CONNECT is denied inside TLS by the proxy itself, one allow rule later the same CONNECT becomes a forward-bypass tunnel, and both leave a log row. Read top down. Lifelines are curl inside m101-policy, the forward proxy, sandboxd with its authorizer, the policy store, and example.com. Dashed rose arrows are denials, and solid teal arrows are writes to rules and the log.
Fig. 3.5a host allow and a kit HTTP rule on api.github.comcomparison
A host allow and a kit HTTP rule on api.github.com Two columns. On the left, the default-code-and-containers group of the balanced preset allows github.com:443 and every subdomain, so a POST to api.github.com /repos/o/r/issues carrying data in its body is allowed, because a host rule reads host and port only. On the right, a network-policy@2 deny entry with hosts api.github.com and methods DELETE blocks DELETE /repos/o/r while the same POST still passes, because deny wins only where the method matches. A · HOST ALLOW, BALANCED PRESET B · KIT HTTP RULE, NETWORK-POLICY@2 default-code-and-containers github.com:443 · **.github.com:443 allow · net:connect:tcp POST api.github.com /repos/o/r/issues body: your data allowed: any method, path, body a host rule reads host and port only runtime.deny entry hosts: [api.github.com] methods: [DELETE] DELETE api.github.com /repos/o/r blocked by the HTTP rule POST /repos/o/r/issues still passes deny wins only where it matches
A host allow passes any request to github.com, body included, while a network-policy@2 entry denies one method on one host and leaves the rest open. Left, the default-code-and-containers group of the balanced preset and a POST that it admits. Right, the deny entry from the kit specification and the DELETE that it stops.

Docker Sandboxes (sbx) and Docker Agent (docker-agent) sbx 0.47.0, docker-agent 1.149.0 · bf4169c · 2026-10-07 · Edition 2026.10