---
title: Isolation
source: 'https://academia.sh/en/courses/introduction-to-cybersecurity/isolation'
course: 'Introduction to Cybersecurity'
language: en
updated: '2026-09-13T05:08:03+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

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

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.

```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
// 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 };
}
```

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

```js
// 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?

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