如何在智能體連接免費模型端點前設定預設拒絕出口規則
Default-Deny Egress Before Your Agent Meets a Free Model Endpoint
jaryn 提出一套在智能體連接託管模型前管控資料外傳的方法,結合明文內容檢查與主機核心層的目的地預設拒絕規則。文章示範以 nftables 限制 IP 和連接埠、以內容閘門攔截秘密字串及禁止傳送的資料類別,並在 CI 執行單元測試和 canary 連線測試。文章披露是 MonkeyCode 產品推廣內容,並稱其免費方案當時包括 10M tokens 和免費伺服器,提醒讀者自行核實現行配額。
Here is a script I keep watching play out. A team reads that a free tier exists, spins up an agent on someone else's host, points it at a hosted model, and ships a demo by Friday. Nobody asks where the data went until an auditor does.
So let me ask the uncomfortable version early: if your agent runs on a host you don't fully control, what exactly is stopping it from sending your repository somewhere you never approved?
I'm not going to re-report benchmark news I haven't verified myself. But the trend signal is easy to read. DEV's front page this week is full of AI fatigue, "AI got better while software didn't," and posts about thousands of open-source AI projects landing in a single weekend. Teams respond by trying free tiers. And a free tier is exactly when the trust-boundary question gets real, because you're adopting two new boundaries at once.
This post is a security walkthrough. Threat model first, then two enforcement layers you can actually test, then a decision table for what may cross which boundary. Everything below is runnable fixture code, not a policy document.
The threat model, in five lines
Write this down before you write any agent code. If you can't fill all five lines, you don't have a threat model, you have a vibe.
-
Asset: the plaintext your agent can read — source,
.envfiles, logs, ticket bodies, fixture data with real customer rows. - Actor: anything that can influence the payload. That includes prompt injection from a fetched file, a compromised dependency, and a helpful teammate who pastes a token "just for testing."
- Path: prompt assembly → HTTP client → TLS → someone else's inference host.
- Invariant: no secret-shaped string and no denied data class ever leaves the host, even when the agent is wrong.
- Evidence: a failing canary connection and a failing unit test, both executed in CI.
Note line four. The invariant has to survive an agent that is confidently incorrect, because that is the normal state of agents.
Two boundaries, and only one can see plaintext
This is the part people get wrong, and it costs them a false sense of safety.
| Layer | Where it lives | What it can see | What it can enforce |
|---|---|---|---|
| Content gate | Your process, before encryption | Full plaintext payload | Regex/classifier rules on strings |
| Destination gate | The kernel, on the host | IP, port, protocol, volume | Allowlist, default deny, counters |
The content gate can read everything and can be bypassed by any code path that skips it. The destination gate cannot read anything, and cannot be bypassed by application code — but it also can't tell a secret from a shopping list. You need both, and you need to know which one is load-bearing when they disagree.
Step 1 — Write the egress contract before you write the agent
The contract is a small file that both layers read. One source of truth, two enforcement points.
{
"version": 1,
"reviewed_at": "2026-10-16",
"allowed_destinations": ["203.0.113.10:443"],
"resolver": "203.0.113.53:53",
"data_classes": {
"public_docs": "send",
"internal_source": "send_after_review",
"customer_pii": "never",
"credentials": "never",
"logs_with_tokens": "never"
}
}
Two rules for this file. Pin the destination by IP and port, not by hostname — DNS is a second trust boundary and I'd rather not argue about it in a review meeting. Second, date it. An undated contract is a rumor.
Step 2 — Enforce destinations in the kernel
On a host where you control the kernel, this is the whole game. Default deny, allow the one endpoint, count and log the rest.
#!/usr/sbin/nft -f
# /etc/nftables.d/agent-egress.nft
table inet agent_egress {
set allowed_dst {
type ipv4_addr
flags interval
elements = { 203.0.113.10 }
}
chain output {
type filter hook output priority 0; policy drop;
oifname "lo" accept
ct state established,related accept
ip daddr @allowed_dst tcp dport 443 accept
ip daddr 203.0.113.53 udp dport 53 accept
meta nfproto ipv6 drop
counter log prefix "agent-egress-drop " level warn
}
}
Load it and confirm it took effect:
sudo nft -f /etc/nftables.d/agent-egress.nft
sudo nft list table inet agent_egress
Now prove it with a canary. A rule you haven't tested is a comment with extra steps.
CANARY=203.0.113.99
if timeout 5 bash -c "exec 3<>/dev/tcp/${CANARY}/443" 2>/dev/null; then
echo "FAIL: canary destination reachable"; exit 1
else
echo "PASS: egress blocked"
fi
sudo nft list table inet agent_egress | grep -c "agent-egress-drop"
If the canary connects, stop. Fix the policy before you debug the agent.
Step 3 — Enforce content where plaintext exists
Kernel rules can't read a payload, so the string policy has to live in the process that builds the request. Keep it dumb and fast.
# gate/preflight.py
import json, re
SECRET_PATTERNS = {
"aws_access_key": re.compile(r"\bAKIA[0-9A-Z]{16}\b"),
"github_pat": re.compile(r"\bghp_[A-Za-z0-9]{36}\b"),
"private_key": re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
"bearer": re.compile(r"\bBearer\s+[A-Za-z0-9._\-]{20,}"),
}
DENY_CLASSES = {"customer_pii", "credentials", "logs_with_tokens"}
class EgressViolation(Exception):
"""Raised before bytes leave the process."""
def assert_payload_allowed(payload: str, contract: dict, declared: str) -> str:
if contract["data_classes"].get(declared) == "never" or declared in DENY_CLASSES:
raise EgressViolation(f"data class '{declared}' is not sendable")
hits = sorted(n for n, rx in SECRET_PATTERNS.items() if rx.search(payload))
if hits:
raise EgressViolation(f"payload matched {hits}")
return payload
if __name__ == "__main__":
contract = json.load(open("egress.contract.json"))
print(assert_payload_allowed("how do I pin nftables?", contract, "public_docs"))
And the fixture pair, because one-sided tests are theatre:
# tests/test_preflight.py
import pytest
from gate.preflight import EgressViolation, assert_payload_allowed
CONTRACT = {"data_classes": {"public_docs": "send", "customer_pii": "never"}}
def test_negative_secret_shaped_string_is_blocked():
with pytest.raises(EgressViolation):
assert_payload_allowed("token: ghp_" + "a" * 36, CONTRACT, "public_docs")
def test_negative_denied_class_is_blocked():
with pytest.raises(EgressViolation):
assert_payload_allowed("email a@b.example", CONTRACT, "customer_pii")
def test_positive_public_question_passes():
assert assert_payload_allowed("regex help?", CONTRACT, "public_docs") == "regex help?"
Step 4 — Wire both layers into one gate
Enforcement that isn't in CI is a suggestion.
# .github/workflows/egress.yml
name: egress-gate
on: [pull_request]
jobs:
gate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install pytest
- run: pytest -q tests/test_preflight.py
- run: bash scripts/canary.sh
The unit tests catch content regressions in seconds. The canary catches the day someone relaxes the firewall "temporarily." Different failure modes, different layers, same gate.
Decision table: what may cross which boundary
| Data class | Send to hosted model? | Store on the host? | Enforced by |
|---|---|---|---|
| Public docs, OSS source | Yes | Yes | Nothing — it's already public |
| Internal source | Only after review, pinned revision | Yes | Content gate + review |
| Customer PII | No | Only if already required by policy | Content gate, deny class |
| Credentials, tokens | No | Canary/short-lived only | Both layers, deny class |
| Logs with tokens | No | Redact at write time | Content gate, plus log sink policy |
Where a free tier genuinely helps, and where it doesn't
Here's the honest version of the pitch. Two things matter for this workflow.
Disclosure: This article was prepared as part of MonkeyCode's product outreach.
First, the free model access matters because this whole approach depends on repeatedly running negative fixtures — canary probes, secret-shaped payloads, denied data classes. That is cheap, boring traffic, and paying per token to run it is silly. Second, the free server option matters because Layer 2 only exists if you control the kernel. On a pure client-side SaaS you can't write an output chain; you can only hope the application calls your gate.
If the host you get is a locked-down container without NET_ADMIN, you cannot load that nftables table — plan for substituting a forced proxy plus a resolver you control, or accept that you only have Layer 1. Check before you design around it.
The project advertises a free tier that at the time of writing includes 10M tokens and a free server. Treat that as operator-supplied and verify the current limits yourself on the project's own page before you build a budget on it; quotas move, and dated assumptions rot fast.
If you want to try the negative-fixture suite against an endpoint you don't own, a free tier is a reasonable place to start — just bring your own contract.
Limitations and failure modes
Be clear-eyed about what this does not do.
- TLS is opaque to you. The destination gate sees IPs and ports. You cannot content-inspect the inference request without terminating TLS with a CA you control, which means running a broker you now have to secure too.
- DNS is a second door. Pin the resolver, or an attacker swaps the name behind an allowed IP.
- DoH/DoT bypass. A dependency that ships its own resolver walks around your port-53 rule.
- Package installs break. Default-deny means your mirror must be on the allowlist, or builds fail at 2 a.m.
- Counters lie by omission. If the log rule is the only evidence and nobody reads it, you have detection without detection.
Who should not use this
Don't build this for a throwaway weekend experiment on public sample data — the overhead isn't worth it, and default-deny rules will just slow you down. Don't use it if you need full payload inspection of encrypted traffic and you're not prepared to run a TLS-terminating broker with its own threat model. And don't adopt it as a substitute for data classification: the gate enforces what you declared. If your team can't say which column is PII, no firewall will guess for you.
The boundary question
The gate works when both layers are boring and both are tested. So the question I'd leave you with isn't whether to allowlist — it's where the decision belongs. Should the invariant "this data class never leaves" live in CI as a unit test, or in the kernel as a rule that ignores your application entirely?
I have an opinion, but it depends on your blast radius. Which layer would you trust when the agent is confidently wrong?
來源:DEV Community · dev.to