All lessons › Function B — Securing AI Architectures › Securing the Architecture — Runtime and the Gateway
B3.3

Egress control

B3
▶
Lightboard recording

Not recorded yet. The lesson below is complete and runnable today — the video is added later without touching this page.

Video placeholderpublishes automatically from recordings/B3.3.mp4
Risk

An agent with unrestricted egress turns any successful injection into data loss.

Control

An allow-list at the network boundary, enforced where the agent cannot rewrite it.

The lab

Attempt exfiltration to several destinations under an allow-list and see which survive.

◉Use case relevance

Every exfiltration path in the architecture ends at the same place: a packet leaving your network. That makes egress the highest-leverage control you have, and the one most often left as allow-all because it broke something once.

At CyberTravels. Nothing in your tree constrains where the agent reaches — _stubs.HTTP takes any URL and the only reason nothing leaves is that the stub is inert. You add cybertravels/egress.py: an allow-list of hosts and a check on what is being sent, because the vendor API the agent is supposed to call is a perfectly good channel for a key to leave through.

◴What this lesson is — Day 0, Day 1, Day 2

Egress control for a destination chosen at run time by a model, which is what makes it different from a firewall rule written against a deployment. Both halves: the destination, and what is being sent to it — because a vendor API the agent is supposed to call is a perfectly good channel for data to leave through.

Day 0 — why

An agent with unrestricted egress turns any successful injection into data loss.

Day 1 — how

An allow-list at the network boundary, enforced where the agent cannot rewrite it.

Day 2 — measure

Outbound destinations outside the allow-list, and the bytes that reached them.

Measure this

what to measuredenominator or target
outbound destinations outside the allow-listcount
bytes that reached thembytes

▦The framework, and how it works

   every exfiltration path, whatever its start, ends here:

   prompt leak  --+
   tool abuse   --+---> data assembled ---> [ egress ] ---> out
   code exec    --+                            ^
   memory read  --+                            |
                                    one place to enforce, one to log

   allow-all egress makes every control upstream best-effort

Mitigates: T2 Tool Misuse · T6 Intent Breaking · LLM02 Sensitive Information Disclosure.

Egress is the highest-leverage control in the architecture, for a structural reason: every exfiltration path ends at the network boundary, no matter how the agent was persuaded to take it.

Injection, tool misuse, a compromised MCP server, model-authored code, a poisoned peer message — all of them converge on the same final step. Data leaves. A control at that step does not need to understand what happened upstream, which is exactly what makes it robust: it is the one place where you do not have to predict the attack.

Two rules that decide whether it works:

Allow-list, never deny-list. You cannot enumerate the internet. A deny-list blocks the destinations you thought of.

Specific destinations. .googleapis.com or .s3.amazonaws.com is not an egress policy — anyone can create a bucket in those namespaces, and B1.3's attacker will. The allow-list holds the destinations this workload actually needs, and there are usually fewer than five.

And one placement rule: enforce it where the agent cannot rewrite it — the network layer, the sidecar, the gateway. An egress check inside the agent's own process is a suggestion to the component being attacked.

The cost is honest: an agent that needs the open internet cannot have this control, and that is a design decision to make deliberately rather than by default.

What this control closes. The one control that does not need to know how the attack worked, because every exfiltration path ends here.

2 · The control

3 · Probing the sandbox, as a skill

An allow-list in a config file is a claim. CyberTravels' Coding Agent runs model-authored code, so the claim worth making is this runtime has no egress, and that one is settled by probing, in an environment you own. The procedure names the residual paths — DNS first, because it is the one that has actually been used — and requires you to record what you did not test, because a probe list is a statement about coverage. This is the file in this repository:

▶Real time execution as skill

The skill — skills/attestation/sandbox-egress-verifier/SKILL.md

name: sandbox-egress-verifier
description: >-
  Verify AI-generated code executes only in an approved sandbox with no
  network egress, and probe the known bypass paths. Use when attesting a no-
  egress execution claim, choosing a code-execution runtime, or asked
  whether a sandbox actually contains model-authored code.
allowed-tools: Bash, Read

Sandbox Egress Verifier

Controls: Control 2 — sandboxed no-egress runtime

Confidence: PARTIAL — and this ceiling is not negotiable

You can prove a sandbox is misconfigured. You cannot prove the negative "no covert channel exists". Network isolation has documented bypass paths, and a verdict of PASS on this control would be a claim the evidence cannot support.

Cap every verdict this skill produces at PARTIAL.

When to use this

When a deployment claims isolation. Run it knowing what it cannot do: absence of a covert channel is not provable, DNS and object-storage bypasses are documented, and the honest output is a capped PARTIAL rather than a pass.

Procedure

  1. Identify the runtime and its network mode. Record which isolation technology is in use and which mode it runs in. A mode that permits general outbound network access is an immediate FAIL — no probing required.
  1. Confirm the runtime is on the approved list. An approved sandbox in the wrong mode and an unapproved sandbox in the right mode are both findings.
  1. Probe the known residual paths, in an environment you own:
  • DNS. Name resolution is frequently permitted where general egress is not, and A/AAAA queries carry data outward. This has been demonstrated to yield interactive command-and-control on a major managed sandbox.
  • Object storage reachability. Managed storage endpoints are commonly left reachable and have been used as a command-and-control channel.
  • Host escape. Subprocess spawning and any path from the sandboxed process to the host.
  1. Record what you did not test. A probe list is a statement about coverage, and coverage is the honest part of this verdict.

