Lesson 10 / 20
Network Segmentation and the Demilitarized Zone
Four layouts are run over an eight-zone graph: flat network, perimeter control alone, segmentation without a demilitarized zone, and full segmentation; each one counts the open path count, the item reached at the first hop and transitively from the outside zone, the zone reached when one zone falls, and the legitimate flow missed when rules were written.
Published Updated
In this lesson
- Counts the open path and the item reached from the outside zone separately for four layouts in the zone graph
- Measures how the demilitarized zone changes the item reached at the first hop and the number of boundaries to the data zone
- Compares the zone reached when one zone falls in the flat and segmented layouts: names the path that closes and the zone that does not
Contents
The previous lesson counted what the box standing at the boundary could see, and assumed that every unproven claim entered the network behind it. That assumption itself is this lesson’s subject: was “the network behind it” a single thing? All eight flow classes stood on the same flat plane; a message reaching the meter collector could reach the billing interface with the same move. Segmentation does not change the number of claims the box proves; it changes the number of paths along which a claim can be presented.
Three things are measured. How many directed pairs are open in the zone graph, how many are reached when one zone falls, and how many rule lines legitimate cross-zone traffic needs to keep running. Address planning and subnet arithmetic at cloud-network scale were measured in another course; the axis here is not the address, but the zone graph itself.
- PD7. The eight zones, item counts, and rule list belong to a fictional regional measurement
network. No real traffic flows; what is measured is the graph model built with
node. - PD8. No constraint within a zone was assumed: whoever reaches an item in a zone reaches every item of that zone. In reality intra-zone controls can also be written; that lowers the item count, it does not change the direction of the between-zone measurement.
- PD9. The outside zone was counted as a destination, not a pass-through point. Leaving an inner zone to the outside does not mean reaching everywhere the outside zone can reach.
- PD10. Rule lines were counted at the level of the directed zone pair. In reality each line also states a port and a protocol; that grows the line count, it does not change the direction of the comparison.
- PD11. The phrase “a zone falls” names only the situation in which an item in that zone passes outside of control. How it does so was neither modeled nor written.
- PD12. The two legitimate flows missed when rules were written were placed into the model by hand. In reality this number depends on the observation period and on how rarely the flow runs.
Zone Graph and Four Layouts
The model holds zones as nodes and the allowed directed pairs as edges. Four layouts do the same work and differ along only two axes: whether there is a boundary between the inner zones, and whether the public-facing items sit in a separate zone. When the public-facing items stand apart, that zone is a demilitarized zone; when they do not, the same items sit inside the application zone.
// zone.mjs — zone graph, item counts and four layouts; a model, not a real network const COMMON = { outside: 0, branch: 120, field: 60, data: 6, management: 5, backup: 3 }; export const ITEM = { // whether public-interface items sit in a separate zone or the same zone as the application separate: { ...COMMON, dmz: 4, application: 12 }, inside: { ...COMMON, application: 16 }, }; // Legitimate flows with rules written: allowed directed zone pair under default-deny export const LEGITIMATE = [ ['outside', 'dmz'], ['dmz', 'application'], ['field', 'dmz'], ['branch', 'dmz'], ['branch', 'application'], ['application', 'data'], ['application', 'outside'], ['data', 'backup'], ['backup', 'outside'], ...['dmz', 'branch', 'field', 'application', 'data', 'backup'].map((b) => ['management', b]), ]; // Two legitimate flows missed when rules were written: quarter-end reconciliation and branch archive export const MISSED = [['field', 'data'], ['branch', 'backup']]; const unique = (l) => [...new Set(l.map((e) => e.join('>')))].map((s) => s.split('>')); const fold = (l) => unique(l.map(([a, b]) => [a === 'dmz' ? 'application' : a, b === 'dmz' ? 'application' : b])) .filter(([a, b]) => a !== b); const flatEdge = (zl) => { // no constraint between inner zones, outbound direction free const inner = zl.filter((z) => z !== 'outside'); return [...inner.flatMap((x) => inner.filter((y) => y !== x).map((y) => [x, y])), ...inner.map((z) => [z, 'outside'])]; }; const Z8 = ['outside', 'dmz', 'branch', 'field', 'application', 'data', 'backup', 'management']; const Z7 = Z8.filter((z) => z !== 'dmz'); export const LAYOUT = { // [zones, edge, rules written, item map] flat: [Z7, [...flatEdge(Z7), ['outside', 'application']], 0, 'inside'], perimeter: [Z8, [...flatEdge(Z8), ['outside', 'dmz']], 1, 'separate'], nodmz: [Z7, fold(LEGITIMATE), fold(LEGITIMATE).length, 'inside'], segmented: [Z8, LEGITIMATE, LEGITIMATE.length, 'separate'], }; export function reached(edge, start) { // transitive access; outside is a destination, not a pass-through point const seen = new Set(), queue = [start]; while (queue.length) { const z = queue.shift(); if (z === 'outside' && start !== 'outside') continue; for (const [x, y] of edge) if (x === z && !seen.has(y)) { seen.add(y); queue.push(y); } } seen.delete(start); return seen; } export function distance(edge, start, target) { // how many zone boundaries are crossed to reach the target let layer = new Set([start]), n = 0; const seen = new Set([start]); while (layer.size) { n++; const next = new Set(); for (const [x, y] of edge) if (layer.has(x) && !seen.has(y)) { seen.add(y); next.add(y); } if (next.has(target)) return n; layer = next; } return '-'; } export const path = (zl, edge) => zl.reduce((a, z) => a + reached(edge, z).size, 0); export const itemsOf = (set, h) => [...set].reduce((a, z) => a + (ITEM[h][z] ?? 0), 0); export const firstHop = (edge, start) => new Set(edge.filter(([x]) => x === start).map(([, y]) => y)); export const direct = (edge, [a, b]) => edge.some(([x, y]) => x === a && y === b);
Two measurement functions walk over the graph. Transitive access starts from one zone and, following edges, collects every zone reached; the distance function makes the same walk layer by layer and gives how many boundaries are crossed to reach the target. The item counts are read from two maps, because the four public-interface items sit either in their own zone or inside the application zone, and the total of two hundred ten items stays the same either way. What changes place is not the item count, but which boundary the item sits behind.
The Path Each Layout Opens
// run1.mjs — open path, item reached from the outside zone, and cut legitimate flow in four layouts import { LAYOUT, LEGITIMATE, MISSED, reached, distance, path, itemsOf, firstHop, direct } from './zone.mjs'; console.log(`legitimate flow with rules written ${LEGITIMATE.length}, missed when rules were written ${MISSED.length}\n`); console.log(`${'layout'.padEnd(11)} ${'zones'.padStart(5)} ${'rules'.padStart(5)} ${'open/all paths'.padStart(15)} ` + `${'first-hop item'.padStart(16)} ${'transitive item'.padStart(16)} ${'boundary to data'.padStart(17)} ${'legitimate cut'.padStart(15)}`); for (const [name, [zl, edge, rules, h]] of Object.entries(LAYOUT)) { const total = zl.length * (zl.length - 1); console.log(`${name.padEnd(11)} ${String(zl.length).padStart(5)} ${String(rules).padStart(5)} ` + `${`${path(zl, edge)}/${total}`.padStart(15)} ${String(itemsOf(firstHop(edge, 'outside'), h)).padStart(16)} ` + `${String(itemsOf(reached(edge, 'outside'), h)).padStart(16)} ${String(distance(edge, 'outside', 'data')).padStart(17)} ` + `${String(MISSED.filter((m) => !direct(edge, m)).length).padStart(15)}`); } console.log('\nzones reached from the outside zone:'); for (const [name, [, edge]] of Object.entries(LAYOUT)) console.log(` ${name.padEnd(11)} ${[...reached(edge, 'outside')].join(', ')}`); const cut = MISSED.filter((m) => !direct(LAYOUT.segmented[1], m)); console.log(`\nlegitimate flow cut in the segmented layout: ${cut.map(([a, b]) => `${a} -> ${b}`).join(', ')}`);
legitimate flow with rules written 15, missed when rules were written 2 layout zones rules open/all paths first-hop item transitive item boundary to data legitimate cut flat 7 0 42/42 16 210 2 0 perimeter 8 1 56/56 4 210 2 0 nodmz 7 12 23/42 16 25 2 2 segmented 8 15 31/56 4 25 3 2 zones reached from the outside zone: flat application, branch, field, data, backup, management perimeter dmz, branch, field, application, data, backup, management nodmz application, data, backup segmented dmz, application, data, backup legitimate flow cut in the segmented layout: field -> data, branch -> backup
The second row counts this lesson’s most expensive misconception. In the perimeter layout the public-interface items were moved into a separate zone, a single rule allowing access from outside only to that zone was written, and the items the outside zone reaches at the first hop dropped from sixteen to four. Against that, the transitive item count stayed at two hundred ten and open paths came out at a full fifty-six. Because there is no boundary between the inner zones, the side that reaches one of the four public-interface items reaches the whole of the six inner zones. A demilitarized zone by itself is not a zone; it is a first hop — without a boundary behind it, it only delays the first step.
The third and fourth rows show the same segmentation in two separate layouts. Both bring the transitive item count from two hundred ten down to twenty-five; the difference is at the first hop. When the public-interface items are left inside the application zone, a connection from the outside zone lands directly next to sixteen items and, because there is no boundary within the zone, reaches all of them. Moved to a separate zone, that number drops to four and the number of boundaries that must be crossed to reach the data zone rises from two to three. The entire gain of the separate zone is these two numbers: twelve items and one boundary.
The rule column gives the cost. The flat layout has no rules at all; the segmented layout needs fifteen lines, and these fifteen lines are nothing but zone pairs — once ports and protocols are added, each line splits into several. The column on the right gives a sneakier cost: segmentation cuts two legitimate flows. The quarter-end reconciliation ran from the field zone to the data zone, the branch archive from the branch zone to the backup zone; neither occurred to anyone while rules were being written, because both run rarely. On the flat network these flows had never appeared at all, because they needed no rule.
There is a reverse-reading trap in the open-path column. The segmented layout shows thirty-one open paths, the layout without a demilitarized zone twenty-three; going by the number alone, the second looks narrower. The difference comes from the zone count: an eight-zone graph has fifty-six directed pairs, a seven-zone graph forty-two. Adding a zone grows the pair count. The path count therefore cannot be compared on its own; what can be compared is the item the path lands on, and that column reads twenty-five in both layouts.
When a Zone Falls
// run2.mjs — zones and items reached when a zone falls; flat and segmented layouts side by side import { LAYOUT, reached, itemsOf } from './zone.mjs'; const [Z, FLAT] = [LAYOUT.flat[0], LAYOUT.flat[1]]; const SEG = LAYOUT.segmented[1]; const inner = (s) => [...s].filter((z) => z !== 'outside'); console.log(`${'fallen zone'.padEnd(11)} ${'flat zone'.padStart(9)} ${'flat item'.padStart(9)} ` + `${'seg. zone'.padStart(13)} ${'seg. item'.padStart(13)} reached in segmented layout`); let [td, ts] = [0, 0]; for (const z of Z.filter((x) => x !== 'outside')) { const [f, s] = [inner(reached(FLAT, z)), inner(reached(SEG, z))]; td += f.length; ts += s.length; console.log(`${z.padEnd(11)} ${String(f.length).padStart(9)} ${String(itemsOf(f, 'inside')).padStart(9)} ` + `${String(s.length).padStart(13)} ${String(itemsOf(s, 'separate')).padStart(13)} ${s.join(', ') || '-'}`); } console.log(`\nzone total reached flat ${td} segmented ${ts} closed ${td - ts} (${Math.round((td - ts) * 100 / td)}%)`); const d = inner(reached(SEG, 'dmz')), m = inner(reached(SEG, 'management')); console.log(`when dmz falls ${d.length} zones, ${itemsOf(d, 'separate')} items (${d.join(', ')})`); console.log(`when management falls ${m.length} zones, ${itemsOf(m, 'separate')} items — ${SEG.filter(([a]) => a === 'management').length} direct rules`); console.log(`paths segmentation closes ${td - ts}; zone it cannot close 1`);
fallen zone flat zone flat item seg. zone seg. item reached in segmented layout branch 5 90 4 25 dmz, application, data, backup field 5 150 4 25 dmz, application, data, backup application 5 194 2 9 data, backup data 5 204 1 3 backup backup 5 207 0 0 - management 5 205 6 205 dmz, branch, field, application, data, backup zone total reached flat 30 segmented 17 closed 13 (43%) when dmz falls 3 zones, 21 items (application, data, backup) when management falls 6 zones, 205 items — 6 direct rules paths segmentation closes 13; zone it cannot close 1
In the flat column every row gives the same number: five zones. Which zone falls does not change, the result does not change; the item difference in the second column comes only from excluding the falling zone’s own items. In the segmented column the rows separate from one another, and the direction of that separation is the direction of the rule list. When the backup zone falls, no zone is reached, because the only rule leaving that zone points outward. When the data zone falls, only the backup zone is reached. Because the flow direction runs downward, the zones at the bottom of the chain cannot climb back up.
The branch and field zones carry the largest item sets — one hundred twenty and sixty items — and when they are the zone that falls, the items reached stay at twenty-five. In the flat layout the same two rows gave ninety and one hundred fifty. The total of closed paths is thirteen out of thirty; segmentation closes roughly half of the zone pairs, not all of them.
The two cut legitimate flows have a counterpart in this table. If a rule is written for the reconciliation flow from the field zone to the data zone, the line count rises to sixteen and the field zone reaches the data zone through a single boundary instead of three. The items reached stay at twenty-five — the data zone was already at the end of the chain — but the path shortens; and since the flow is legitimate, the rule gets written. Segmentation’s measure is therefore not the number taken on the day it was set up. Every new legitimate flow adds a line, every line opens a path, and lines accumulate in one direction only — no one is warned to delete the rule once the flow stops.
The last lines name the zone segmentation cannot close. The management zone is written directly into six zones, and when it falls, two hundred five items are reached — the same number as on the flat network, even in the segmented layout. The reason is not a gap but the work itself: the management zone must reach every zone to distribute configuration and troubleshoot. Segmentation closes no path in this zone. The twenty-one items reached when the demilitarized zone falls belong to the same class: a public-facing zone carries an inbound rule by definition, without which the zone serves no purpose at all. The path segmentation closes can be counted; the zone it cannot close can also be counted, and there is one of it.
Summary
- Perimeter control alone drops the items the outside zone reaches at the first hop from 16 to 4, but the transitive item count stays at 210: with no boundary between the inner zones, the side that reaches a single public-facing item reaches all 6 inner zones.
- Segmentation drops the transitive item count from 210 to 25. Moving the public-facing items to a separate zone gains 2 numbers on top of that: the item reached at the first hop goes from 16 to 4, and the number of boundaries to the data zone goes from 2 to 3.
- The cost is 15 rule lines and 2 cut legitimate flows. Both of the cut flows are jobs that run rarely (the quarter-end reconciliation, the branch archive); on the flat network they had never appeared, because they needed no rule.
- The zone total reached when one zone falls is 30 in the flat layout, 17 in the segmented layout: 13 closed paths. When the branch zone falls, the items reached drop from 90 to 25.
- The zone segmentation cannot close is the management zone: it is written directly into 6 zones, and when it falls, 205 items are reached — the same number as in the flat layout.
Next Step
The management-zone row leaves one item unresolved: a zone written directly into six zones is the one place segmentation closes no path. The next lesson’s question is not removing this zone, but what is gained by routing the connections leaving it through a single point. When a checkpoint is placed, how many connections pass through that point, what does the number of claims that point records and proves become, how many paths are cut at once when the point falls, and most importantly — how many paths reach the target without ever using that point.
To keep your progress and take notes, Log in
My notes
Log in to take notes.