---
title: 'Defense in Depth'
source: 'https://academia.sh/en/courses/introduction-to-cybersecurity/defense-in-depth'
course: 'Introduction to Cybersecurity'
language: en
updated: '2026-09-13T05:08:03+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Counts the paths from entry point to asset in the access graph and builds the path set'
    - 'Adds layers in sequence and measures the new path each one closes: the remaining path and exception cost are written together'
    - 'Finds the layer that closes no new path and computes its cost in managed items'
    - 'Shows that layer order changes the distribution of closures without changing the total closed path'
---

# Defense in Depth

Five defense layers are added in sequence over the same access graph: the new path each layer closes, the path left over, the legitimate flow needing an exception, and the managed item count are all counted. A layer that closes no new path is found, and how order changes the distribution of closures is shown.

The previous topic turned the attack surface into a countable unit: how many separate paths
there are to reach an asset got written down. What was counted was the path left open, and no
defense was run against that number yet. This topic takes the same unit of measure and runs
each of six defense principles against it in turn. Every lesson gives three columns: how many
paths there are, how many the principle closes, and how much legitimate work stands in exchange.

The network model is extended at one point. The surface count counted paths to two targets; for
the defense principles to be measurable, the network's four protected assets and the vendor
maintenance connection also enter the model, because which asset a principle protects is only
visible once assets are counted separately. The extended model is the same fictional network and
is rebuilt below; all six lessons that follow run on this single path set.

The first principle is **defense in depth**: instead of relying on a single control, independent
layers are stacked on top of each other, so the next one still stands when one is bypassed. This
carries no decision — it answers neither how many layers, what order, nor what cost. This lesson
adds layers to the same graph in sequence and counts the **new path each one closes**. The count
here measures a defense's scope; running it on a real network without **written authorization
and a defined scope** cannot be done — doing so is both illegal and unprofessional.

- **DP1.** The network is fictional: the regional measurement network's four entry points, six
  intermediate nodes, and four assets are modeled with `node`. What is measured is this model;
  this extended graph replaces the narrowed graph from the surface count and does not change
  through the topic.
- **DP2.** A path is a step sequence from an entry point to an asset with no repeated node.
- **DP3.** Which step each layer stops was not measured, it was chosen. In a real network this
  mapping is worked out by testing; the lesson's point is not the mapping itself but the count
  run on top of it.
- **DP4.** A layer either stops a step or it does not. Partial blocking and the possibility of
  evasion are not modeled; these would lower the count of closed paths, not change its direction.
- **DP5.** Legitimate flow passes through the same graph's edges. If a layer stops a step of a
  legitimate flow, that flow requires a hand-written exception; the exception count and the daily
  requests it affects are the measure of the cost.
- **DP6.** The managed item count is a given model value for each layer: the total of rule,
  mapping, and exclusion lines that layer's maintenance requires.

## Protected Network and Path Set

The model separates three node types: externally reached entry points, services in between, and
protected assets. Edges are the steps that can be crossed from one node to another.

```js
// network.mjs — fictional regional measurement network's access graph and legitimate flows (model)
export const ENTRY = ['field-endpoint', 'branch-network', 'public-endpoint', 'maintenance-endpoint'];
export const ASSET = ['measurement-store', 'invoice-db', 'subscriber-registry', 'configuration'];
export const EDGE = [
  ['field-endpoint', 'collector'], ['field-endpoint', 'work-order'], ['branch-network', 'collector'],
  ['branch-network', 'work-order'], ['branch-network', 'management'], ['public-endpoint', 'collector'],
  ['maintenance-endpoint', 'management'], ['maintenance-endpoint', 'queue'], ['collector', 'queue'],
  ['collector', 'measurement-store'], ['queue', 'validator'], ['queue', 'billing'],
  ['validator', 'measurement-store'], ['validator', 'invoice-db'], ['billing', 'invoice-db'],
  ['billing', 'subscriber-registry'], ['work-order', 'queue'], ['work-order', 'subscriber-registry'],
  ['management', 'collector'], ['management', 'validator'], ['management', 'work-order'],
  ['management', 'configuration'],
];
// Legitimate flow: work that runs daily; the step sequence passes through the same graph's edges.
export const FLOW = [
  ['field-reading', ['field-endpoint', 'collector', 'queue'], 1440],
  ['validation', ['queue', 'validator', 'measurement-store'], 1440],
  ['public-reading', ['public-endpoint', 'collector', 'measurement-store'], 300],
  ['branch-query', ['branch-network', 'collector', 'measurement-store'], 210],
  ['invoice-write', ['queue', 'billing', 'invoice-db'], 48],
  ['work-order-opening', ['branch-network', 'work-order', 'subscriber-registry'], 32],
  ['work-order-queuing', ['branch-network', 'work-order', 'queue'], 32],
  ['subscriber-update', ['billing', 'subscriber-registry'], 6],
  ['configuration-change', ['branch-network', 'management', 'configuration'], 2],
  ['maintenance-session', ['maintenance-endpoint', 'management', 'collector'], 1],
  ['queue-maintenance', ['maintenance-endpoint', 'queue'], 1],
  ['reconciliation', ['management', 'validator', 'invoice-db'], 1],
];
export const edgeName = (a, b) => `${a}>${b}`;
export const steps = (d) => d.slice(0, -1).map((x, i) => edgeName(x, d[i + 1]));
export function paths(closed = new Set()) {
  const neighbor = new Map();
  for (const [a, b] of EDGE) if (!closed.has(edgeName(a, b))) neighbor.set(a, [...(neighbor.get(a) || []), b]);
  const out = [];
  const walk = (d, y) => {
    if (ASSET.includes(d)) return void out.push(y);
    for (const k of neighbor.get(d) || []) if (!y.includes(k)) walk(k, [...y, k]);
  };
  for (const g of ENTRY) walk(g, [g]);
  return out;
}
export const blocked = (closed) => FLOW.filter(([, d]) => steps(d).some((k) => closed.has(k)));
```

