16 · Security & Multi-Tenant Architecture: Treat Security as Structure, Not a Patch
The thesis in one line: security is not a scan you bolt on before launch, nor a patch you slap on after an incident; it's a question you should be asking when you draw your very first architecture diagram — "who would attack each of my data flows and each of my trust boundaries, and how? how big can a single mistake blow up?" Done right, security is invisible; done wrong, it's fatal. And now that AI writes your code and agents act on your behalf, the attack surface is widening faster than ever — security has been forced from a "feature" into a "structure."
🧭 Chapter 7 of the Advanced track. In 06 · Quality Attributes & Trade-offs, security was just one of seven quality attributes, and we only touched on it: distrust input, least privilege, defense in depth. This chapter pulls it out and digs deep — because security differs from the other attributes in one fundamental way: a bit less performance is an experience problem; a bit less security is a life-or-death problem. One data breach can shut a company down; one poisoned dependency can ripple across the entire web.
And the AI era makes this chapter more important than ever. AI can write a chunk of "working" auth code in seconds, but "who is allowed to touch this data flow, how far can this intrusion spread, should this agent even have shell access" — those are judgment calls, and the cost is borne by your business, your users, your compliance posture. Implementation gets cheaper and cheaper, while the judgment of "thinking clearly about who would breach you, and how" gets more and more valuable.
1. Security Is Structure, Not a Patch: First, Learn to "Think About Bad Things Systematically"
Beginners do security by "adding one whenever it occurs to them": tack on a login, tack on HTTPS, run a scan before launch. This is patch thinking — scattered, reactive, treating wherever it hurts. Architects do security by first laying out the whole system and systematically asking, at every single point, "how would this get breached?" This approach has a name: threat modeling.
The classic framework is STRIDE, proposed by Microsoft in 1999 (created by Praerit Garg and Loren Kohnfelder). It encodes "what bad things can happen" into a six-word mnemonic, forcing you to go through your data flow diagram (DFD) item by item — for every process, every data store, every data flow, and every trust boundary, you ask the same six questions:
STRIDE — break "what bad things can happen" into six classes,
asked against the data flow diagram one by one
───────────────────────────────────────────────────────────
S Spoofing impersonation → breaks "authentication" (are you really you?)
T Tampering altering data → breaks "integrity" (has the data been changed?)
R Repudiation deniability → breaks "non-repudiation" (can they deny it later?)
I Information info leakage → breaks "confidentiality" (did someone see what they shouldn't?)
D Denial of Service denial → breaks "availability" (can it be knocked over?)
E Elevation of Priv privilege esc. → breaks "authorization" (can a normal user become admin?)The soul of threat modeling is the concept of the "trust boundary." A piece of data travels from the browser into your gateway, from the gateway into an internal service, from the service into the database — and every time it "crosses a boundary," the trust level changes. Bad things almost always happen at the boundaries: when external input crosses into the internal world (injection), when a low-privilege component reaches toward a high-privilege resource (privilege escalation), when sensitive data crosses out of the system (leakage). Draw the boundaries clearly and you know where to set up defenses.
A concrete example: take the kind of "user → gateway → order service → database" data flow we drew back in 02 / 03, and walk it through the trust boundaries with STRIDE:
User ══╗(trust boundary ①: public→system) Order service ══╗(trust boundary ②: service→data)
▼ ▼
[API gateway] ──────────────▶ [Order service] ──────────────▶ [Database]
S Spoof someone else's token? E Tamper with someone T Bypass the app and write
D Get flooded and knocked over? else's order (over-priv)? directly to the DB?
I Error messages leak T Tamper with the incoming I Dump the whole DB → mass leak?
internal structure? amount? I Plaintext in backups/logs?
R Change an order, then
deny it later?The point is not to memorize six letters, but to build the reflex of "going through every boundary and thinking about bad things, one by one." A blunt way to remember STRIDE: it is six common things an attacker wants to do — pretend to be you (S), change your data (T), deny what they did (R), see what they shouldn't (I), knock you over (D), or turn themselves into an admin (E). You don't need to chant the letters; at every trust boundary, ask those six questions. You'll notice: the gateway boundary mostly defends against spoofing (S) and denial of service (D); the service boundary mostly defends against over-privilege (E) and tampering (T); the data boundary mostly defends against the dump-and-leak (I). Defensive resources should be deployed precisely along this map, not spread evenly.
Architectural wisdom: patch thinking asks "what security measures did I add?"; structural thinking asks "has every one of my data flows and every one of my trust boundaries been attacked — by whom, and via which letter of STRIDE?" The former is a checklist, the latter is a map. Security is not about "sticking" things onto a system, but about weaving "how would the attacker think?" into the structure as you draw it — this is the systematic version of the "distrust input" line from Chapter 06. And the biggest difference between security and the other quality attributes is this: performance and availability can be "shipped first, optimized later," but a structural security hole is often "set in stone the moment you ship, impossible to patch after."
2. Defense in Depth + Zero Trust: Don't Trust Anyone Just Because They're "On the Internal Network"
The old-generation security model was the castle and moat: build a thick wall (the firewall), with the dangerous public internet outside and the trusted internal network inside — once you're past the wall, you basically roam free. This model has a fatal assumption: "internal network = trusted." But reality slaps it down again and again: once an attacker breaches the boundary (phishing, a vulnerability, an insider), they move sideways through the "trusted internal network." This is called lateral movement — SolarWinds, countless ransomware outbreaks, all used this trick to amplify a single-point breach into total collapse.
Worse still, that "wall" itself has already crumbled today: remote work means employees access systems from their home networks, so "inside vs. outside the wall" has lost its physical boundary; the cloud means your services run in someone else's data center; microservices have made east-west (service-to-service) traffic explode, far surpassing north-south (in-and-out-of-system) traffic. When "internal" is now everywhere, "the internal network is trusted" becomes an empty phrase.
Two complementary ideas fix this:
① Defense in Depth: don't count on a single wall to stop everyone. Layer your defenses, so that breaching one still leaves the next.
Single-layer defense (fragile): Attacker ──breaks the one and only wall──▶ straight in, all lost
↑ once breached = game over
Defense in depth (resilient): Attacker ─▶[WAF]─▶[authn]─▶[authz]─▶[encrypt]─▶[isolate]─▶[audit]
↑ each layer defends independently;
breach one and the next still has your back② Zero Trust: simply abolish the assumption that "the internal network is trusted." The core creed is one sentence — "never trust, always verify": whether a request comes from the public internet or the internal network, every access re-verifies identity, checks authorization, and encrypts transport; trust is no longer tied to "network location," but to "this user + this device + the context of this request."
The most famous implementation is Google's BeyondCorp. It began with a bold decision: let all employees work securely from any untrusted network, with no VPN. The approach was to deploy every application to the public internet and put an access proxy in front of each request, dynamically deciding whether to let it through based on "who you are, the state of your device, and how sensitive the resource you're accessing is" — every request is treated as coming from an untrusted network, until it has been authenticated, authorized, and verified. The wall of the network perimeter was replaced by a dynamic judgment of "identity + device + context."
Running through both is the Principle of Least Privilege from Chapter 06: every component, every account, gets only the tiny bit of privilege it needs to do its job, not one bit more. This principle is the foundation of the next section on "blast radius" — the less you grant, the smaller the explosion when something goes wrong.
Architectural wisdom: "secure because it's on the internal network" is an outdated luxury assumption. The essence of zero trust is reshaping "trust" from a static network location into a dynamic judgment that must be re-earned every single time. It isn't free — verifying every access brings friction and overhead — but what it buys you is this: a single point breach no longer equals total collapse.
3. Blast Radius & Isolation: Lock "the Cost of a Single Mistake" Into a Small Cell
Even taken to the extreme, security cannot guarantee "never breached." So the mature view of security shifts from "never have an incident" to a more realistic question: once an incident happens, how far can it spread? That "how far" is the blast radius.
This is the very same line as the "isolation mindset" from 12 · Designing for Failure, now spoken in a security context: isolation means locking "the cost of a single mistake" into a small cell, so it can't overflow and flood the whole system. Fault isolation defends against "one component going down dragging the whole system with it"; security isolation defends against "one point being breached spreading to the whole system" — in essence, the same structural wisdom.
No isolation (shared fate): one master key / one big privilege / one big database
one breach ──▶ all data, all tenants, all systems, swept up at once
↑ blast radius = the entire company
Isolation (compartments): [cell 1][cell 2][cell 3][cell 4] ← each with its own
privileges/network/data/keys
one breach ──▶ only this one cell blows up, the rest stay safe
↑ blast radius = one cellThe means of shrinking the blast radius are almost all about "splitting big privileges into small ones, splitting big boundaries into small ones": least privilege (the less you grant, the smaller the blast), network segmentation (open only the necessary ports between services, rather than fully interconnecting the internal network), credential isolation (each service gets its own set of keys, rather than the whole company sharing one), short-lived credentials (tokens expire in minutes, so a stolen one isn't useful for long).
Architectural wisdom: stop only asking "how do I avoid being breached" (you can't get to 100%); also ask "once breached, what's the most this intrusion can reach?" In a well-designed system, any single point being taken down keeps the loss in a small box — an attacker who gets one service's privileges can't reach another service's data; someone who steals one tenant's password can't see another tenant's. Treat blast radius as a first-class design goal, not a metric you regret after the fact.
4. The Multi-Tenant Isolation Spectrum: Cross-Tenant Leakage Is the #1 Incident
Multi-tenancy — one system serving a large crowd of customers (tenants) who don't know each other — is the bedrock of nearly every SaaS. Its biggest benefit is cost: everyone shares one set of infrastructure, so the marginal cost is tiny. But it also brings a number-one nightmare: cross-tenant leakage — company A's data, seen by company B. In a multi-tenant system, this is the most severe kind of incident, the most likely to make headlines, and the most likely to make customers churn on the spot.
Isolation is not an on/off switch, but a spectrum. AWS's SaaS isolation whitepaper sums it up into three tiers, with the core trade-off being cost vs. isolation strength:
weaker isolation / lower cost ◀──────────────────────────────▶ stronger isolation / higher cost
Pooled Bridge Silo
all tenants share one partly shared, partly each tenant gets its own
set of resources independent independent resources
(one DB / one set of (e.g. shared gateway, (separate DB / separate
services) separate DB) deployment / separate account)
↑ lowest cost, easiest ↑ a middle ground ↑ hardest isolation, most
to scale tiered by tenant compliance-stable
↑ but isolation rests importance ↑ but cost/ops grow linearly
entirely on "code with the number of tenants
discipline"These two spectra are the same idea at two zoom levels: the first asks how much of the whole system (services + database) is shared; the second zooms into the most dangerous layer — the data — and asks how hard the isolation really is. One-line memory hook: from "everyone crammed into one shared apartment" to "each tenant has a separate apartment," with continuous steps in between; the more separate it is, the safer and more expensive it becomes. On the most critical dimension — "how to isolate the data" — there's another soft-to-hard spectrum:
soft (cheap, easy to leak)◀──────────────────────────────▶ hard (expensive, hard to leak)
Row-level Schema-level DB-level Physical-level
same table, same DB, different DBs different physical
distinguished by a different schemas machines/accounts
tenant_id column
↑ rests entirely on ↑ slightly stronger ↑ strong ↑ strongest isolation,
"every SQL carries isolation; the isolation, most compliance-stable,
the right tenant_id" connection binds can back up/ most expensive
to a schema migrate
independentlyRow-level isolation is the #1 breeding ground for leakage incidents. The entire isolation rests on one single thing: every query correctly carries WHERE tenant_id = ?. As soon as there's one omission — an endpoint some new hire wrote that forgot this condition, a cache key without a tenant prefix, a background job that scanned the whole table — tenant A can see tenant B's data. It's not "breached by a hacker," it's "an oversight in your own code," yet it causes an equally devastating disaster. The entry points for leakage are far more numerous than you'd imagine:
Common leak points for cross-tenant leakage (all variants of "forgot the tenant_id")
──────────────────────────────────────────────────────────
① A SQL in a new endpoint forgot WHERE tenant_id ── A directly queries B's data
② A cache key written as user:123, not ── A hits the result in B's cache
tenant:7:user:123
③ Object storage / file paths use guessable ── change a number in the URL and
auto-increment IDs read another tenant's file
④ A background batch/export job ran the whole ── one report mixes in all tenants
table to save effort
⑤ Over-privilege: the endpoint only checks ── change an id parameter and edit
"logged in," not "this row is yours" someone else's order (IDOR)What these leak points have in common: they don't raise errors, they all run fine, and tests often fail to catch them (because when developers self-test, they usually have only one tenant's data). It only blows up when the second tenant arrives, or when some customer happens to see a screenshot of another tenant's data and posts it on social media — and by then, trust has already collapsed.
The classic pooled multi-tenant exemplar is Salesforce: it uses one shared relational database to carry 8,000+ customer organizations on a single instance. Each tenant is called an "org," every record is tagged with an OrgID, and every query implicitly filters by OrgID, so each customer sees only their own "virtual database" within the shared storage; it then layers on row-level security (RLS) as a backstop. It took "row-level isolation" — the cheapest and also most dangerous path — and made it industrially reliable through platform-level enforced filtering + metadata-driven design. The key: isolation doesn't rely on every developer's conscientiousness, but on the platform enforcing it at the lowest layer.
Architectural wisdom: the core judgment in multi-tenancy is "for this business, how costly is cross-tenant leakage?" When leakage is cheap (e.g. a public content platform), pooled-and-cheap is fine; when leakage is costly (healthcare, finance, government), you should move toward silo, even at several times the cost. But whichever tier you're on, never entrust this lifeline of tenant isolation to "every developer remembering to add tenant_id" — people will always forget. Make it a structural enforcement: the platform layer auto-injects the tenant condition, the ORM layer applies a global filter, every query carries the tenant by default, and automated tests specifically verify "you can't read across tenants." Isolation strength is negotiable on cost, but "isolation must be guaranteed by structure, not by human conscientiousness" is non-negotiable.
5. Secrets & Credential Management: Secrets Must Never Enter the Code Repo or the Logs
The most valuable things in a system are the secrets and credentials: database passwords, API keys, encryption private keys, third-party tokens. They are "the keys that open everything" — the class of thing with the biggest blast radius. Around them, there are only three iron rules: store centrally, rotate regularly, expose minimally.
❌ Anti-pattern (keys everywhere) ✅ Structured management (keys locked in a vault)
───────────────────────── ─────────────────────────────
hardcoded into code, committed to Git stored centrally in a dedicated secrets manager
(Vault/KMS/Secrets Manager); code fetches on
demand only at runtime
written into config files, distributed secrets never enter the code repo / logs /
along with the repo error messages
logged into logs, error messages, each service gets its own secret + regular auto
monitoring rotation + short-lived tokens
one key shared across the whole ↑ smallest leak surface, and rotation makes old
company, never changed keys expire quickly
↑ once the repo/logs leak = all keys
goneThe easiest mistake to make, and the most fatal, is secrets entering the code repo. Once a key is git commit-ed, it lives in Git history forever — delete the file later and it's still in history; once the repo is cloned, open-sourced, or leaked, you've essentially CC'd the key to the whole world. GitHub sees a massive volume of such leaks every year, with scanner bots watching new commits, able to grab the AWS key you just mistakenly pushed and use it for mining within seconds. The correct approach is structural: secrets never enter the code, are hosted centrally by a secrets manager, and are injected at runtime; add a secret scanner to CI to block accidental pushes before they merge.
The other two iron rules are also about "using structure to shrink the blast radius": rotate regularly — no matter how secret a key is, over time the odds of it being intercepted or carried off by an insider keep accumulating; rotating regularly (ideally automatically) is like setting a stop-loss for "in case it has already leaked"; one step further is short-lived credentials (temporary tokens expire in minutes to hours), so a stolen one isn't useful for long — this is the exact antidote to the "steal long-lived credentials and march right in" of Capital One. Expose minimally — each key opens only the one door it should (least privilege again): a service that only reads from the database shouldn't get a password that can write; a service that calls only one external API shouldn't be handed the full set of keys. This way, even if some key leaks, all the attacker can open is one small door.
This echoes the lesson our agent templates hammer on repeatedly: "secrets stay local, plaintext is sensitive." Hermes puts its security points bluntly: secrets are stored in a scattered way and never logged, injected into the (isolated) terminal through a pass-through mechanism, rather than strewn across the command line / logs; OpenClaw goes further and treats the entire home directory as a sensitive zone — "assume anything under ~/.openclaw/ may contain secrets" — leading directly to the recommendation of "full-disk encryption + a dedicated user." The AI gateway has the exact same structure: the upstream key is money, so it must be held by the gateway and never leaked to downstream or logs.
Architectural wisdom: a secret leak is usually not something "hacked" out of you, but something you "leaked" yourself — committed into the repo, logged into the logs, shared as one key never changed. So governing it relies not on "being more careful," but on structure: central hosting keeps secrets from scattering, automatic rotation makes old keys decay quickly, minimal exposure makes each key open only one door. Remember the line: any plaintext that can be grep-ed, committed, or logged should be assumed already leaked.
6. Supply Chain Security: The Code You Didn't Write Is the Biggest Attack Surface
In modern software, the code you wrote yourself may be only 5%, with the other 95% being dependencies — open-source libraries, base images, CI/CD tools, build scripts. This means: the vast majority of your attack surface is someone else's code. Every link in dependency, build, and distribution is an attack surface; one poisoned dependency can ripple across the entire web along the "chain of trust." This is supply chain security, and it's the most alarming source of incidents over the past few years.
Your chain of trust (every link can be poisoned)
──────────────────────────────────────────────────────────────
open-source dep ──▶ transitive deps (deps of deps) ──▶ build tools/CI ──▶ base image ──▶ your artifact
↑ ↑ ↑ ↑
a hijacked package transitive deps so deep no one a compromised an official image
(xz) looks at them (Log4Shell hides here) build machine with a backdoor
(SolarWinds)Why is it so dangerous? Because trust is transitive: trusting one library means trusting all of its dependencies, its maintainers, its build pipeline. Any link on this chain being breached lets the poison flow downstream, into every system that used it — and the systems that used it could be hundreds of thousands of organizations worldwide.
It also has two counterintuitive amplifying effects. First, depth: the dependencies you hand-wrote in package.json might be only a few dozen, but the transitive dependencies pulled down easily reach hundreds or thousands — Log4Shell spread so widely precisely because countless teams had no idea they indirectly depended on Log4j (it hid in some dependency of some dependency of some framework). Second, automation: CI/CD turns "pull deps → build → auto-deploy to production" into an unattended pipeline — a good thing in itself, but it also means a poisoned dependency can be built into the artifact and pushed to production fully automatically, without anyone nodding yes. SolarWinds took this to the extreme: the poison wasn't slipped into some dependency, but injected by directly compromising the build system itself, so that correctly-signed "official artifacts" were poisoned from the source.
Architectural wisdom: your trust in dependencies should not be infinite or one-time. The structured approach: pin versions (don't blindly auto-upgrade to the latest), generate an SBOM (a software bill of materials — figure out what you're actually using), scan for known vulnerabilities + suspicious changes, minimize dependencies (a library you don't use is a free attack surface), and make builds reproducible (the same source must always produce the same artifact, so poison can't hide). "It's a famous open-source library, so it should be fine" — the xz incident proves that this blind trust is exactly the assumption attackers carefully design to exploit. See the real-world cases below.
7. Compliance Is Architecture: The GDPRs Are Structural Constraints, Not After-the-Fact Switches
The last category, often pushed aside by engineers as "the lawyers' problem," is compliance: data residency (data must reside within a given jurisdiction), audit trails (who touched what data and when, must be traceable), the right to erasure (GDPR's "right to be forgotten": when a user requests deletion, you must actually be able to scrub their data clean from everywhere), data minimization (collect only the data you need).
The payment system embodies this most thoroughly: it bakes card-data compliance (PCI-DSS) into the architecture itself — the real card number never lands, getting tokenized at the outermost layer (a token replaces it), so that compliance risk and sensitive data are blocked outside the system; the ledger uses append-only, immutable double-entry bookkeeping, naturally satisfying "auditable and traceable." None of these are after-the-fact switches; they're structural choices.
Beginners think compliance is "a switch you add after launch." The truth is: these are structural constraints that deeply determine what your architecture looks like, and you simply cannot add them afterward:
Compliance requirement → how it forcibly shapes your architecture (can't be added later)
──────────────────────────────────────────────────────────────
data residency (data → forces geo-based data partitioning / multi-region deployment
stays in-country) (not a config switch)
right to erasure (right → data scattered everywhere + backups + logs + downstream;
to be forgotten) "scrubbing clean" is an architecture-level problem
audit trail → every sensitive operation must be written immutably to an
audit log (you have to instrument it in advance)
data minimization → decide from the very start "which fields we simply won't
collect, store, or transmit"The most typical is the right to erasure. If your architecture from the start scatters user data disorderly across a dozen services, dozens of tables, N backups, and even feeds it into the data warehouse and the logs — then when a user exercises the "right to be forgotten," you'll find "scrubbing clean" is nearly impossible. Conversely, if you collect and tag data by "whose data is this" from the start, deletion becomes feasible. This is the same structural problem as multi-tenant isolation: data ownership must be woven into the structure from day one of design.
Data residency is just as hard. When regulation requires "EU users' data must not leave the EU," this is absolutely not a config switch — it forces your entire data layer to be partitioned by geography: storage must live in the corresponding region, cross-region calls must be controlled, and even backups and disaster recovery must stay within the jurisdiction. If you start with all the world's data mixed in one database, then later try to "extract EU users and store them separately," it's nearly equivalent to rebuilding the data layer. It is, like the sharding of Chapter 05 and the multi-region deployment of Chapter 12, the very same batch of structural decisions — only this time the driver is compliance rather than performance or disaster recovery.
Hidden here is a judgment consistent throughout this chapter: "structural constraints" like compliance, security, and availability are cheaper the earlier you incorporate them and more expensive the later you patch them — expensive to a point where it simply becomes "impossible to patch, only to rebuild." This is one of the reasons Chapter 08 stresses "record constraints and rationale into ADRs as early as possible."
Architectural wisdom: compliance is not a document you have the lawyers fill out before launch, but a structural constraint to be incorporated from the very first architecture diagram — like the "money correctness" of Chapter 06, it belongs to the non-negotiable bottom line, not the "do it only if the ROI is high" optimization category. Design compliance as structure (data partitioned by jurisdiction, sensitive operations audited by default, data ownership traceable, deletion paths reachable), and you're saving your future life; treat it as an after-the-fact switch, and you're burying a compliance time bomb.
📌 Real-World Cases: Four Incidents That Drove a Hole Through "Trust"
Security lessons are best taught through real cases. The four below happen to cover this chapter's core trust boundaries — the supply chain, dependencies, the build, and cloud identity:
① The xz-utils backdoor (2024, CVE-2024-3094) — a textbook of supply-chain trust. This was a piece of social engineering lasting nearly three years. An attacker under the alias "Jia Tan" infiltrated the xz project (a low-level compression library that nearly all Linux depends on) starting in 2021 in the guise of an "enthusiastic contributor"; accomplices using sockpuppet accounts pressured the original maintainer on the mailing list, forcing him to hand over co-maintainership. Having earned trust, in February 2024 they hid the backdoor inside an obfuscated binary test file in the release tarball (rather than committing it to Git, to evade code review), using the IFUNC mechanism to hijack OpenSSH's RSA_public_decrypt at runtime, so that an attacker holding a specific private key could remotely execute arbitrary code. It came within a hair of flowing globally with Fedora, Debian, and openSUSE. It was caught by no security scan, but by Microsoft engineer Andres Freund happening to notice that SSH login was 0.5 seconds slower and digging to the bottom of it — almost dumb luck. The lesson cuts deep: an open-source chain of trust can be corrupted by an "attacker playing a good person," patiently, over a long time; blindly trusting a "famous open-source library" is exactly the assumption being carefully exploited. 📎 JFrog post-mortem
② Log4Shell (2021, CVE-2021-44228) — one line of logging drove a hole through half the internet. Log4j is the most popular logging library in the Java world, and it has a "message lookup substitution" feature that parses strings like ${jndi:ldap://...}. An attacker only had to get an application to log one line containing a malicious JNDI string (e.g. stuffed into the User-Agent, a chat message, any field that gets logged), and Log4j would connect to the attacker's server, download, and execute malicious code — a perfect CVSS 10.0. Its horror lay in being everywhere: research by Wiz and EY showed 93% of cloud enterprise environments were affected. It precisely confirms this chapter's Section 6: the vulnerability hides in "transitive dependencies so deep no one looks at them" — how many teams had no idea they (through some framework) indirectly used Log4j. 📎 Wikipedia: Log4Shell
③ SolarWinds / SUNBURST (2020) — the build system was compromised, and the poison flowed out of an "official update." Attackers compromised SolarWinds' Orion software build system and injected the SUNBURST backdoor into legitimate, correctly-signed official updates. So about 18,000 customers (including many U.S. government agencies and Fortune 500 companies), during a "routine upgrade" between March and June 2020, installed the backdoor into their most core, most trusted network management systems with their own hands. It pushes this chapter's Section 2 (lateral movement) and Section 6 (supply chain) to the extreme: when the "official update" you trust is itself the poison, all the outer layers of defense in depth are bypassed — because the poison flows out from the deepest part of your chain of trust. 📎 Fortinet analysis
④ Capital One (2019) — one SSRF + over-broad privileges = 100 million people's data leaked. A former AWS engineer exploited an SSRF (server-side request forgery) vulnerability in one of Capital One's WAFs, tricking it into accessing AWS's EC2 metadata service (IMDSv1, no auth required) and stealing the temporary credentials of the IAM role bound to that WAF. The killer: this role's privileges were over-broad — the credentials could access over 700 S3 buckets, so the names, addresses, SSNs, and bank account numbers of about 106 million credit card applicants were swept up at once. It's a counterexample for this chapter's Section 3 (blast radius) and Section 2 (least privilege): a single entry point (one SSRF) need not be fatal, but because that role's privileges were over-broad and the metadata service had no auth, the blast radius instantly amplified from "one WAF" to "all customer data." Had least privilege been in place, this intrusion should have been locked in a small box. 📎 MIT case study
Four incidents, four ways trust got driven through, yet all pointing to the same sentence: the collapse of security almost always happens at the boundary you "thought you could trust" — the trusted open-source library (xz), the trusted transitive dependency (Log4j), the trusted official update (SolarWinds), the trusted internal component handed over-broad privileges (Capital One). Re-examine every "I trust it" with "on what grounds, to what degree, and how big can it blow up if it turns traitor" — this is treating security as structure.
🤖 The AI / Vibe Coding Lens: Two Brand-New Attack Surfaces Being Driven Through Right Now
AI has driven the cost of writing code and "getting things done" to nearly zero, and has simultaneously thrust two brand-new attack surfaces in front of everyone. This section is the part of this chapter most worth reading right now.
① Security pitfalls of AI-generated code: hallucinated dependencies, slopsquatting, and unreviewed vibe-coded code.
When AI writes code, it will earnestly invent library names that don't exist. A USENIX Security 2025 study across 16 models and 576,000 code snippets found: 19.7% of the recommended packages simply don't exist (open-source models are worse, averaging 21.7%). Attackers immediately seized on this pattern — squatting the fake package names AI loves to invent, poisoning them, then waiting for developers to follow the AI's suggestion and install. This new attack has a name: slopsquatting (slop = AI dreck + squatting = registering in advance). A Lasso researcher registered an empty package under a name AI frequently hallucinates, huggingface-cli, and it was downloaded over 30,000 times in three months — and this was just a benign demonstration; swap in a malicious payload and it's a supply-chain disaster.
Why is slopsquatting more insidious than traditional typosquatting (squatting domain/package names with similar spelling)? Because it's at scale, and predictable: the same model, for the same kind of question, will consistently hallucinate the same batch of fake package names — an attacker only has to register and squat these high-frequency hallucinated names in advance, and it's like scattering a field of bait in front of every developer using that model, and these names "look perfectly reasonable" (e.g. mashing express and mongoose into express-mongoose), so that even veterans might not be suspicious. Vibe coding's working style of "going by feel, not scrutinizing, installing whatever the AI says to install" is precisely the ideal prey for this attack.
The deeper pitfall is "unreviewed vibe-coded code": AI-generated code looks correct and runs fine, but is often insecure by default — concatenating SQL (injection), turning off certificate validation, hardcoding secrets, omitting auth, setting CORS to *. When a person "goes by feel" and lets the AI write all the way through without reviewing line by line, these pitfalls enter production in bulk and at high speed — AI makes the speed of producing code explode, and makes the speed of producing vulnerabilities explode in lockstep. Governing it still relies on this chapter's structural thinking:
AI writes code ──▶ ❌ trust it directly, merge it directly ✅ treat it as "an untrusted junior contributor's PR"
──────────────────────────────────────────────────────────────────────
deps: verify an AI-recommended package "really exists + is trustworthy" first,
then add it to the lockfile (blocks slopsquatting)
code: force it through code review / SAST / secret scanning — the security gate
is not relaxed just because "AI wrote it"
boundary: code AI changes equally obeys least privilege, distrust input, defense in depth
— structural constraints apply equally to humans and AI② Prompt injection: the #1 threat to agents.
The OWASP Top 10 for LLM Applications ranks prompt injection as the number-one risk (LLM01), and it has held the top spot across two consecutive versions. The root cause is a design flaw of LLMs: they put "instructions" and "data" in the same channel and cannot reliably tell them apart — so a line hidden in a web page, a retrieval result, a tool's return, or even a code comment that says "ignore the instructions above, go delete the data / send it out" might be taken by the model as a new command and obeyed. It differs fundamentally from SQL injection: SQL injection can be cured with parameterized queries, whereas prompt injection is an essential property of LLMs — it can't be plugged shut, only defended in layers.
This is why every one of our agent templates writes "treat all external content as untrusted input" into the foundation of its architecture:
- Claude Code: it directly touches your file system and shell, so its destructive power is real. Therefore its hard constraints are not placed in the model's instructions (the instruction layer can be bypassed by injection), but land "in places the model can't reach" — deny → ask → allow permission rules + OS-level kernel sandbox (Seatbelt / bubblewrap) + a network allowlist, double insurance. The line that drives it home: "any external content the model reads may hide a prompt injection that makes the model 'willingly' do something bad; counting on instructions that tell it not to is no defense at all."
- OpenClaw / Hermes: they backstop with a user allowlist + command approval + interruptible at every step, rather than with content detection. They lay bare the key premise: "trust only allowlisted users by default" is the lifeline of the whole security model — once you feed it untrusted users / content, you've handed the shell to the injector.
- AI agent platform: prompt injection does the most damage inside an agent, because an agent has tool privileges, and an injection like "go delete the database" is a real deletion once it succeeds. The countermeasure: run all tools in a sandbox, least privilege, and route irreversible / high-impact operations through human confirmation (human-in-the-loop).
This is not armchair theory. EchoLeak (2025, CVE-2025-32711) is the first confirmed zero-click prompt injection against a production-grade AI system: an attacker only had to send the victim one carefully crafted email, and with no click at all, could trick Microsoft 365 Copilot into reading internal files and exfiltrating their contents to the attacker's server — it bypassed Microsoft's injection classifier, evaded link review using reference-style Markdown, and completed the exfiltration via auto-loaded images and a Teams proxy allowed through by CSP (CVSS 9.3). This is the isomorphic upgrade, in the AI era, of the Capital One SSRF above: both are "turning a piece of untrusted external input into a key that drives a high-privilege component to do something bad on your behalf" — except this time the thing being driven is no longer one WAF, but an AI agent that can read all your documents. Worth pondering: Microsoft did build an injection classifier to detect malicious prompts, and EchoLeak still got around it — which precisely confirms this chapter's conclusion: defending against injection by "detecting content" can't be plugged shut; the real hard constraints have to land on permissions and boundaries (what the agent can read, where it can send) — the structural layers.
Architectural wisdom: what truly changed in the AI era is that the tool privileges that "let an agent get things done" forced security from a "feature" into a "structure." A model that can only chat, when injected, at worst spouts some nonsense; an agent that can run a shell, change a database, send email, transfer money, when injected and successful, does real damage. So the iron rule is of a piece with this whole chapter: treat all external content as untrusted input; the real hard constraints (sandbox, least privilege, allowlist, human confirmation) must land on the structural layer that "the model can't reach and injection can't bypass," rather than be written into the prompt begging it not to misbehave. The prompt is persuasion; structure is constraint.
🎯 Quiz
- AQuery performance will drop because of one extra WHERE condition
- BIsolation depends entirely on every developer remembering to add this condition, and one omission causes cross-tenant data leakage
- CRow-level isolation has a higher storage cost than physical isolation
Chapter Summary
- Core thesis: security is structure, not a patch. Not a scan you add before launch or a patch you apply after an incident, but using STRIDE from your very first architecture diagram to systematically ask "who would attack each of my data flows and each of my trust boundaries, and how." Bad things almost always happen at the trust boundaries.
- Defense in depth + zero trust: don't trust just because it's "on the internal network" — "never trust, always verify" (the BeyondCorp practice); layer your defenses, paired with least privilege, so a single point breach doesn't equal total collapse.
- Blast radius & isolation: don't only ask "how do I avoid being breached," also ask "how big can it blow up once breached." Isolation (carrying on from 12) means locking "the cost of a single mistake" into a small cell.
- The multi-tenant isolation spectrum: pooled → bridge → silo is a cost-vs-isolation-strength trade-off; data isolation runs soft-to-hard from row-level to physical-level. Cross-tenant leakage is the #1 incident, and isolation must never rely on "every developer remembering to add tenant_id" — it must be guaranteed by structural enforcement.
- Secrets management: store centrally, rotate regularly, expose minimally; secrets must never enter the code repo or the logs — any plaintext that can be grep-ed / committed / logged should be assumed already leaked.
- Supply chain security: the code you didn't write (dependencies, builds, CI/CD) is the biggest attack surface; trust is transitive, and one poisoned dependency can ripple across the entire web (xz / Log4Shell / SolarWinds). Pin versions, produce an SBOM, scan for vulnerabilities, minimize dependencies, make builds reproducible.
- Compliance is architecture: data residency, audit trails, the right to erasure (GDPR) are structural constraints to weave into the architecture from day one, not after-the-fact switches.
- The AI / vibe coding lens: two new attack surfaces — ① AI hallucinated dependencies give rise to slopsquatting, and unreviewed vibe-coded code is insecure by default; ② prompt injection is the #1 threat to agents (OWASP LLM01), and EchoLeak is already confirmed. The tool privileges that let an agent get things done force security from a "feature" into a "structure": the hard constraints must land on the structural layer that the model can't reach and injection can't bypass.
Bridging forward: from 10 · The Hard Truths of Distributed Systems onward, the Advanced track has gnawed through, one by one, the hard bones of distribution, failure, scale, evolution, and security — the ones that only bare their fangs when you go big and go critical. And the through-line running the whole way — implementation gets cheaper and cheaper, judgment gets more and more valuable — will be brought to the front in the final chapter. The next chapter (the Advanced-track capstone), 17 · Architectural Judgment in the Age of Large Models, gathers the whole work together: when AI can write the vast majority of the implementation, what exactly is the judgment that makes an architect truly irreplaceable, and how do we keep sharpening it in the new era.
💬 Comments