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

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

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.

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

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

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