Example

Input — the fixture committed at the top of scripts/sandbox_egress_verifier.py. Edit it and re-run: the buckets, counts and verdicts below are derived from it, not hard-coded.

Output — the opening lines of a real run:

destination                           deny-list   allow-list  note
api.corp.example                      allow       allow       the one it actually needs
archive.evil.example                  block       block       B1.3's exfiltration target
attacker-bucket.s3.amazonaws.com      allow       block       a bucket anyone can create
169.254.169.254                       allow       block       cloud metadata - every credential
pastebin.example                      allow       block       not on anyone's deny-list

deny-list lets through : 3  ['attacker-bucket.s3.amazonaws.com', '169.254.169.254', 'pastebin.example']

The run continues past this. test_skills.py executes the script on every build, with no model configured — so what CI proves is that it runs and refuses legibly, not that it prints these lines. The output above is a recorded run against a real model, and nothing re-diffs it: a model's answer is not reproducible, and this repository does not claim otherwise anywhere it can be checked.

Output contract

{
  "deployment_id": "str",
  "sandbox_runtime": "str",
  "network_mode": "str",
  "approved_runtime": true,
  "bypass_probes": [{"path": "dns|object_storage|host_escape", "reachable": false, "detail": "str"}],
  "untested_paths": ["str"],
  "verdict": "PARTIAL|FAIL",
  "verdict_ceiling_reason": "absence of a covert channel is not provable at runtime"
}

PASS is not a permitted value. If a tool emits one, that tool is wrong.

Failure modes

  • Reporting PASS because the configuration looked right. That is the whole reason this skill has a ceiling.
  • Probing only general HTTP egress. DNS is the path that has actually been used.
  • Testing against someone else's infrastructure. Probe only what you own.

Three ways to do this lesson. Pick one. They run the same procedure against the same code — what changes is who drives it, and how much you see.

1In your AI agent — you are taught

The agent reads the lesson, explains the idea, fetches the code, runs the check and answers your questions as you go. This is the one to take if you are here to learn.

Install it, once. From the folder you cloned. --all does every agent it finds on your machine:

python3 scripts/install_skills.py --all --lessons B3.3

Then restart your agent and open it in that folder. On Windows, if python3 says Python was not found, use python instead.

Then pick b3-3-egress-control:

agentinstall just for this agentstart it
Claude Codepython3 scripts/install_skills.py --tool claude --lessons B3.3type /b3-3-egress-control
GitHub Copilot (VS Code)python3 scripts/install_skills.py --tool copilot --lessons B3.3type /b3-3-egress-control in Chat
Cursorpython3 scripts/install_skills.py --tool cursor --lessons B3.3type / in Agent chat and search for it
Codexpython3 scripts/install_skills.py --tool agents --lessons B3.3type $b3-3-egress-control, or run /skills

2As one Python command — you just run it

No agent, no conversation. The same lesson executes and prints the same readback. Take this one if you have no assistant, if you are reviewing rather than learning, or if you want the result without the teaching around it.

python3 scripts/lesson.py B3.3

3Read the code, and the difference — nothing runs

Neither of the above, and no model needed. The first command writes CyberTravels exactly as it stood at the end of this lesson; the second prints the diff — the lines this lesson added or changed, and nothing from any later one.

python3 scripts/checkpoint.py --at B3.3 --out work/cybertravels
python3 scripts/checkpoint.py --at B3.3 --diff

On Windows, type python wherever these say python3 — and use \ instead of / in the paths. Everything else is identical. A0.0 step 2 explains why.

Ways 1 and 2 need a model; way 3 does not. A signed-in Claude Code CLI needs no API key, and any OpenAI-compatible endpoint works too. With neither, the audit exits 2 and says so rather than inventing an answer. A0.0 sets this up.

Ways 1 and 2 both end with the same readback: What I did, What changed, The number and Read this next. A step that was skipped or refused is reported as exactly that, never as a pass.

If you did the last lesson’s challenge, expect this to stop. Most challenges ask you to edit the code, and ways 1 and 2 will not overwrite your edits: you get stopped, not a failure, with two options — --out for a fresh folder that keeps your work, or --force to discard it and start this lesson clean.

✓What you just proved

Five destinations are evaluated both ways. The deny-list permits three exfiltration paths — a public-cloud bucket namespace anyone can register in, the cloud metadata address, and a host nobody listed — while the exact allow-list permits only the one destination the workload needs. The skill also reports its shape. Its ceiling is PARTIAL and not negotiable: a configuration that looks right is not a PASS, and probing general HTTP while leaving DNS alone tests the path nobody uses. The untested list is part of the output, not an omission from it.

✎Your turn

Write the allow-list for one agent by listing the hosts it genuinely calls. If it is under five, you can ship this control this week; if it is unbounded, that is the finding.

Tools used

Ciliumagentgateway

Skill source: skills/attestation/sandbox-egress-verifier

← PreviousB3.2 · Sandboxed execution