How it works
The default is not "an empty allowlist".
Deny-all is the floor, not a setting
The default is not "an empty allowlist". It is deny-all as a floor no policy can remove, and the difference matters:
- a policy that names no permitted destination is refused at load, not loaded as an empty one — because an empty allowlist that loads silently lets you believe it was reviewed and accepted, when in fact it was never written;
- a proxy configured with no policy at all refuses to start. It does not start and deny everything, which looks exactly like a working proxy and is in fact one nobody configured.
A rule admits a host and a port. Never a host alone. There is no way to write "this host, any port" — one open port never implies another. That is deliberate: a machine you trust on 443 is not a machine you trust on 22.
The run reaches the network only through the proxy
A run is given one way out: a socket created for that run alone. Because the socket exists only for that run, the socket is the identity — there is no proxy password to present, so there is none to steal, log, rotate or leak. A client that tries to authenticate to the proxy is refused, and that refusal is not a misconfiguration to fix by finding the right credential (§6).
⚠ Since 2026-08-30 that half is BUILT AND LIVE — §2's vocabulary, and §8's row the per-run
socket in the run lifecycle. The run lifecycle mints the socket, opens the door, hands the agent
ALL_PROXY and unlinks on exit, and all three confinement arms carry their half (§9A.1, §9A.5,
§9A.6). ⚠ On one condition, which is the whole of the caveat: it happens for a run that passed
--egress-policy and for no other. The older wording said what kept it unreached was the
assignment, and that is what shipped: a run can now name a policy, and the opener is wired
into the engine's dependencies. ⇒ Read this as shipped code that a run reaches when it asks. ⛔
What is still missing is the compulsion — nothing makes the proxy mandatory rather than
merely available, because nothing forces a dispatch to ask (§8's last row).
The tunnel reads nothing — a privacy and compliance property, not an implementation note
The proxy terminates no TLS and inspects no content. It is a tunnel: it reads enough bytes to learn which host and port a connection is for, judges that, and from then on moves opaque bytes.
For a security officer, three consequences worth stating separately:
- there is no interception certificate, no private key to protect, and nothing on your fleet that can read your traffic's contents. You are not being asked to install a man-in-the-middle;
- no request bodies, headers, tokens or URL paths ever exist in a decodable form anywhere in this control. That is not a redaction policy that could be misconfigured — the code that would parse them does not exist (§7);
- the request-smuggling class of vulnerability is deleted by construction. Smuggling needs
two parsers that disagree about where one request ends. The HTTP-style door reads exactly one
request line and its header block, and then stops reading: everything the client sends behind
that is tunnel traffic and is moved as opaque bytes, never parsed. There is no second request
here for a second parser to find. A client that sends its first tunnel bytes immediately,
without waiting for the proxy's
200, is served — the standard treats those bytes as tunnel payload, and both doors buffer them and relay them after the destination has been judged.
One lookup, and the address that was checked is the address that is dialled
A permitted hostname is resolved once. That one answer is what gets checked against the floor, and that same answer is what gets connected to. The name is never resolved a second time.
This closes a specific attack: an allow-listed name whose second DNS answer is
169.254.169.254. If the proxy checked one answer and dialled another, the window between
them is the whole exploit.
The first answer is the answer. There is no "try the next address until one is permitted" — that would turn a DNS server an attacker partly controls into a search for a reachable destination. A denied first answer is a denied destination.
The TLS hostname is compared to the hostname that was permitted
After a connection is approved and before one byte reaches the far side, the proxy looks at the client's opening bytes. If they are the start of a TLS handshake, the hostname the client announces in it (the SNI) is compared to the hostname the policy judged.
- not TLS ⇒ nothing to compare, and the destination was already judged;
- TLS announcing a different hostname ⇒ refused. The connection is torn down and is not re-judged against the hostname it actually asked for. A second judgement is a second door;
- TLS announcing no hostname at all, or an unreadable one ⇒ refused, and refused identically. A handshake toward a permitted name that declines to say which name it wants cannot be attributed to the rule that permitted it. Both cases share one refusal so a client cannot learn that omitting the hostname is the cheaper way past.
Both sides are normalised the same way, which is why EXAMPLE.com. matches example.com and
why a Cyrillic look-alike does not.
⚠ A rule written as an IP address is not hostname-checked — there is no name in the request to compare. See §4.4 and §6.3.
The connection is bounded
Seven ceilings bound a connection: how much tunnel data a client may send before the hostname check can be answered, how much the protocol request itself may cost, two bounds on a line-oriented handshake, a deadline for the whole handshake, an idle timeout, and a total-bytes limit for one connection. Each has its own name in the audit record (§6.4) — with one deliberate exception, noted below.
The request and the data it carries are counted separately, and that is a correctness
property rather than a refinement. The bytes a protocol spends naming your destination — an
HTTP-style CONNECT line, a SOCKS5 request — are the wire format's cost, and they are charged
to the request ceiling. The bytes your client sends through the tunnel are charged to the data
ceiling. When one number counted both, two things followed that an operator would not expect:
a long hostname could be refused for being long on one door and served on the other, and the
same bytes could be refused or served depending on how your client happened to split its
writes. Neither is possible now.
⚠ The request ceiling and the over-long-line ceiling share one name in the record
(egress-limit-line-length). They are two sizes of the same thing — what the wire format cost —
and the number printed with the refusal tells you which one you crossed. The audit vocabulary is
a closed set of seventeen names and it was not widened for this.
All seven or none. A partial set is refused by name rather than filled in with defaults, because a default here is a ceiling nobody reviewed. A connection missing one bound is not a bounded connection.
⚠ One consequence a DevOps engineer should size deliberately: the total-bytes limit is the only bound on a busy connection. A connection whose two ends keep talking has no wall-clock limit — the idle timeout measures silence, not duration. A large repository clone or a container image pull will hit the total-bytes ceiling and nothing else.
Where this stands today
THREE STATES, and a security reviewer must not collapse them into two. This legend is the ONLY place that says what they mean, and the other chapters use the same three words and point back here…
Writing a policy
⚠ The policy document is BUILT AND LIVE (§2's three states, §8's row) — a run that passes --egress-policy is judged by the file this section teaches you to write. This paragraph said "the policy document does not…