Skip to content
academia.sh

Lesson 08 / 15

Isolation

The same network is built with seven separate compartment schemes, and the reached item is counted when a node falls. Gate count, the daily legitimate request passing through the gate, and how much the compartment count narrows the reached asset are all measured together; the location of the boundary is shown to be more decisive than the compartment count.

Published Updated

In this lesson

  • Distributes nodes into compartments and derives the gates between compartments from legitimate flows
  • Counts the reached item when a node falls: compares a flat network with a compartmentalized one using the same measure
  • Separates compartment count from the location of the compartment boundary and measures each one's effect on reached asset
  • Finds the node isolation cannot narrow and derives the reason from the legitimate flow set
Contents

The previous lesson removed permission items one by one and closed forty-three paths for the cost of eleven daily requests. The whole measurement rested on one assumption: what closes is a step, the nodes stay in place. In a real incident, what closes is not a step but a node — a service gets compromised, and from that moment the attacker moves from where that service stood. Where they can go from there is decided not by the permission set but by the shape of the network.

Isolation is dividing nodes into compartments (compartmentalization) and restricting the crossing between compartments. The Containers course covered the same concept through runtime boundaries; what is measured here is not the mechanism but the outcome: how many items are reached when one compartment falls. This lesson’s three columns are the reached item, the gate count, and the daily legitimate request passing through the gate.

  • DP12. Movement inside a compartment is free. A fallen node reaches every node in its own compartment; this models the fact that a flat network also has undeclared connections between nodes.
  • DP13. Crossing between compartments happens only through a gate. The gate set is derived from legitimate flows: if a flow’s step crosses a compartment boundary, that step is opened as a gate.
  • DP14. A gate is open for a fallen node in the direction it was opened, too. The gate itself carries no control; if it did, that would be what the previous two lessons already measured.
  • DP15. The reached item is the number of nodes the fallen node can reach; the node itself is not counted. The asset column counts the four protected items among these separately.
  • DP16. The compartment schemes are chosen model decisions. In reality a compartment boundary is drawn together with network topology, physical placement, and operational boundaries; this lesson measures only the boundary’s outcome.

Compartment Schemes and Gates

Four schemes group the same node set in different ways: a single compartment, an outer/inner split, a four-layer split, and one compartment per node. None of the schemes change the graph; the only thing that changes is which crossing cuts a boundary. A step that cuts a boundary stays open as a gate if it belongs to a legitimate flow, and closes if it does not — meaning the compartmentalization decision is set by the legitimate flow list. This dependency is also the source of the result waiting at the end of the lesson.

// 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)));
// compartment.mjs — compartment schemes and the reached-item count when a node falls
import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs';
export const NODE = [...new Set(EDGE.flat())];
export const INTERMEDIATE = NODE.filter((d) => !ENTRY.includes(d) && !ASSET.includes(d));
const SERVICE = ['collector', 'queue', 'validator', 'billing', 'work-order'];
const build = (f) => Object.fromEntries(NODE.map((d) => [d, f(d)]));
export const SCHEME = [
  ['single-compartment', build(() => 'network')],
  ['two-compartment', build((d) => (ENTRY.includes(d) ? 'outer' : 'inner'))],
  ['four-compartment', build((d) => (ENTRY.includes(d) ? 'outer'
    : SERVICE.includes(d) ? 'service' : d === 'management' ? 'management' : 'data'))],
  ['per-node', build((d) => d)],
];
const LEGITIMATE = new Set(FLOW.flatMap(([, d]) => steps(d)));
// Free movement inside a compartment; crossing between compartments only through an open gate.
export function resolve(map) {
  const cross = EDGE.map(([a, b]) => edgeName(a, b)).filter((k) => map[k.split('>')[0]] !== map[k.split('>')[1]]);
  const gate = cross.filter((k) => LEGITIMATE.has(k)), member = {}, neighbor = new Map();
  for (const d of NODE) (member[map[d]] ||= []).push(d);
  for (const g of gate) {
    const [a, b] = g.split('>');
    neighbor.set(map[a], [...(neighbor.get(map[a]) || []), map[b]]);
  }
  const reached = (d) => {
    const seen = new Set([map[d]]), stack = [map[d]];
    while (stack.length) for (const x of neighbor.get(stack.pop()) || []) if (!seen.has(x)) { seen.add(x); stack.push(x); }
    return [...seen].flatMap((b) => member[b]).filter((x) => x !== d);
  };
  return { gate, closed: cross.filter((k) => !LEGITIMATE.has(k)), reached };
}
// run1.mjs — scheme by scheme: gate count, reached item, and legitimate request through the gate
import { FLOW, ASSET, steps } from './network.mjs';
import { SCHEME, INTERMEDIATE, resolve } from './compartment.mjs';

