Lesson 05 / 15
Attack Surface
Converting surface into a countable unit: in a 13-node, 21-edge access graph, four entry points to two targets produce 35 separate paths; the entry point carrying only 1.4% of total legitimate work opens 37.1% of the paths; a single edge can close 14 paths; one edge stops 4 legitimate work items and closes no path at all; greedy narrowing by ratio zeroes the paths at a cost of 31 legitimate work items, and closing all entry points reaches the same result at a cost of 27.
Published Updated
In this lesson
- Models attack surface as a set of paths in an access graph and runs the path count
- Measures, for every edge, both the number of paths it closes and the number of legitimate work items it stops
- Calculates the cost of reducing paths to zero and compares two reduction strategies
Contents
Every measurement in the previous lesson stood on a single assumption: that access is one step. All twelve of the twelve chains started from that step, and that step was never opened up. But reaching an asset is not passing through a single door. This lesson opens that step up and establishes the unit of measure the rest of the curriculum will stand on.
Attack surface is the whole of the entry points that can be used to reach an asset, plus the crossings between them. “The surface is wide” carries no decision. The surface’s countable counterpart is the count of attack paths: how many separate sequences go from source to target. From this lesson on, every defense will be measured by the same question — how many paths does it close, how many remain, how much legitimate work stops.
Surface Is a Path Count
The graph below is a model: there is no real network, no real hostname, no real service endpoint. Running a count like this on a real network without written authorization and a defined scope is both illegal and unprofessional.
SC21: every edge is an access crossing and carries the count of fictional legitimate-work items that pass through it per day. SC22: a path is a sequence from the source to a target that visits no node twice; the path count below is actually run with the depth-first walk that follows. SC23: when an edge is removed, every path that goes through it closes, and the legitimate work that edge carried stops.
// security/graph.mjs — fictional measurement network's access graph (model): there is no real // network, real hostname, or real service endpoint. The graph is directed and acyclic. No randomness. // SC21: an edge is written "source target legitimate-work"; legitimate work is the count of // fictional work items passing through that edge per day. export const EDGE = ` outer-network branch-endpoint 12 outer-network field-app 9 outer-network shared-interface 4 outer-network management-endpoint 2 branch-endpoint network-gateway 12 field-app network-gateway 9 shared-interface billing 4 management-endpoint network-gateway 2 management-endpoint validator 1 network-gateway collector 15 network-gateway billing 6 network-gateway work-order 5 network-gateway report-endpoint 4 collector validator 15 collector measurement-store 14 validator measurement-store 13 validator billing 7 billing measurement-store 5 billing identity-store 3 work-order validator 2 work-order identity-store 4` .trim().split("\n").map((r) => { const [a, b, work] = r.trim().split(/\s+/); return { from: a, to: b, work: +work, id: `${a}->${b}` }; }); export const SOURCE = "outer-network"; export const TARGET = ["measurement-store", "identity-store"]; export const NODE = [...new Set(EDGE.flatMap((e) => [e.from, e.to]))]; // SC22: a path is a sequence from the source to a target that visits no node twice; the path // count below is actually run with the depth-first walk that follows. export function paths(edges = EDGE) { const adj = {}; for (const e of edges) (adj[e.from] ||= []).push(e.to); const found = [], trail = [SOURCE], seen = new Set([SOURCE]); (function walk(d) { if (TARGET.includes(d)) return found.push([...trail]); for (const n of adj[d] || []) { if (seen.has(n)) continue; seen.add(n); trail.push(n); walk(n); trail.pop(); seen.delete(n); } })(SOURCE); return found; } export const PATH = paths(); export const remainingPaths = (removed) => paths(EDGE.filter((e) => !removed.includes(e.id))).length; export const stoppedWork = (removed) => EDGE.filter((e) => removed.includes(e.id)).reduce((s, e) => s + e.work, 0); export const TOTAL_WORK = EDGE.reduce((s, e) => s + e.work, 0);
// security/surface.mjs — the surface is a path count: how many paths, what closes them, how much legitimate work stands. import { EDGE, NODE, TARGET, PATH, SOURCE, TOTAL_WORK, remainingPaths, stoppedWork } from "./graph.mjs"; const write = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("")); const len = PATH.map((p) => p.length - 1); const entry = [...new Set(EDGE.filter((e) => e.from === SOURCE).map((e) => e.to))]; console.log(`${NODE.length} nodes, ${EDGE.length} edges, ${entry.length} entry points, ` + `${TARGET.length} targets; path count ${PATH.length}, path length ` + `${Math.min(...len)}..${Math.max(...len)}, total legitimate work ${TOTAL_WORK}`); const A = [-21, 9, 10, 11]; console.log("\n1. where paths enter, where they exit"); write(A, "point", "paths", "shortest", "longest"); for (const e of entry) { const p = PATH.filter((x) => x[1] === e); write(A, e, p.length, Math.min(...p.map((x) => x.length - 1)), Math.max(...p.map((x) => x.length - 1))); } for (const t of TARGET) { const p = PATH.filter((x) => x.at(-1) === t); write(A, t, p.length, Math.min(...p.map((x) => x.length - 1)), Math.max(...p.map((x) => x.length - 1))); } const B = [-38, 11, 9, 8, 11]; console.log("\n2. if an edge is removed (path/work ratio, high to low)"); write(B, "edge", "leg. work", "closed", "left", "path/work"); const measured = EDGE.map((e) => ({ ...e, closes: PATH.length - remainingPaths([e.id]) })); for (const e of [...measured].sort((x, y) => y.closes / y.work - x.closes / x.work)) write(B, e.id, e.work, e.closes, PATH.length - e.closes, (e.closes / e.work).toFixed(2)); const C = [-36, 11, 12, 14, 13]; console.log("\n3. greedy narrowing: each round removes the highest path/work edge"); write(C, "removed edge", "leg. work", "paths left", "work stopped", "work left %"); const removed = []; while (remainingPaths(removed) > 0) { const pick = EDGE.filter((e) => !removed.includes(e.id)) .map((e) => ({ e, closes: remainingPaths(removed) - remainingPaths([...removed, e.id]) })) .sort((x, y) => y.closes / y.e.work - x.closes / x.e.work)[0]; removed.push(pick.e.id); write(C, pick.e.id, pick.e.work, remainingPaths(removed), stoppedWork(removed), (((TOTAL_WORK - stoppedWork(removed)) / TOTAL_WORK) * 100).toFixed(1)); } const D = [-30, 12, 14, 13]; console.log("\n4. three ways to shrink the surface"); write(D, "approach", "paths left", "work stopped", "work left %"); const entryEdge = EDGE.filter((e) => e.from === SOURCE).map((e) => e.id); const free = measured.filter((e) => e.closes === 0).map((e) => e.id); for (const [name, set] of [["nothing", []], ["edges that close zero paths", free], ["all entry points", entryEdge], ["greedy pick", removed]]) write(D, name, remainingPaths(set), stoppedWork(set), (((TOTAL_WORK - stoppedWork(set)) / TOTAL_WORK) * 100).toFixed(1)); console.log(`edge that closes zero paths: ${free.join(", ") || "none"}`); const E = [-21, 16, 14, 14, 14]; console.log("\n5. what counts as the surface measure (if an entry point is removed)"); write(E, "entry point", "share of pts %", "paths closed", "path share %", "work share %"); for (const e of entry) { const edge = EDGE.find((x) => x.from === SOURCE && x.to === e); const closes = PATH.length - remainingPaths([edge.id]); write(E, e, (100 / entry.length).toFixed(1), closes, ((closes / PATH.length) * 100).toFixed(1), ((edge.work / TOTAL_WORK) * 100).toFixed(1)); }
13 nodes, 21 edges, 4 entry points, 2 targets; path count 35, path length 3..6, total legitimate work 148 1. where paths enter, where they exit point paths shortest longest branch-endpoint 10 4 6 field-app 10 4 6 shared-interface 2 3 3 management-endpoint 13 3 6 measurement-store 21 3 6 identity-store 14 3 6 2. if an edge is removed (path/work ratio, high to low) edge leg. work closed left path/work outer-network->management-endpoint 2 13 22 6.50 management-endpoint->network-gateway 2 10 25 5.00 work-order->validator 2 9 26 4.50 billing->identity-store 3 11 24 3.67 management-endpoint->validator 1 3 32 3.00 network-gateway->work-order 5 12 23 2.40 billing->measurement-store 5 11 24 2.20 validator->billing 7 14 21 2.00 outer-network->field-app 9 10 25 1.11 field-app->network-gateway 9 10 25 1.11 network-gateway->billing 6 6 29 1.00 outer-network->branch-endpoint 12 10 25 0.83 branch-endpoint->network-gateway 12 10 25 0.83 network-gateway->collector 15 12 23 0.80 work-order->identity-store 4 3 32 0.75 collector->validator 15 9 26 0.60 validator->measurement-store 13 7 28 0.54 outer-network->shared-interface 4 2 33 0.50 shared-interface->billing 4 2 33 0.50 collector->measurement-store 14 3 32 0.21 network-gateway->report-endpoint 4 0 35 0.00 3. greedy narrowing: each round removes the highest path/work edge removed edge leg. work paths left work stopped work left % outer-network->management-endpoint 2 22 2 98.6 work-order->validator 2 16 4 97.3 billing->identity-store 3 11 7 95.3 billing->measurement-store 5 6 12 91.9 work-order->identity-store 4 4 16 89.2 network-gateway->collector 15 0 31 79.1 4. three ways to shrink the surface approach paths left work stopped work left % nothing 35 0 100.0 edges that close zero paths 35 4 97.3 all entry points 0 27 81.8 greedy pick 0 31 79.1 edge that closes zero paths: network-gateway->report-endpoint 5. what counts as the surface measure (if an entry point is removed) entry point share of pts % paths closed path share % work share % branch-endpoint 25.0 10 28.6 8.1 field-app 25.0 10 28.6 6.1 shared-interface 25.0 2 5.7 2.7 management-endpoint 25.0 13 37.1 1.4
Numbers belong to the measurement class; their inputs are SC21–SC23.
Thirty-Five Paths
Thirteen nodes and twenty-one edges produce 35 paths. This is the number the surface will use in this lesson and through the rest of the curriculum. Neither node count nor edge count can stand in for it: if edges grow by 5%, the path count can double, because path count depends on how the edges combine.
The first table separates where paths enter and where they exit. 21 paths go to
measurement-store, 14 to identity-store. On the entry side, though, there is an unexpected
distribution: branch-endpoint and field-app each carry ten paths, shared-interface only two,
management-endpoint thirteen — the highest of the four. The management endpoint is the
network’s smallest gate, and it opens the most paths, because it connects to two of the graph’s
middle nodes at once.
The shortest path is three steps and leaves from two endpoints — shared-interface and
management-endpoint; the longest is six steps.
The length distribution changes where a defense should go: a three-step path has two points to
place an intermediate control, a six-step path has five.
An Edge’s Two Numbers
The second table is this lesson’s central measurement, and it names every edge with two numbers: how many paths it closes, how many legitimate work items it stops. The ordering is by the ratio between them.
The top row is the outer-network->management-endpoint edge: it closes 13 paths and stops only
2 legitimate work items — a ratio of 6.50. This is not the edge that closes the most paths; that
is validator->billing, which closes 14 paths but stops 7 legitimate work items (ratio 2.00).
Judged by “closes the most” alone, the second edge wins; once the cost is counted too, the first
is three times as efficient.
The bottom of the table shows the opposite direction. collector->measurement-store closes only 3
paths and stops 14 legitimate work items — a ratio of 0.21. This edge is the network’s actual job:
readings get written to the store through it. Removing it narrows the surface almost not at all,
and stops a tenth of the work.
The last row is the item every surface-reduction list should look for.
network-gateway->report-endpoint closes no path at all — 35 paths stay 35 — and stops 4
legitimate work items. The reporting endpoint connects to no target, so it does not sit on any
path. In a simplification made without counting the surface, this edge could get removed as an
“open end”; once measured, what shows up is a decision that has a cost and a return of zero.
What Counts as the Surface Measure
The fifth table shows why no other measure can substitute for path count. Each of the four entry points is 25% of the entry-point count; the share of paths they close is, respectively, 28.6%, 28.6%, 5.7%, and 37.1%. Four gates of the same size carry shares of the surface that differ by six and a half times.
A surface assessment made by counting entry points writes these four items as equal and produces
the sentence “we have four open ends.” An assessment that counts edges falls into the same error:
paths coming through shared-interface use two edges, so do paths starting at
management-endpoint, but one opens two paths and the other thirteen. The one countable thing is
combination, and the count of combination is the path.
The last column reads the same table in the opposite direction. management-endpoint carries only
1.4% of total legitimate work and opens 37.1% of the paths; branch-endpoint carries 8.1%
of the work and 28.6% of the paths. Reading the two ratios together gives the order for surface
reduction directly: the entry point carrying the least work while opening the most paths comes
first. The three columns gathered through this lesson — paths closed, paths remaining, cost in
legitimate work — turn, for the first time in this table, into a ranking rule.
The Cost of Reducing to Zero
The third table tries to reduce the paths to zero: each round removes the highest-ratio edge and the paths are counted again. After six edges, the paths remaining are zero and the legitimate work stopped is 31; 79.1% of the network’s work stays standing.
The order is instructive on its own. The first removal takes 35 paths down to 22, the second to
16, the third to 11. The first three edges close 68.6% of the paths at a total cost of 7
legitimate work items. The cost paid for the remaining eleven paths is 24 legitimate work items.
The return on surface reduction is concentrated in the first steps; closing the last paths gets
exponentially more expensive. The last row is the clearest form of this: network-gateway->collector
alone stops 15 legitimate work items and closes the four paths that remain.
The fourth table compares the greedy choice against two alternatives. Removing the edges that close zero paths spends 4 legitimate work items and closes none of the 35 paths. Cutting all entry points off from the outer network zeroes the paths and stops 27 legitimate work items. The greedy choice by ratio also zeroes the paths, but stops 31 legitimate work items.
This last comparison is this lesson’s most striking result: choosing the edge that looks most efficient at every step gives a more expensive total in the end. The greedy choice spends the cheap edges in the first rounds and ends up having to remove an expensive one. The cheapest complete cut is not an edge ranking but a cut selection — which set of edges together separates the source from the targets. When a defense list is ordered item by item on efficiency, unless what the chosen items do together is counted, the total cost comes out four units higher.
Summary
- Attack surface’s countable counterpart is path count: in a 13-node, 21-edge graph, 35 paths run from four entry points to two targets, with lengths between 3 and 6 steps.
- Each of the four entry points is 25% of the entry-point count, but they open 28.6%, 28.6%, 5.7%, and 37.1% of the paths; the entry point carrying the least legitimate work (1.4%) carries the most paths. Counting entry points or edges does not measure the surface.
- Every edge is named with two numbers: the edge that closes the most paths is 14 paths / 7 work (ratio 2.00), the most efficient is 13 paths / 2 work (ratio 6.50), and among the path-closing edges the least efficient is 3 paths / 14 work (ratio 0.21).
- One edge stops 4 legitimate work items and closes no path; because it connects to no target, it does not sit on any path.
- The first three removals close 68.6% of the paths at a cost of 7 legitimate work items; the cost paid for the remaining eleven paths is 24 legitimate work items.
- Zeroing the paths costs 31 legitimate work items by greedy selection and 27 by cutting all entry points; the most efficient choice item by item is not the cheapest choice overall.
Next Step
This topic established security’s units of measure. The three axes were overlapped on the same asset, asset, threat, and vulnerability were split into separate lists, risk was ranked as the composition of probability and impact, the chain from vulnerability to impact was broken into steps, and this lesson reduced surface to a single number: 35 paths. Every question now has a unit.
Yet through all of these measurements, one thing was never actually done. Paths were counted, removing edges was tried, and its cost was written down — but removal is not a defense; it is stopping work. A real defense leaves the edge in place and puts a control on top of it: adds a layer, narrows privilege, splits into compartments, removes location-based trust, separates authentication from authorization, turns defaults off. Each of these principles can be run over the same 35 paths, and the same three numbers can be written for each: how many paths closed, how many remain, how much legitimate work stopped. The next topic — Defense Principles — measures six principles one at a time with these same three columns.
To keep your progress and take notes, Log in
My notes
Log in to take notes.