Lesson 14 / 20
Virtual Private Networks
The access scope a tunnel opens is run across three placements: how many items the remote endpoint reaches directly when it lands in the internal zone, how many items fall out of scope when it lands in a separate zone and in a zone per role; how many connections a split tunnel leaves outside inspection, and how many connections a single verification is enough to count as proven subject identity.
Published Updated
In this lesson
- Counts the directly and transitively reached items by the tunnel's landing zone, separately across three placements
- Compares the concentrator load of full tunnel versus split tunnel and the connections each leaves outside inspection
- Measures the scope of verification: how many hours and how many connections one proof covers
Contents
Every defense counted so far either split traffic into zones, routed it through a single point, logged it, or dropped it. Their shared assumption was that the connecting party stood outside the network. A tunnel removes that assumption: once a remote endpoint is verified, it takes an address from the internal network and becomes a node in the zone graph. From that moment on, distance is not a security property.
Three things are measured. Which zone the tunnel lands in when it opens and how many items are reachable from there; how many connections a split tunnel leaves outside the tunnel; and how many connections a single verification is enough to count as proven subject identity. Tunneling and private connectivity in cloud networks were covered in another course; the axis here is not the connection’s establishment but its scope after establishment.
- PD29. The three remote endpoint classes, item counts, connection and session counts belong to a
fictional zonal measurement network. No real tunnel is established; what is measured is the model built
with
node. - PD30. Encryption, key exchange, and the tunnel protocol are not modeled. It is assumed that the tunnel is established and that traffic inside it cannot be read from outside; what that assurance rests on is outside this course.
- PD31. Restrictions inside a zone are disregarded: an endpoint that lands in a zone reaches all of that zone’s items. Direct access is counted as the landing zone plus the targets of the rules that leave it.
- PD32. Session length is taken as eight hours, and every session is assumed to run its full length. Re-verification, session renewal, and idle timeout are not modeled.
- PD33. The split between corp-bound and external-bound connections is marked by hand; every run produces the same numbers.
The Zone the Tunnel Lands In
A single configuration item of the remote-access tunnel decides the whole scope: which zone the remote endpoint takes its address from. The easiest path is to land it in the zone where employees already sit, because that zone’s rules are already written and no new line is needed. The model compares three placements: landing in the internal zone, landing in a single remote-access zone, and landing each endpoint class in its own zone.
// tunnel.mjs — the zone the tunnel lands in, its access scope, and connection distribution; a model, not a real tunnel export const ITEM = { dmz: 4, branch: 120, field: 60, app: 12, data: 6, management: 5, backup: 3 }; const MANAGEMENT = ['dmz', 'branch', 'field', 'app', 'data', 'backup'].map((b) => ['management', b]); export const BASE = [['branch', 'dmz'], ['branch', 'app'], ['field', 'dmz'], ['dmz', 'app'], ['app', 'data'], ['data', 'backup'], ...MANAGEMENT]; // [class, endpoint count, corp-bound connections, external-bound connections, sessions in period] export const ENDPOINT = [ ['field laptop', 40, 26400, 5100, 880], ['remote worker', 25, 18600, 92000, 550], ['vendor', 6, 740, 0, 24], ]; // which zone an endpoint class lands in, and which rules get written from that zone export const PLACEMENT = { flat: { land: () => 'branch', extra: [] }, 'single remote zone': { land: () => 'remote', extra: [['remote', 'dmz'], ['remote', 'app']] }, 'zone per role': { land: (s) => ({ 'field laptop': 'remote-field', 'remote worker': 'remote-office', vendor: 'remote-vendor' })[s], extra: [['remote-field', 'dmz'], ['remote-office', 'dmz'], ['remote-office', 'app'], ['remote-vendor', 'dmz']], }, }; export function reached(edges, start) { const seen = new Set(), queue = [start]; while (queue.length) { const b = queue.shift(); for (const [x, y] of edges) if (x === b && !seen.has(y)) { seen.add(y); queue.push(y); } } return seen; } export const itemsOf = (set) => [...set].reduce((a, b) => a + (ITEM[b] ?? 0), 0); export const scope = (y, s) => { // items an endpoint class can reach while the tunnel is open const edges = [...BASE, ...y.extra], start = y.land(s); const near = new Set([start, ...edges.filter(([x]) => x === start).map(([, b]) => b)]); const all = reached(edges, start); all.add(start); return { zone: [...all].filter((b) => ITEM[b]).length, direct: itemsOf(near), transitive: itemsOf(all) }; }; export const TOTAL_ITEMS = itemsOf(Object.keys(ITEM)); export const SESSION_HOURS = 8; // length of a session opened by a single verification export const sum = (i) => ENDPOINT.reduce((a, u) => a + u[i], 0);
The model gives two measures, and the two must not be confused. Directly reached items sum the landing zone’s own items with the targets of the rules that leave that zone; transitively reached items walk the whole graph. The difference is what taking one more step depends on: directly reached items are in the remote endpoint’s own hands, transitive ones require an application flow to be used somewhere along the way. The Network Segmentation and the Demilitarized Zone lesson counted the same distinction as the first hop versus transitive items.
// run1.mjs — the zone and items an endpoint class can reach while the tunnel is open, across three placements import { ENDPOINT, PLACEMENT, BASE, scope, TOTAL_ITEMS } from './tunnel.mjs'; console.log(`inside network ${TOTAL_ITEMS} items, ${BASE.length} zone rules\n`); console.log('columns: direct / transitive items reached per endpoint class'); console.log(`${'placement'.padEnd(19)} ${'extra rules'.padStart(11)}` + ENDPOINT.map((u) => u[0].padStart(16)).join('')); for (const [name, y] of Object.entries(PLACEMENT)) console.log(`${name.padEnd(19)} ${String(y.extra.length).padStart(11)}` + ENDPOINT.map((u) => { const k = scope(y, u[0]); return `${k.direct}/${k.transitive}`.padStart(16); }).join('')); console.log(`\n${'placement'.padEnd(19)} ${'extra rules'.padStart(11)} ${'widest direct'.padStart(17)} ` + `${'items out of scope'.padStart(19)} ${'closed-path share'.padStart(18)}`); for (const [name, y] of Object.entries(PLACEMENT)) { const en = Math.max(...ENDPOINT.map((u) => scope(y, u[0]).direct)); console.log(`${name.padEnd(19)} ${String(y.extra.length).padStart(11)} ${String(en).padStart(17)} ` + `${String(TOTAL_ITEMS - en).padStart(19)} ${`${((TOTAL_ITEMS - en) * 100 / TOTAL_ITEMS).toFixed(1)}%`.padStart(18)}`); } const r = PLACEMENT['zone per role']; console.log('\nitems reached per class under zone-per-role:'); for (const u of ENDPOINT) console.log(` ${u[0].padEnd(14)} ${u[1]} endpoints ${String(scope(r, u[0]).direct).padStart(4)} direct, ${String(scope(r, u[0]).transitive).padStart(3)} transitive items`);
inside network 210 items, 12 zone rules columns: direct / transitive items reached per endpoint class placement extra rules field laptop remote worker vendor flat 0 136/145 136/145 136/145 single remote zone 2 16/25 16/25 16/25 zone per role 4 4/25 16/25 4/25 placement extra rules widest direct items out of scope closed-path share flat 0 136 74 35.2% single remote zone 2 16 194 92.4% zone per role 4 16 194 92.4% items reached per class under zone-per-role: field laptop 40 endpoints 4 direct, 25 transitive items remote worker 25 endpoints 16 direct, 25 transitive items vendor 6 endpoints 4 direct, 25 transitive items
The first row is what writing no rules at all costs. When the tunnel lands in the branch zone, the remote endpoint stands on the same flat plane as that zone’s hundred and twenty items and also inherits the rules that leave the branch zone: directly reached items are a hundred and thirty-six, transitively reached items a hundred and forty-five. Two-thirds of a two-hundred-and-ten-item network sits behind a single tunnel. The vendor endpoint and the field laptop endpoint get the same number, because scope is decided not by who the endpoint is but by which zone it lands in.
The second row is what two rule lines cost. When remote endpoints land in their own zone, directly reached items drop to sixteen, transitively reached items to twenty-five; items out of scope climb from seventy-four to a hundred and ninety-four. Two lines move a hundred and twenty items out of scope. This is the highest-yield move in this lesson, and its cost is two lines.
The third row adds two more lines and does not change total scope at all: transitive access is still twenty-five. What changes is direct access per class. The field laptop and the vendor endpoint now reach four items directly instead of sixteen; the remote worker’s scope stays the same, because its work is in the app zone. Zone per role does not narrow total scope — it leaves the widest scope to the fewest endpoints. Forty-six of the seventy-one endpoints now see four items instead of sixteen.
The shared lesson of these three rows is that the tunnel itself is not a security item. The same tunnel is established, the same verification is performed, and traffic inside is carried the same way in all three placements. The only thing that decides the measure is which zone the remote endpoint takes its address from and which rules are written from that zone. A tunnel is an access mechanism; its scope is a segmentation decision, and when no segmentation decision is made, the default scope is the widest one.
The cost of the placements shows up in the rule line, but there is one more item, and it is not in the table: which class the remote endpoint belongs to must be determined on every session. In the flat placement that lands in the internal zone, this question is never asked. In the zone-per-role placement, a mapping is made on every session, and the information that mapping rests on is not something the endpoint declares itself but an attribute that verification returns. The real cost of the four rule lines is maintaining that mapping: an endpoint that changes class changes scope without the rule changing.
Split Tunnel and the Scope of Verification
// run2.mjs — full tunnel versus split tunnel: concentrator load, out-of-inspection connections, and the scope of verification import { ENDPOINT, SESSION_HOURS, sum } from './tunnel.mjs'; console.log(`${'endpoint class'.padEnd(15)} ${'eps'.padStart(3)} ${'sessions'.padStart(8)} ${'corp-bound'.padStart(13)} ` + `${'external-bound'.padStart(14)} ${'full tunnel'.padStart(12)} ${'split'.padStart(8)} ${'unproven per session'.padStart(22)}`); for (const [name, eps, corp, ext, sessions] of ENDPOINT) console.log(`${name.padEnd(15)} ${String(eps).padStart(3)} ${String(sessions).padStart(8)} ${String(corp).padStart(13)} ` + `${String(ext).padStart(14)} ${String(corp + ext).padStart(12)} ${String(corp).padStart(8)} ` + `${(corp / sessions).toFixed(1).padStart(22)}`); const [eps, corp, ext, sessions] = [sum(1), sum(2), sum(3), sum(4)]; console.log(`\n${'measure'.padEnd(42)} ${'full tunnel'.padStart(11)} ${'split tunnel'.padStart(14)}`); for (const [name, full, split] of [ ['connections through the concentrator', corp + ext, corp], ['connections outside inspection', 0, ext], ['endpoints straddling both networks at once', 0, eps], ['sessions opened by a single verification', sessions, sessions], ['hours covered by one verification', SESSION_HOURS, SESSION_HOURS], ['connections covered by one verification', Math.round((corp + ext) / sessions), Math.round(corp / sessions)], ]) console.log(`${name.padEnd(42)} ${String(full).padStart(11)} ${String(split).padStart(14)}`); console.log(`\nconnections cut when the tunnel drops full ${corp + ext} split ${corp}`); console.log(`${sessions} verifications happen in the period; ${corp + ext} connections' subject identity rests on these ${sessions} proofs`);
endpoint class eps sessions corp-bound external-bound full tunnel split unproven per session field laptop 40 880 26400 5100 31500 26400 30.0 remote worker 25 550 18600 92000 110600 18600 33.8 vendor 6 24 740 0 740 740 30.8 measure full tunnel split tunnel connections through the concentrator 142840 45740 connections outside inspection 0 97100 endpoints straddling both networks at once 0 71 sessions opened by a single verification 1454 1454 hours covered by one verification 8 8 connections covered by one verification 98 31 connections cut when the tunnel drops full 142840 split 45740 1454 verifications happen in the period; 142840 connections' subject identity rests on these 1454 proofs
Full tunnel routes all traffic through the concentrator: a hundred and forty-two thousand eight hundred and forty connections, zero outside inspection. Split tunnel routes only the corp-bound portion, and the load drops to forty-five thousand — but ninety-seven thousand one hundred connections pass through no inspection at all. Where that number comes from shows up in the second row: the remote worker class’s external-bound connections alone are ninety-two thousand. The entire source of split tunnel cutting the load to a third is this set that it removes from inspection.
The third row is split tunnel’s real item. In split tunnel, seventy-one endpoints stand in both networks at once: holding an address from the internal network and connected directly to the external network. There is no rule set between the two networks; the endpoint itself is a node facing both sides. Every boundary drawn in the zone graph intersects at these seventy-one points. In full tunnel this number is zero — its cost is that the ninety-seven thousand outbound connections also pass through the corporate exit, and the concentrator carries three times the load.
The last three rows return to the measure itself. A thousand four hundred and fifty-four verifications happen in the period, and once the tunnel opens, it is not asked again for eight hours. In full tunnel, one verification counts an average of ninety-eight connections as proven; in split tunnel, thirty-one. Of the forty-eight claims counted in the first lesson, three were subject identities that no generation could prove; the tunnel proves it once and leans on that single proof for the next ninety-seven connections. If the proof turns out wrong — if the endpoint is in someone else’s hands — the network has no place to notice this for eight hours, because every connection inside the tunnel appears as part of a verified session.
There is one more detail in the first of the columns. The unproven-connections-per-session count sits around thirty in all three classes: thirty, thirty-three point eight, and thirty point eight. The number not changing across classes shows that the measure has nothing to do with endpoint count or volume of work. What decides it is session length; an eight-hour session ties every piece of work done in that time to a single proof. Halving the session length halves this number too, and in exchange doubles the number of verifications — its cost is asking for identity two thousand nine hundred and eight times in the period instead of a thousand four hundred and fifty-four.
The connections-cut-when-the-tunnel-drops column is the last item of the cost. The concentrator belongs to the same class as the inspection points measured in the previous lesson: a visibility gain and an availability debt taken on in the same move. In full tunnel the debt is a hundred and forty-two thousand connections, in split tunnel forty-five thousand. The choice is not between the two; it is over which number gets written on which side.
Site-to-site tunnels repeat the same measure at a different scale. In a remote-access tunnel a single endpoint lands in a zone; in a site-to-site tunnel an entire network lands. A tunnel established between a branch network and the central zone collapses the two networks into a single edge in the zone graph, and which items that edge covers still depends on the rules written. When no rule is written, the two networks become a single flat plane; the measure is still directly reached items, and this time the branch network’s item count is added not to the remote endpoint’s scope but to the other side’s.
Summary
- When the tunnel lands in the internal zone, the remote endpoint reaches 136 items directly and 145 transitively: two-thirds of a 210-item network. Scope depends not on who the endpoint is but on which zone it lands in.
- A separate remote-access zone, with 2 rule lines, brings direct access down to 16 and transitive access down to 25; items out of scope climb from 74 to 194. This is the highest-yield move.
- Zone per role adds 2 more lines and does not change total scope: it leaves the widest scope to the fewest endpoints. 46 of 71 endpoints see 4 items instead of 16.
- Split tunnel brings the concentrator load down from 142,840 to 45,740; in exchange, 97,100 connections pass through no inspection at all, and 71 endpoints stand in both networks at once.
- 1,454 verifications happen in the period and one verification covers 8 hours: in full tunnel an average of 98, in split tunnel 31 connections rest on a single proof. If the proof turns out wrong, there is no place for the network to notice for eight hours.
Next Step
Six lessons of writing defenses have each come down to the same thing: a rule list. Zone boundaries are fifteen lines, remote-access zones are four lines, inspection points and block lists have their own lines. All of these lines sit side by side in a single box, and when a packet arrives, they are not all looked at at once — they are looked at in order, and the first match wins. The next lesson’s question is not which rule is written but which one comes first: for a known set of flows, the first matching rule is found for each flow, and how many flows’ outcomes change when the list’s order changes is counted.
To keep your progress and take notes, Log in
My notes
Log in to take notes.