{"slug": "threat-model-the-egress-not-the-model", "title": "Threat-Model the Egress, Not the Model", "summary": "A developer argues that AI features should be secured by threat-modeling the outbound payload rather than the model itself, since every prompt is a data transfer. The proposed approach is a \"boundary ledger\" and a deliberately simple preflight gate—pattern matching, a size cap, and a content-free decision record—that classifies data into public, synthesized, internal, personal, secret, and regulated tiers before any call leaves the worktree. The author notes that only the first two of four hops (local context builder and transport code) can be fully audited, and warns that free shared hosts are not a security boundary for customer records or credentials.", "body_md": "The thing you must defend in an AI feature is the payload, not the model, because every prompt is an outbound data transfer with your name on it. Models change weekly and pricing changes faster, so arguing about which one to call is cheap; the expensive mistake is shipping the wrong bytes. You can make that mistake once, quietly, and pay for it in an incident review months later. So the first artifact for any model-backed feature should be a boundary ledger, not a benchmark.\n\nThis matters more now that AI features are drifting into browsers and weekend side projects, where the egress path is assembled in five lines of code and never reviewed. When a call is that cheap to add, the review happens after the fact or not at all. A short, boring preflight check catches more real problems than a page of policy prose.\n\nEvery model call crosses four hops, and you can only audit two of them well. The first is your own worktree and context builder, where files, issue text, and stack traces get concatenated into one string. The second is your transport code, which decides headers, retries, timeouts, and whether the request body gets written to disk for debugging. The third is the provider endpoint, which sees the full payload and may retain it under terms you cannot inspect from your side. The fourth is whatever machine actually runs the call when you do not run it yourself, and a free shared host belongs to that fourth category no matter how convenient it feels.\n\nYou control hops one and two completely, which is exactly why the ledger should start there. A free server is not a security boundary; it is someone else's machine with a clock, a hypervisor, and a log rotation policy you did not set. That does not make hosted free tiers useless, and it does make them the wrong room for customer records, session cookies, or production connection strings. Write that sentence into your design doc now, while nobody is defending a deadline.\n\nA decision table beats a list of banned words, because it forces a choice per class of data instead of per scary keyword.\n\n| Data class | Examples | Default action | Why | \n|---|---|---|---|\n| Public | docs, open-source source, published schemas | send | Already public; leakage is not the risk | \n| Synthesized | types, interfaces, errors with IDs stripped | send after review | Reveals shape, not data | \n| Internal | architecture notes, ticket titles, roadmap text | transform | Aggregates reveal more than any single line | \n| Personal | emails, user IDs, addresses, free-text support notes | transform or block | Regulatory and contractual exposure | \n| Secret | keys, tokens, cookies, private keys, signed URLs | block | Rotation cost exceeds any feature value | \n| Regulated | health, payment, government identifiers | block | Needs a compliance review, not a regex | \n\nThe \"transform\" rows are the interesting ones, because transformation is real engineering and not a synonym for \"hope\". Tokenize identifiers deterministically per session, keep the mapping table local, and make sure the model never needs to reverse it. If the task cannot survive tokenization, you have found a feature that should not use a hosted model at all.\n\nThe gate below is deliberately dumb: pattern matching, a size cap, and a decision record that contains no payload text. It is not a DLP system and it will not catch a paraphrased secret, but it catches the mistakes people actually make at 2 a.m.\n\n```\n# egress_check.py — preflight for outbound model payloads\n# Reads a payload on stdin, writes a content-free decision record.\nimport hashlib, json, re, sys, time\nfrom dataclasses import dataclass, asdict\n\nRULES = [\n    (\"private_key\", re.compile(r\"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----\")),\n    (\"aws_key\",     re.compile(r\"\\bAKIA[0-9A-Z]{16}\\b\")),\n    (\"bearer\",      re.compile(r\"\\bBearer\\s+[A-Za-z0-9._\\-]{20,}\\b\")),\n    (\"email\",       re.compile(r\"\\b[\\w.+-]+@[\\w-]+\\.[\\w.]{2,}\\b\")),\n    (\"private_ip\",  re.compile(r\"\\b(?:10|192\\.168|172\\.(?:1[6-9]|2\\d|3[01]))\\.\\d{1,3}\\.\\d{1,3}\\b\")),\n]\nBLOCK = {\"private_key\", \"aws_key\", \"bearer\"}\nMAX_BYTES = 200_000\n\n@dataclass\nclass Decision:\n    ts: float\n    sha256: str\n    bytes: int\n    classes: list\n    verdict: str\n    reason: str\n\ndef classify(text: str):\n    hits = sorted({name for name, rx in RULES if rx.search(text)})\n    if hits & BLOCK:\n        return hits, \"block\", \"secret-class token present\"\n    if hits:\n        return hits, \"transform\", \"identifier-class token present\"\n    return hits, \"send\", \"no rule matched\"\n\ndef main() -> int:\n    raw = sys.stdin.buffer.read()\n    text = raw.decode(\"utf-8\", \"replace\")\n    hits, verdict, reason = classify(text)\n    if len(raw) > MAX_BYTES:\n        verdict, reason = \"block\", f\"payload over {MAX_BYTES} bytes\"\n    rec = Decision(time.time(), hashlib.sha256(raw).hexdigest(),\n                   len(raw), hits, verdict, reason)\n    with open(\"egress-log.jsonl\", \"a\", encoding=\"utf-8\") as fh:\n        fh.write(json.dumps(asdict(rec)) + \"\\n\")\n    print(f\"{verdict}: {reason} classes={hits or 'none'}\", file=sys.stderr)\n    return {\"send\": 0, \"transform\": 1, \"block\": 2}[verdict]\n\nif __name__ == \"__main__\":\n    sys.exit(main())\n```\n\nWire it in front of the request builder so the string that reaches the network is the same string that reached the gate, and fail closed on anything except exit code 0. The decision record holds a hash, a byte count, matched classes, and a verdict, with no snippet and no filename. That is enough to answer \"what left, when, and why did the gate allow it\" during a review, and not enough to become a second copy of the leak itself.\n\n```\ngrep -rn \"requests.post\\|fetch(\" app/ | grep -i \"model\\|completion\"\n# for each call site: route the assembled payload through the gate\nprintf '%s' \"$PROMPT\" | python egress_check.py || exit 2\n```\n\nA gate that has never failed is a gate you have not tested. Put a fake credential in a fixture, run the pipeline, and assert two things at once: the call is blocked, and the log line does not contain the fixture string. The second assertion is the one people skip, and it is the one that turns a logging habit into a security property.\n\n``` python\n# test_egress.py\nimport json, subprocess, pathlib\n\nCANARY = \"AKIAIOSFODNN7EXAMPLE\"  # fake, matches the aws_key rule\n\ndef run(payload: str):\n    return subprocess.run([\"python\", \"egress_check.py\"], input=payload.encode(),\n                          capture_output=True)\n\ndef test_secret_blocks_and_never_lands_in_log(tmp_path, monkeypatch):\n    monkeypatch.chdir(tmp_path)\n    assert run(f\"deploy key: {CANARY}\").returncode == 2\n    log = pathlib.Path(\"egress-log.jsonl\").read_text()\n    assert CANARY not in log\n    assert json.loads(log.splitlines()[-1])[\"verdict\"] == \"block\"\n```\n\nAdd a positive case for public text, one for an email that must come back as `transform`, and one oversized payload that must block on size alone. Rotate the canary value each quarter so the suite cannot pass by accident on a stale fixture. If your provider retains payloads beyond your contract window, the gate also gives you a defensible answer for what was exposed, because you can replay the hash list without replaying content.\n\nDisclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option, and those two things are genuinely useful for this workflow in one narrow way: you can exercise the whole path, gate included, against a real endpoint without waiting on a spend approval. Practice on real transport is where the ledger earns its keep, because retries, streamed responses, and SDK defaults are exactly where stray payload copies appear. The free server is convenient for that rehearsal and it is still somebody else's machine, so keep the canary fixtures fake and the data public while you learn the shape of your own egress.\n\nThe limitations deserve plain language. A pattern gate cannot see a secret that has been base64-encoded, paraphrased into prose, or split across two messages, and it says nothing about what the provider does with a payload after the response arrives. Free tiers also change shape over time, which is another reason to keep the gate in your repository instead of in a vendor dashboard. Treat all of it as a guardrail that raises the cost of a mistake, not as proof that no mistake happened.\n\nSkip this approach if your data is regulated, if a contract names a data residency region, or if you need per-request audit evidence that a third party will accept. In those cases you need a reviewed processing agreement and an internal endpoint, and a regex gate in a shell script is not a substitute for either. Skip it too if your payloads are public by design, since the gate would only add friction to a path with nothing to protect.\n\nFor everyone else, the sequence is short and unglamorous. Draw the four hops, fill in the table for the data you actually handle, put the gate in front of the request builder, and write the canary test before the first real user touches the feature. Then, when the next model release makes you want to swap providers, the swap is a one-line change and the boundary work is already finished. If you want a cheap place to rehearse that loop, the free tier mentioned above is a reasonable starting room.", "url": "https://wpnews.pro/news/threat-model-the-egress-not-the-model", "canonical_source": "https://dev.to/devrs_886/threat-model-the-egress-not-the-model-7j9", "published_at": "2026-10-19 08:40:26+00:00", "updated_at": "2026-10-19 08:44:33.106492+00:00", "lang": "en", "topics": ["ai-safety", "ai-tools", "developer-tools"], "entities": [], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/threat-model-the-egress-not-the-model", "markdown": "https://wpnews.pro/news/threat-model-the-egress-not-the-model.md", "text": "https://wpnews.pro/news/threat-model-the-egress-not-the-model.txt", "jsonld": "https://wpnews.pro/news/threat-model-the-egress-not-the-model.jsonld"}}