Defense in Depth
Design so that no single control failing is enough to lose the asset — because every control will eventually fail, and the question that matters is what the next layer does about it.
Frame the problem
Security starts with a concrete asset, attacker capability and trust crossing.
Assume the layer above failed
The design question is not "is this control good?" but "what happens when this control does not run?" A control does not run for boring reasons: a new endpoint missed the middleware, a refactor moved the check, a feature flag skipped it, a library upgrade changed escaping behaviour, an engineer added a code path in an incident. Every layer of a real system has a nonzero rate of this happening.
Defense in depth means the layers are chosen so their failures are *independent*. Two controls that fail for the same reason are one control. An authorization check in middleware and an authorization check in the controller both fail if the route is registered without middleware; an authorization check in the application and a row-level policy in the database fail for different reasons, which is what makes the pair worth having.
The layers below are ordered by how much of the attack they see. Nothing here is optional-because-the-one-above-exists; each one is what saves you on the day the one above is skipped.
- Authentication — establishes who. Fails to: stolen tokens, credential stuffing, session fixation.
- Authorization — decides whether this principal may act on this resource. Fails to: a missing ownership check on a new endpoint.
- Input validation — rejects malformed and out-of-range values at the edge. Fails to: a path that parses input somewhere else.
- Restricted service identity — the workload can only call what it needs. Turns "arbitrary code execution in the worker" into "arbitrary code execution with two S3 prefixes".
- Restricted database role — the query can only touch its own tables and verbs. Turns SQL injection into a read of the product catalogue.
- Network segmentation and egress control — the compromised host cannot reach the admin network or send data out. Turns a foothold into a dead end.
- Audit logging — you can answer what happened. Turns an unbounded incident into a scoped one.
Depth is not repetition
Adding more of the same control is not depth, and it is the most common way teams convince themselves they have it. Three input-validation libraries in a row are one layer with extra latency. A WAF in front of an application that also validates input adds depth only against attacks the application misses — and adds a false sense of security if the team treats it as the reason validation can be sloppy.
Real depth changes the *type* of control between layers. Application code enforces intent; the database enforces schema and privilege; the network enforces reachability; the cloud IAM layer enforces capability; the audit log enforces knowability. Each is implemented by a different system, run by a different mechanism, and fails for different reasons.
Depth also has a cost curve that flattens. The first three layers buy enormous risk reduction. The seventh usually buys operational complexity, more places for a legitimate request to be wrongly blocked, and more configuration to drift. Choose depth for the assets whose loss is unacceptable, and accept a thinner stack elsewhere — which is exactly the judgment Risk, Residual Risk and Honest Reporting is about.
| Layer | Shallow system | Layered system |
|---|---|---|
| SQL injection found in a report endpoint | Query runs as superuser | Query runs as reports_reader |
| What the attacker reads | Every table, including users and payments | Two reporting views |
| Can they write? | Yes — UPDATE, DELETE, DROP | No — role has SELECT only |
| Can they exfiltrate? | Yes — direct outbound connection | Blocked by egress allowlist |
| Do you find out? | On the breach notification | Denied-egress alert within minutes |
| Scope of the incident | Assume total compromise | Two views, bounded and provable |
Key points
- Design each layer for the case where the layer above did not run — that is the only case where it matters.
- Layers must fail independently; two controls that fail for the same reason are one control.
- Depth changes the *kind* of control (application → database → network → IAM → audit), not the quantity of the same one.
- A WAF or scanner adds depth only if the team does not treat it as permission to skip the real control.
- Depth costs complexity and false positives; buy it where the asset justifies it and be explicit where you did not.
Boundary control exercise
This lesson uses the shared boundary-control exercise.
Follow the attack
Safe conceptual simulation: capability → missing control → crossed boundary → asset impact.
- 1Attacker → exploit the first control gap: a missing authorization check, an unescaped output, a stale dependency.
- 2Gap → capability: read a resource, run a query, execute code in a worker.
- 3Capability → next layer: whatever the compromised component is *permitted* to do next decides whether the attack continues.
- 4Layer by layer → asset: in a shallow system this is one step; in a layered one, each step needs a new, independent failure.
- Without depth, the blast radius of any single bug is the whole system, and incident scoping becomes "assume everything".
- With depth, the impact statement is specific and provable, which changes both the remediation cost and the notification obligation.
Defend, detect, recover
One prevention is a single point of security failure. Layer it and make failure observable.
- • Enforce authorization at the resource, and back it with a database-level constraint or row policy where the data model allows.
- • Scope service identities and database roles so that code execution in one component is not access to everything.
- • Restrict egress so that exfiltration requires defeating a second, differently-implemented control.
- • Make the secure path the default in the framework — a route that forgets the auth decorator should fail closed, not open.
- • Instrument each layer separately so you can see *which* one stopped something; a layer that never fires is either unnecessary or not wired up.
- • Alert on denials at inner layers — an inner-layer denial means an outer layer was already bypassed.
- • Track "requests that reached layer N" as a metric; a change in that distribution after a deploy usually means a layer was skipped.
- • Identify which layers held, because that is what bounds the incident.
- • Fix the failed layer, then ask why the next layer did not also stop it — an incident that got through two layers is a design problem, not a bug.
- • Add the missing layer as part of the fix rather than only patching the specific bug.
- • Layers can share a hidden dependency — a single identity provider, one configuration system, one network — and fail together.
- • Each added layer adds a way to wrongly block legitimate traffic, and outages caused by security controls erode the will to keep them.
- • Depth does not help against a legitimate identity performing a legitimate-looking action, which is the insider and the successful phish.