```js
// run1.mjs — the path set is built: how many separate paths from an entry point to an asset
import { ENTRY, ASSET, EDGE, FLOW, paths } from './network.mjs';

const y = paths();
const intermediate = [...new Set(EDGE.flat())].filter((d) => !ENTRY.includes(d) && !ASSET.includes(d));
console.log(`entry ${ENTRY.length}, intermediate node ${intermediate.length}, asset ${ASSET.length}, edge ${EDGE.length}, path ${y.length}`);
const count = (f) => y.reduce((m, p) => m.set(f(p), (m.get(f(p)) || 0) + 1), new Map());
const print = (b, m) => { console.log(`\n${b}`); for (const [k, v] of m) console.log(`  ${String(k).padEnd(22)}${v}`); };
print('paths per entry point', count((p) => p[0]));
print('paths per asset', count((p) => p.at(-1)));
print('paths by step count', new Map([...count((p) => p.length - 1)].sort()));
print('paths through intermediate node', new Map(intermediate.map((d) => [d, y.filter((p) => p.includes(d)).length])
  .sort((a, b) => b[1] - a[1])));
console.log(`\nlegitimate flow ${FLOW.length}, daily legitimate request ${FLOW.reduce((s, [, , n]) => s + n, 0)}`);
```

```
entry 4, intermediate node 6, asset 4, edge 22, path 55

paths per entry point
  field-endpoint        10
  branch-network        23
  public-endpoint       5
  maintenance-endpoint  17

paths per asset
  measurement-store     17
  invoice-db            22
  subscriber-registry   14
  configuration         2

paths by step count
  2                     7
  3                     12
  4                     20
  5                     16

paths through intermediate node
  queue                 40
  management            26
  collector             25
  validator             24
  work-order            20
  billing               20

legitimate flow 12, daily legitimate request 3513
```

Fourteen nodes and twenty-two edges produce fifty-five paths. The path count is two and a half
times the edge count; every edge added to the graph is not a path on its own but a multiplier of
every path it lies on. The branch network alone is the start of twenty-three paths, because it
connects to three separate intermediate nodes. The maintenance endpoint has only two edges yet
opens seventeen paths: the management station it connects to is itself connected to four more
nodes. An entry point's danger is measured not by how many connections it has, but by how many
connections the node it connects to has.

The asset distribution shows a second thing. Twenty-two paths go to the invoice database, two to
the configuration store. Both sit on the same network, but one is reachable from eleven times as
many paths as the other. The step distribution, in turn, determines where a defense should be
placed: only seven of the paths are two steps long, thirty-six are four steps or more. Long
paths pass through most of the intermediate nodes, meaning a single control sitting in the
middle cuts all of them at once.

The last table states directly where a control belongs. The queue lies on forty of the
fifty-five paths; a control stopping every step into the queue touches seventy-three percent of
the paths alone. The management station carries twenty-six paths, the measurement collector
twenty-five. At the low end, the work-order and billing services carry the least load, twenty
paths each. These numbers rank placement, but with one warning: the bottleneck is also the
bottleneck for legitimate work. A control on the queue sits in the path of one thousand four
hundred forty daily field-reading uploads and as many daily validation requests.