const avg = (a) => (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1);
console.log('scheme               compartments  gate  closed  avg item  worst  avg asset  gate request');
for (const [nm, map] of SCHEME) {
  const { gate, closed, reached } = resolve(map);
  const re = INTERMEDIATE.map(reached);
  const gr = FLOW.filter(([, d]) => steps(d).some((k) => gate.includes(k))).reduce((s, [, , n]) => s + n, 0);
  console.log(`${nm.padEnd(21)}${String(new Set(Object.values(map)).size).padEnd(14)}${String(gate.length).padEnd(7)}` +
    `${String(closed.length).padEnd(8)}${avg(re.map((s) => s.length)).padEnd(11)}` +
    `${String(Math.max(...re.map((s) => s.length))).padEnd(9)}` +
    `${avg(re.map((s) => s.filter((x) => ASSET.includes(x)).length)).padEnd(12)}${gr}`);
}
scheme               compartments  gate  closed  avg item  worst  avg asset  gate request
single-compartment   1             0      0       13.0       13       4.0         0
two-compartment      2             7      1       9.0        9        4.0         2018
four-compartment     4             16     2       8.2        9        4.0         3513
per-node             14            20     2       4.8        8        2.8         3513

The first row is the measure of the network without compartments. Whichever service falls, all thirteen of the other nodes and all four of the four assets are reached. On a flat network, the answer to “which service was compromised” does not change the outcome; every compromised node opens the same radius.

The second row draws the first compartment boundary, and the average reached item drops from thirteen to nine — a thirty-one percent narrowing. But the asset column does not move: still four. The boundary separated the entry points from the inside, but every asset stayed inside. Seven gates opened, and two thousand eighteen requests a day now pass through them. The cost of isolation was paid, the count of protected items did not change.

The third row is this lesson’s most expensive row. Four compartments require sixteen gates, and the whole of the daily legitimate request, three thousand five hundred thirteen, passes through at least one gate. In exchange, the average reached item drops from nine to eight point two, and the asset count is still four. Nine extra gates bought zero point eight of an item. Increasing the compartment count alone does not produce isolation.

The last row shows what actually works. When every node is in its own compartment, the average reached item drops to four point eight, the reached asset to two point eight. The asset column only moves here. The difference comes not from the compartment count but from the granularity of the gates: in the four-compartment scheme, one gate opening from the service compartment into the data compartment opened every asset in that compartment; in the per-node scheme, the same gate opens onto only one asset.

Radius per Fallen Node

The average alone is misleading. When the same measurement is done node by node, it becomes visible which node narrows and which does not.

// run2.mjs — reached item per fallen node; how it narrows as the scheme changes
import { ASSET } from './network.mjs';
import { SCHEME, INTERMEDIATE, resolve } from './compartment.mjs';

const c = SCHEME.map(([nm, h]) => [nm, resolve(h)]);
console.log(`fallen node   ${c.map(([nm]) => nm.split('-')[0].padEnd(9)).join('')}`);
for (const d of INTERMEDIATE)
  console.log(`${d.padEnd(14)}${c.map(([, x]) => String(x.reached(d).length).padEnd(9)).join('')}`);
console.log('\nthe same rows counting only assets:');
for (const d of INTERMEDIATE)
  console.log(`${d.padEnd(14)}${c.map(([, x]) => String(x.reached(d).filter((y) => ASSET.includes(y)).length).padEnd(9)).join('')}`);
const last = c[3][1];
console.log(`\nwidest radius in the per-node scheme: ${INTERMEDIATE.map((d) => [d, last.reached(d).length]).sort((a, b) => b[1] - a[1])[0].join(' -> ')} items`);
fallen node   single   two      four     per      
collector     13       9        8        6        
work-order    13       9        8        6        
management    13       9        9        8        
queue         13       9        8        5        
validator     13       9        8        2        
billing       13       9        8        2        

the same rows counting only assets:
collector     4        4        4        3        
work-order    4        4        4        3        
management    4        4        4        4        
queue         4        4        4        3        
validator     4        4        4        2        
billing       4        4        4        2        

widest radius in the per-node scheme: management -> 8 items

The first two columns are completely flat: in the no-compartment and two-compartment schemes, it does not matter which node falls. The distinction only opens up in the last column. When the validator or billing falls, the reached item drops from thirteen to two; these two services sit near the end of the graph, and behind them there is only the asset they write to. The measurement collector and the work-order service drop to six, the queue to five.

The management station does not narrow. Nine items in the four-compartment scheme, eight in the per-node scheme — and four in the asset column, meaning every asset is reached regardless of scheme. The reason is where the gate set comes from: the management station’s legitimate work already touches the collector, the validator, the work-order service, and the configuration store. Because gates are derived from legitimate flows, a node whose legitimate work is wide also gets wide gates.

This is isolation’s structural limit: compartmentalization narrows nodes whose legitimate work is narrow, not ones whose work is wide. The node whose compromise would cost the most is the node that benefits least from isolation. The same observation can be turned into a decision — the way to narrow the management node’s radius is not adding a compartment, it is splitting that node’s legitimate work: putting the subject that does maintenance and the subject that changes configuration on separate nodes splits eight items into two smaller, separate sets.

