Skip to content
academia.sh

Lesson 06 / 15

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.

Published Updated

In this lesson

  • 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
Contents

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.

// 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)));
// 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.

// 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']],
];
// 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.

// 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.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close