## Path Closed Layer by Layer

Five layers are added to the same graph in sequence. Each layer stops specific steps; a path
counts as closed once at least one of its steps has been stopped.

```js
// layer.mjs — five defense layers and the step each one stops (model decision)
export const LAYER = [
  ['network-filter', 22, ['field-endpoint>work-order', 'branch-network>management', 'maintenance-endpoint>management', 'maintenance-endpoint>queue']],
  ['authentication', 14, ['public-endpoint>collector', 'branch-network>management', 'maintenance-endpoint>management', 'management>configuration']],
  ['endpoint-control', 9, ['field-endpoint>work-order', 'branch-network>management', 'maintenance-endpoint>queue']],
  ['input-validation', 11, ['queue>billing', 'validator>invoice-db']],
  ['data-control', 7, ['validator>measurement-store', 'management>validator']],
];
```

```js
// run2.mjs — layers are added in sequence; the new path each one closes is counted
import { paths, blocked } from './network.mjs';
import { LAYER } from './layer.mjs';

const total = paths().length, closed = new Set();
let previous = total;
console.log('layer              item   alone       newly closed  left   exception  daily extra step');
for (const [k, item, edges] of LAYER) {
  const alone = total - paths(new Set(edges)).length;
  edges.forEach((e) => closed.add(e));
  const left = paths(closed).length, cut = blocked(closed);
  console.log(`${k.padEnd(19)}${String(item).padEnd(7)}${String(alone).padEnd(12)}${String(previous - left).padEnd(14)}` +
    `${String(left).padEnd(7)}${String(cut.length).padEnd(11)}${cut.reduce((s, [, , n]) => s + n, 0)}`);
  previous = left;
}
console.log('\npath remaining after five layers:');
for (const p of paths(closed)) console.log(`  ${p.join(' > ')}`);
```

```
layer              item   alone       newly closed  left   exception  daily extra step
network-filter     22     35          35            20     3          4
authentication     14     31          5             15     4          304
endpoint-control   9      22          0             15     4          304
input-validation   11     32          9             6      6          353
data-control       7      14          3             3      7          1793

path remaining after five layers:
  field-endpoint > collector > measurement-store
  branch-network > collector > measurement-store
  branch-network > work-order > subscriber-registry
```

The first layer closes thirty-five of the fifty-five paths and its cost is almost nothing: three
legitimate flows need an exception, and together they total four daily requests. This is why the
network filter looks cheap — the flows that run often already sit on edges that pass through the
filter.

In the following layers the ratio reverses. Authentication closes five new paths, but the daily
requests needing an exception jump from four to three hundred four, because the public-reading
endpoint falls under this layer. Data control closes only three paths; in exchange, one thousand
seven hundred ninety-three daily requests gain an extra step — the entire validation flow passes
through this layer. The cost paid per closed path is one-ninth of a request in the first layer,
five hundred ninety-seven requests in the last.

The exception column builds up a separate burden of its own. By the end of the five layers,
seven of the twelve legitimate flows depend on a hand-written exception — fifty-eight percent of
the flows have been carved out of some control. When an exception is removed or written wrong, a
path counted as closed reopens, but the table does not show this — the table assumes the
exceptions are correct.

The last three rows are this lesson's hard finding. After five layers, fifty-two of the
fifty-five paths are closed, three are still open. The three remaining paths go from the field
endpoint to the measurement store, from the branch network to the measurement store, and from
the branch network to the subscriber registry — that is, the legitimate channel itself. Adding a
layer does not close these three; closing them means stopping the work. Defense in depth does
not zero out the path count, it narrows it down to the width of the legitimate channel.

## The Layer Assumed Free

The third row in the table reads zero: endpoint control closes no new path. The same row's left
side says it closes twenty-two paths on its own. The two numbers do not contradict each other;
they say the layer's value depends on order.

