Lesson 11 / 15
Secure Defaults
What happens when one of twenty-two configuration items is forgotten is counted separately under two defaults: the path opened under an open default, the daily work stopped under a closed default. The count of forgotten items is swept, and resilience to misconfiguration is measured by comparing the visibility rate of the two error types.
Published Updated
In this lesson
- Ties every step to a configuration item and runs the forgotten item's default in both directions
- Measures the cost of forgetting a single item separately under two defaults: opened path and stopped daily work
- Sweeps the count of forgotten items and compares the two defaults' average and worst-case outcome
- Computes the error's visibility rate and derives the share of misconfiguration that stays silent
Contents
The previous lesson separated the two controls and counted which request each one stops. Every scheme shared one assumption: the scope list is written correctly, and every step that should be in scope is in scope. In reality this list is hand-written, gets updated as the network changes, and one item gets forgotten. This lesson’s question is what happens to the forgotten item.
Secure by default is treating an unconfigured item as closed. Its opposite is letting an unconfigured item through. The two options only diverge at the moment of error: when everything is written correctly, both give the same network. This lesson measures that moment of error — when one item is forgotten, how many paths open under an open default, how much legitimate work stops under a closed default, and which of the two errors shows itself. The path count is again done on the fictional network’s model; scanning configuration items on a real network cannot be carried out without written authorization and a defined scope.
- DP27. Each of the twenty-two steps is a configuration item. At full configuration, every item checks authorization, and the network narrows to the twenty paths of the previous lesson’s narrowest scheme.
- DP28. A forgotten item performs no check. Under an open default it lets everyone through; under a closed default it stops everyone, legitimate flow included.
- DP29. Items are forgotten independently of each other, and the probability of being forgotten is equal for every item. In reality, newly added items get forgotten more often; this grows the cost of the open default.
- DP30. Sampling is done with a hand-written linear congruential generator. The seed is visible and fixed; the output stays the same run to run.
- DP31. The “invisible” column is the share of samples where the error leaves no measurable trace: unchanged path count under open, no legitimate work stopped under closed.
The Cost of One Item
Twenty-two items are forgotten one at a time, and each forgetting’s outcome under the two defaults is written down separately.
// 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)));
// default.mjs — the default state of an unconfigured item and the path count import { EDGE, FLOW, ENTRY, ASSET, edgeName, steps } from './network.mjs'; export const EDGE_NAME = EDGE.map(([a, b]) => edgeName(a, b)); const SHARED = new Set(FLOW.filter(([, d]) => !ENTRY.includes(d[0])).flatMap(([, d]) => steps(d))); const own = (g) => FLOW.filter(([, d]) => d[0] === g).flatMap(([, d]) => steps(d)); export const AUTHORIZATION = Object.fromEntries(ENTRY.map((g) => [g, new Set([...SHARED, ...own(g)])])); const neighbor = new Map(); for (const [a, b] of EDGE) neighbor.set(a, [...(neighbor.get(a) || []), b]); // A configured item checks authorization; a forgotten item lets through or stops per the fallback. export function pathCount(forgotten, fallback) { const F = new Set(forgotten); let n = 0; for (const g of ENTRY) { const walk = (d, y) => { if (ASSET.includes(d)) return void n++; for (const k of neighbor.get(d) || []) { const e = edgeName(d, k); if (F.has(e)) { if (fallback === 'closed') continue; } else if (!AUTHORIZATION[g].has(e)) continue; if (!y.includes(k)) walk(k, [...y, k]); } }; walk(g, [g]); } return n; } export function generator(seed) { let s = seed >>> 0; return () => { s = (Math.imul(s, 1664525) + 1013904223) >>> 0; return s / 4294967296; }; }
// run1.mjs — when a single item is forgotten: path opened under open default, job stopped under closed import { FLOW, steps } from './network.mjs'; import { EDGE_NAME, pathCount } from './default.mjs'; const full = pathCount([], 'open'); const stopped = (k) => FLOW.filter(([, d]) => steps(d).includes(k)).reduce((s, [, , n]) => s + n, 0); const row = EDGE_NAME.map((k) => [k, pathCount([k], 'open') - full, stopped(k)]); console.log(`path at full configuration ${full}, configuration item ${EDGE_NAME.length}\n`); console.log('open default — item opening the most path when forgotten'); for (const [k, a] of [...row].sort((x, y) => y[1] - x[1]).slice(0, 5)) console.log(` ${k.padEnd(33)}+${a} path`); console.log('\nclosed default — item stopping the most job when forgotten'); for (const [k, , c] of [...row].sort((x, y) => y[2] - x[2]).slice(0, 5)) console.log(` ${k.padEnd(33)}${c} daily request stopped`); console.log(''); for (const [label, v] of [ ['average path opened under open', (row.reduce((s, x) => s + x[1], 0) / row.length).toFixed(2)], ['item opening no path under open', `${row.filter((x) => x[1] === 0).length} / ${row.length}`], ['average daily request stopped under closed', Math.round(row.reduce((s, x) => s + x[2], 0) / row.length)], ['item stopping no job under closed', `${row.filter((x) => x[2] === 0).length} / ${row.length}`], ]) console.log(`${label.padEnd(45)}${v}`);
path at full configuration 20, configuration item 22 open default — item opening the most path when forgotten collector>queue +12 path management>work-order +5 path collector>measurement-store +2 path management>collector +1 path management>configuration +1 path closed default — item stopping the most job when forgotten field-endpoint>collector 1440 daily request stopped collector>queue 1440 daily request stopped queue>validator 1440 daily request stopped validator>measurement-store 1440 daily request stopped collector>measurement-store 510 daily request stopped average path opened under open 0.95 item opening no path under open 17 / 22 average daily request stopped under closed 319 item stopping no job under closed 2 / 22
The shapes of the two lists are different. Under the open default the distribution is extremely uneven: one item opens twelve paths, one opens five, three open one or two each, and the remaining seventeen items open nothing. The average is zero point nine five paths, but the average is meaningless here — the cost is concentrated in one item. If the check on the queue’s write step is forgotten, the path count rises from twenty to thirty-two, meaning a third of all the work that took the network from fifty-five paths down to twenty is handed back by forgetting a single item.
Under the closed default the distribution is balanced the other way. Forgetting twenty of the twenty-two items stops at least one legitimate job; the average is three hundred nineteen requests a day. Four separate items stop one thousand four hundred forty requests a day, because the field-reading and validation flows are the network’s busiest work. In one sense this is the cost of the closed default; in another sense it is the guarantee that the error shows up immediately.
The overlap between the two lists also deserves attention. The queue’s write step sits at the top of both lists: it is the item that opens the most paths under the open default, and one of the four items that stop the most work under the closed default. The most heavily used step is also the step carrying the most paths, and it is among the most expensive items no matter which default is chosen. The first place a configuration check should look is the step carrying the most traffic. This item deserves separate verification under either default; regardless of which default is chosen, an item that alone carries a third of the achieved narrowing needs to be specifically tested for being written correctly.
However Many Items Are Forgotten
One item is not a realistic error count. The same measurement is repeated when k items are forgotten, and a third column is added: the rate at which the error leaves no trace at all.
// run2.mjs — the cost of the two defaults and the visibility of the error when k items are forgotten import { FLOW, steps } from './network.mjs'; import { EDGE_NAME, pathCount, generator } from './default.mjs'; const SEED = 20260802, SAMPLE = 200, full = pathCount([], 'open'); const r = generator(SEED); const pick = (k) => { const a = [...EDGE_NAME]; for (let i = a.length - 1; i > 0; i--) { const j = Math.floor(r() * (i + 1)); [a[i], a[j]] = [a[j], a[i]]; } return a.slice(0, k); }; const stopped = (u) => FLOW.filter(([, d]) => steps(d).some((x) => u.includes(x))).reduce((s, [, , n]) => s + n, 0); console.log(`seed ${SEED}, sample ${SAMPLE}, path at full configuration ${full}`); console.log('forgotten open: avg path worst invisible closed: avg stopped worst invisible'); for (const k of [1, 2, 3, 5, 8, 22]) { const count = k === 22 ? 1 : SAMPLE, o = { a: 0, am: 0, ag: 0, c: 0, cm: 0, cg: 0 }; for (let i = 0; i < count; i++) { const u = k === 22 ? EDGE_NAME : pick(k); const ya = pathCount(u, 'open'), dur = stopped(u); o.a += ya; o.am = Math.max(o.am, ya); if (ya === full) o.ag++; o.c += dur; o.cm = Math.max(o.cm, dur); if (dur === 0) o.cg++; } console.log(`${String(k).padEnd(11)}${(o.a / count).toFixed(1).padEnd(15)}${String(o.am).padEnd(9)}` + `${`${Math.round((o.ag / count) * 100)}%`.padEnd(11)}${(o.c / count).toFixed(0).padEnd(21)}` + `${String(o.cm).padEnd(9)}${Math.round((o.cg / count) * 100)}%`); }
seed 20260802, sample 200, path at full configuration 20 forgotten open: avg path worst invisible closed: avg stopped worst invisible 1 21.3 32 73% 347 1440 9% 2 22.3 37 53% 644 2880 1% 3 23.2 41 44% 969 2944 0% 5 24.9 42 22% 1343 3423 0% 8 29.4 50 8% 2050 3510 0% 22 55.0 55 0% 3513 3513 0%
The invisible columns are this lesson’s answer. When a single item is forgotten, seventy-three percent of the errors under the open default leave no trace at all: the path count does not change, nobody notices, no record drops. The same error under the closed default stops a job with ninety-one percent probability and is seen the moment it stops. It is not the size of the two errors that differs, it is their discoverability.
The gap closes as the number of forgotten items grows, but in the wrong direction. Under the open default, forgetting three items still leaves forty-four percent silent; at eight items it is eight percent. Under the closed default, silence drops to one percent at two items, zero at three. This means a system running on the open default can carry accumulated misconfiguration for years; a system running on the closed default announces itself by the second error.
The worst-case columns are not symmetric either. Under the open default, forgetting a single item raises the path count to thirty-two, eight items to fifty; forgetting all twenty-two raises it to fifty-five — a network as if no defense existed at all. Under the closed default, at the same extreme the network stops entirely: all three thousand five hundred thirteen requests a day. The result of both errors is catastrophic, but one is a silent opening, the other a loud outage. The case for secure by default is that the second kind of error is known to get fixed; the first kind is never fixed, because its existence is never known.
A bias in the model also has to be written down. Items were assumed to be forgotten with equal probability. In reality the forgotten item is not chosen at random: a newly added service’s steps get forgotten more than old steps that rarely change. The two items that stop no job in the table are steps that carry no legitimate traffic yet; newly added steps also fall into the same group in their first days. Put together, these two observations mean the open default’s real cost comes out larger than the table shows: the place forgetting is most likely is the same place the closed default stays most silent, and where the error is therefore seen latest. The way to bring the measurement closer to reality is to tie the probability of being forgotten to the item’s age; this lesson does not take that step, but which way the result would shift once it is taken is clear.
The average columns also give a balance point. When five items are forgotten, the open default leaves an average of twenty-five paths open — five more than the twenty at full configuration. The same five items under the closed default stop one thousand three hundred forty-three requests a day, thirty-eight percent of the daily work. The closed default’s cost is clearly larger, and that cost lands directly on operations. The decision is made between these two numbers — but a decision made without reading the invisible column is a decision that reads only the first column.
Summary
- At full configuration all 22 items check authorization and the network narrows to 20 paths. The forgotten item’s outcome depends only on the default; the two defaults give the same network when there is no error.
- Under the open default, the distribution of forgetting a single item is uneven: one item opens 12 paths, 17 items open nothing. The average is 0.95 paths, but the cost is concentrated in one item.
- Under the closed default, 20 of the 22 items stop at least one job when forgotten; the average is 319 requests a day, the worst 1,440. This is both the cost and the guarantee that the error is seen immediately.
- The invisibility rate gives the real difference: when a single item is forgotten, 73% of the errors under the open default leave no trace at all, versus only 9% under the closed default.
- The gap closes in the wrong direction as the error count grows: at 3 items, the open default is still 44% silent, the closed default 0%. A system running on the open default accumulates misconfiguration.
- At the worst extreme, forgetting all 22 items turns the network into 55 paths under the open default — as if no defense existed; under the closed default it stops all 3,513 daily requests.
Next Step
Six principles were measured on the same path set, and the cost of each was counted. Defense in depth closed fifty-two of the fifty-five paths and brought sixty-three items of maintenance load; least privilege closed forty-three paths for the cost of eleven daily requests; isolation brought the reached item down from thirteen to four point eight and required twenty gates; zero trust brought the path down to twenty and added two extra steps per request; the authentication-authorization distinction showed that a control sitting in the wrong layer leaves thirty paths open; secure by default revealed that forgetting one item stays silent with seventy-three percent probability under the open default.
There is a question none of these tables asked. Who is going to run the measurements? Who will notice that a layer closes no new path, who will test that a gate is unnecessary, who will scan for a forgotten item? The numbers did not appear by themselves; each one requires testing independent of whoever built the defense. The next topic takes up this question: what is the function of the roles that build defenses, think like an attacker, and combine both; which job descriptions these tasks are divided into; and what determines the written boundary within which a test can be carried out.
To keep your progress and take notes, Log in
My notes
Log in to take notes.