The schemes’ cost must be read together with this table. The per-node scheme requires twenty gates, and each of these gates is a hand-written item that has to be updated whenever legitimate flow changes. The whole of the daily three thousand five hundred thirteen requests passes through at least one gate; every gate is both an operational item and a point of failure. This is the price of bringing the average reached item down from thirteen to four point eight, and that price shows up nowhere unless an incident happens.

The Location of the Boundary

If the compartment count alone is not decisive, where the boundary is drawn is. Three schemes answer the same question differently: should the boundary be drawn around the assets, between the assets, or between the services?

// run3.mjs — the location of the boundary: what changes when the same compartment count is placed differently
import { FLOW, ENTRY, ASSET, steps } from './network.mjs';
import { NODE, INTERMEDIATE, resolve } from './compartment.mjs';

const build = (f) => Object.fromEntries(NODE.map((d) => [d, f(d)]));
const avg = (a) => (a.reduce((s, x) => s + x, 0) / a.length).toFixed(1);
const PLACEMENT = [
  ['around-assets', build((d) => (ASSET.includes(d) ? 'data' : 'rest'))],
  ['per-asset', build((d) => (ASSET.includes(d) ? d : 'rest'))],
  ['per-service', build((d) => (ASSET.includes(d) ? 'data' : ENTRY.includes(d) ? 'outer' : d))],
];
console.log('scheme            compartments  gate  avg item  avg asset  gate request');
for (const [nm, map] of PLACEMENT) {
  const { gate, reached } = resolve(map), re = INTERMEDIATE.map(reached);
  const gr = FLOW.filter(([, d]) => steps(d).some((k) => gate.includes(k))).reduce((s, [, , n]) => s + n, 0);
  console.log(`${nm.padEnd(18)}${String(new Set(Object.values(map)).size).padEnd(14)}${String(gate.length).padEnd(7)}` +
    `${avg(re.map((s) => s.length)).padEnd(11)}${avg(re.map((s) => s.filter((x) => ASSET.includes(x)).length)).padEnd(12)}${gr}`);
}
scheme            compartments  gate  avg item  avg asset  gate request
around-assets     2             7      13.0       4.0         2039
per-asset         5             7      13.0       4.0         2039
per-service       8             20     6.0        4.0         3513

The first two rows give zero gain. Putting every asset in a single compartment and giving every asset its own separate compartment both leave the average reached item at thirteen and the reached asset at four. The reason comes from the gate rule: because all the services stay in the same compartment, falling in one reaches all of them, and because the gate to every asset is open from that same shared compartment, every asset is reached too. The five-compartment scheme is no different from the two-compartment scheme — three extra boundaries were drawn, no path narrowed. Reporting isolation by compartment count is completely misleading here.

The third row shows a gain, but not the expected one. Separating the services brings the average reached item down from thirteen to six; lateral movement genuinely narrowed. The reached asset is still four, because every service gets a gate into the shared data compartment for its own legitimate work, and movement is free inside that compartment. Twenty gates and the whole of the daily request were paid, and the count of protected assets did not change.

Read together, the three rows make the rule visible: a service boundary narrows lateral movement, an asset boundary narrows asset access, and neither substitutes for the other. The reason the per-node scheme brings the reached asset down to two point eight is that it does both at once. An isolation decision is measured not by how many compartments were built, but by which pair of items the boundary was placed between.

Summary

  • On a flat network, whichever service falls, all 13 other nodes and all 4 assets are reached. On a network without compartments, “which node was compromised” does not change the outcome.
  • Two compartments bring the average reached item down from 13 to 9 (31%), but the reached asset stays at 4; the count of protected assets did not change for the cost of 7 gates and 2,018 daily requests.
  • Four compartments bring the average down from only 9 to 8.2 for the cost of 16 gates and the whole of the daily request. Increasing the compartment count alone does not produce isolation.
  • The reached asset only moves in the per-node compartment scheme: average 4 down to 2.8. What is decisive is not the compartment count but the gate’s granularity.
  • The management station is the least-narrowed node: 13 items fall only to 8, and all 4 assets stay reachable in every scheme. Because gates are derived from legitimate flow, a node whose legitimate work is wide cannot benefit from isolation.
  • The per-node scheme’s cost is 20 hand-managed gates and 3,513 daily requests passing through them; this cost is invisible unless an incident happens.

Next Step

This lesson divided the network into compartments and counted how many items a fallen compartment reaches. Every compartment ran on the same silent rule: movement inside a compartment is free. This is also why the reached item count started at thirteen — on a flat network, being inside was enough to pass. Adding compartments did not remove this rule, it only shrank the area where the rule applies; in every scheme, being inside was still a privilege, and gates carried that privilege from compartment to compartment. The next lesson’s question is the rule itself: if trust based on location is removed entirely and every step is verified separately regardless of where it came from, what happens to the path count, where does the reached item land, and how many extra steps per daily request does verifying every request separately cost.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close