```js
// run3.mjs — how the closed path shifts when the order changes, and what happens when a layer is removed
import { paths } from './network.mjs';
import { LAYER } from './layer.mjs';

const total = paths().length, short = (a) => a.split('-')[0];
function run(list) {
  const closed = new Set(), m = new Map();
  let previous = total;
  for (const [a, , edges] of list) {
    edges.forEach((e) => closed.add(e));
    const left = paths(closed).length;
    m.set(a, previous - left); previous = left;
  }
  return [m, previous, list.reduce((s, [, n]) => s + n, 0)];
}
const ep = LAYER.find(([a]) => a.startsWith('endpoint-'));
console.log(`order                ${LAYER.map(([a]) => short(a).padEnd(16)).join('')}left  item`);
for (const [label, list] of [['as written', LAYER],
  ['endpoint first', [ep, ...LAYER.filter((k) => k !== ep)]],
  ['endpoint removed', LAYER.filter((k) => k !== ep)]]) {
  const [m, left, item] = run(list);
  console.log(`${label.padEnd(21)}${LAYER.map(([a]) => String(m.get(a) ?? '-').padEnd(16)).join('')}` +
    `${String(left).padEnd(7)}${item}`);
}
const alone = LAYER.reduce((s, [, , k]) => s + total - paths(new Set(k)).length, 0);
console.log(`\nsum of alone totals ${alone}, actually closed ${total - run(LAYER)[1]}, ` +
  `per item ${((total - run(LAYER)[1]) / run(LAYER)[2]).toFixed(2)} path`);
```

```
order                network         authentication  endpoint        input           data            left  item
as written           35              5               0               9               3               3      63
endpoint first       13              5               22              9               3               3      63
endpoint removed     35              5               -               9               3               3      54

sum of alone totals 134, actually closed 52, per item 0.83 path
```
All three rows arrive at the same result: the remaining path is three in every case. Changing
the order only changes the distribution of the closing. When endpoint control is moved to the
front, twenty-two paths are credited to it and thirteen to the network filter; left at the end,
it gets zero. How many paths a layer "closes" is not a number on its own — it depends on what
came before it.

The third row delivers the real verdict. When endpoint control is removed entirely, the
remaining path is still three, but the managed item count drops from sixty-three to fifty-four.
On this path set, the layer buys nothing in exchange for nine items of maintenance load. The
sentence "adding one more layer does no harm" is falsified here by the numbers: the cost of
adding it is not zero, it is nine items.

The decision is not this short, though. The measurement assumed the network filter stays
standing. If the filter is bypassed, endpoint control is the layer closing twenty-two paths on
its own, and the remaining path is then far more than three. The correct reading: this layer
does not buy coverage, it buys **redundancy** — and that redundancy costs nine items. The sum in
the row above is the numerical counterpart of the same fallacy: standalone measurements sum to
one hundred thirty-four, what actually closes is fifty-two. A report that counts the same path
twice shows coverage as two and a half times larger than it is.

The model's silent assumption also shows up here. Every layer was taken as independent: one
falling does not affect another. In reality most layers are configured through the same
management plane and administered by the same identities. If a subject who can change the
network filter's rule table can also change endpoint control's exclusion list, the two layers
count as one, and the table's fifty-two is an upper bound. This is the managed item count's
second meaning: sixty-three items is not only maintenance load, it is also **sixty-three places
that can be misconfigured**. Defense in depth is measured not by layer count but by how many
genuinely separate control surfaces the layers actually spread across.

## Summary

- The fictional network carries twenty-two edges across four entry points, six intermediate
  nodes, and four assets; 55 separate paths run from entry point to asset. 22 paths go to the
  invoice database, 2 to the configuration store — reachability differs elevenfold on the same
  network.
- When five layers are added in sequence, 52 of the 55 paths close, 3 stay open. The remaining
  three paths are the legitimate channel itself; adding a layer does not close them.
- The cost per closed path grows as the layers progress: the network filter closes 35 paths with
  4 daily exception requests, data control closes 3 paths with 1,793 daily extra steps.
- Endpoint control closes 0 new paths in its actual position but closes 22 paths on its own; the
  layer's count depends not on itself but on what precedes it.
- When the layer is removed, the remaining path is still 3, and managed items drop from 63 to
  54. The layer buys redundancy, not coverage, and that redundancy costs 9 items.
- The sum of what the layers close on their own is 134, what actually closes is 52. Counting the
  overlap inflates the defense's apparent coverage by 2.5 times.

## Next Step

This lesson placed layers on the network and counted how many paths each one closes. Every
closed path closed because a step **could not be crossed**: the filter did not let it through,
the identity was not recognized, the input was not accepted. But there is a second condition for
crossing a step in the graph — the **permission** of the subject taking that step to touch the
target item. None of the five layers looked at that permission; all of them checked the step
itself. The next lesson's question is the permission set: how many paths open when the
permission items granted to each subject are translated into a path set, how many paths close
when permissions are narrowed down to only what is actually used, and in exchange for that
narrowing, which legitimate flow stops, and at how many requests per day.
