---
title: 'Information Technology and Systems Fundamentals'
source: 'https://academia.sh/en/courses/it-fundamentals'
language: en
updated: '2026-09-13T05:08:04+00:00'
license: 'CC BY-SA 4.0'
lessons: 11
outcomes:
    - 'Recognizes hardware and operating system components together with the security boundary they carry'
    - 'Compares the isolation strength of virtualization layers by the number of items needed to cross the boundary'
    - 'Works through system problems methodically, by how many possibilities each hypothesis eliminates'
---

# Information Technology and Systems Fundamentals

Reading the layer beneath the protected thing with a security eye: which boundary the processor, memory, storage, and interfaces carry, the security model difference between operating system families, the items a clean install and baseline hardening close, how permission and ownership models across platforms give different answers to the same request, the difference in traces left by a graphical interface and a command line, the elimination power of the symptom, hypothesis, and verification loop, hypervisor types, the isolation boundary between guest and host and the risk of escape, what snapshots and clones recover and what they do not, a comparison of isolation strength against containers, backup types, and recovery testing.

## Hardware and Operating Systems

Counting item by item the boundaries a single machine draws: the boundary and boot chain carried by hardware components, the security model difference between operating system families, the items a clean install closes and fails to close, the point where two permission models diverge on the same access request, the difference in the trace left by two management forms, and the elimination power of a troubleshooting sequence.

### Computer Hardware Components

Source: https://academia.sh/en/courses/it-fundamentals/computer-hardware-components

Reading hardware as a list of boundaries: the boundary drawn by eight components across a 43-item inventory, the number of items that must be simultaneously wrong to cross a boundary ranging from 1 to 3 over remote access and 0 to 3 over physical access, two components opening the entire inventory on their own, and the opened item count staying at 43 even after three of four hardening settings are turned on.

The previous course counted how many separate paths there are to reach an asset and measured
each defense principle by the paths it closed. All of those paths shared one thing: every one of
them passed through a machine. The machine itself was never opened up.

This lesson does not treat hardware as a catalog. How many cores the processor has or how fast
the memory runs is not the question here. The question is: which boundary does a component draw,
how many items sit on either side of that boundary, and what becomes reachable once the boundary
is crossed.

#### Hardware as a List of Boundaries

A component's security value is written with three numbers.

**The boundary's name and side.** A component forces a distinction: one set of items sits inside,
the rest outside. "The processor is secure" carries no decision; "the processor draws the
privilege-mode boundary, and this many items sit inside that boundary" does.

**The items required to cross the boundary.** Crossing from one side to the other requires how
many separate conditions to be wrong **at the same time**. This number is the isolation strength.
If it is one, there is no boundary: a single fault grants the whole crossing. If it is zero, the
boundary was never established.

**The items reached when the boundary falls.** The boundary is assumed crossed once, and how many
items become reachable is counted. A narrow boundary yields few items even when it falls; a wide
boundary opens the entire inventory when it falls.

#### The Boundary of Eight Components

The example is a branch server in the course's fictional **regional measurement network**: it stores
data arriving from the endpoints that collect meter readings and distributes it to field teams'
laptops. Both the server and the inventory are fictional; the table below is a **model** — there
is no real machine, vendor, or configuration.

**HO1:** the protected asset inventory is 43 items, divided into seven layers. **HO2:** each
component draws a boundary; the layers reached when the boundary falls are written item by item.
**HO3:** the threshold is the number of items that must be simultaneously wrong to cross the
boundary, given separately for remote access and physical access. **HO4:** there are four
hardening settings; each adds two items to its component's physical threshold. **HO5:** the
boundary with the lowest threshold is selected; the items reached are the **union** of the layers
opened by boundaries at a threshold of one or below. **HO6:** the peripheral interface cannot be
crossed remotely — it requires physical attachment.

```js
// it/component.mjs — component and boundary inventory of the fictional regional measurement
// network branch server (model): not a real machine, vendor, or configuration. No randomness.

// Asset inventory: seven layers, items per layer.
export const LAYER = { "kernel-memory": 5, "process-memory": 9, "disk-data": 11,
  "configuration": 6, "key-material": 3, "network-endpoint": 4, "log": 5 };
export const TOTAL = Object.values(LAYER).reduce((a, b) => a + b, 0);
const ALL = Object.keys(LAYER);

// Each component draws a boundary. r/p: items that must be simultaneously wrong to cross the
// boundary over remote and physical access (Infinity: that path cannot cross it). setting:
// hardening that adds items to the boundary; extra: items added to p when the setting is on.
// opens: layers reached when the boundary falls.
export const BOUNDARY = [
  { name: "processor", boundary: "privilege-mode", r: 2, p: 2, setting: null, extra: 0,
    opens: ["kernel-memory", "process-memory", "configuration", "log"] },
  { name: "memory-management-unit", boundary: "process-address-space", r: 2, p: 2,
    setting: null, extra: 0, opens: ["process-memory"] },
  { name: "persistent-storage", boundary: "filesystem-permission", r: 2, p: 0,
    setting: "disk-encryption", extra: 2, opens: ["disk-data", "configuration", "log"] },
  { name: "firmware", boundary: "boot-chain", r: 3, p: 1,
    setting: "secure-boot", extra: 2, opens: ALL },
  { name: "trusted-platform-module", boundary: "key-storage", r: 3, p: 3, setting: null,
    extra: 0, opens: ["key-material"] },
  { name: "network-interface", boundary: "listening-port", r: 1, p: 1, setting: null, extra: 0, opens: ["network-endpoint"] },
  { name: "peripheral-interface", boundary: "device-memory-access", r: Infinity, p: 1,
    setting: "device-address-isolation", extra: 2, opens: ["kernel-memory", "process-memory"] },
  { name: "out-of-band-management-interface", boundary: "management-network", r: 2, p: 1,
    setting: "management-network-segmentation", extra: 2, opens: ALL },
];

export const SETTING = ["secure-boot", "disk-encryption", "device-address-isolation",
  "management-network-segmentation"];

export const item = (layers) => layers.reduce((a, l) => a + LAYER[l], 0);
// Threshold: items needed to cross a boundary given the set of enabled settings.
export const threshold = (s, path, open) => (path === "r" ? s.r : s.p + (open.has(s.setting) ? s.extra : 0));

// Weakest link: lowest threshold, its boundaries, and the items opened by loose boundaries.
export function weakest(path, open = new Set()) {
  const e = BOUNDARY.map((s) => threshold(s, path, open));
  const min = Math.min(...e);
  const loose = BOUNDARY.filter((s, i) => e[i] <= 1);
  const union = new Set(loose.flatMap((s) => s.opens));
  return { min, minName: BOUNDARY.filter((s, i) => e[i] === min).map((s) => s.name).join(" "),
    loose: loose.length, opened: item([...union]) };
}
```

```js
// it/measure.mjs — counts each component's boundary, threshold, and the items opened when the boundary falls.
import { LAYER, TOTAL, BOUNDARY, SETTING, item, threshold, weakest } from "./component.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const tally = (n) => (n === Infinity ? "-" : n);

console.log(`asset inventory: ${TOTAL} items / ${Object.keys(LAYER).length} layers — ` +
  Object.entries(LAYER).map(([k, v]) => `${k} ${v}`).join(", "));

const A = [-34, -23, 9, 10, 8, 10];
console.log("\n1. boundary per component (inside: items reached when the boundary falls)");
write(A, "component", "boundary", "inside", "outside", "remote", "physical");
for (const s of BOUNDARY) {
  const i = item([...new Set(s.opens)]);
  write(A, s.name, s.boundary, i, TOTAL - i, tally(s.r), tally(s.p));
}

const B = [-12, -28, 9, 14, 9];
console.log("\n2. weakest link (with no hardening setting enabled)");
write(B, "path", "weakest component", "lowest", "threshold<=1", "opened");
for (const [ad, path] of [["remote", "r"], ["physical", "p"]]) {
  const z = weakest(path);
  write(B, ad, z.minName, z.min, z.loose, z.opened);
}

const C = [-35, 9, 14, 9];
console.log("\n3. as settings are enabled in sequence (physical access)");
write(C, "open setting", "lowest", "threshold<=1", "opened");
const open = new Set();
write(C, "(none)", weakest("p").min, weakest("p").loose, weakest("p").opened);
for (const a of SETTING) {
  open.add(a);
  const z = weakest("p", open);
  write(C, `+ ${a}`, z.min, z.loose, z.opened);
}

const D = [-43, 9];
console.log("\n4. threshold distribution (all settings enabled, physical access)");
const all = new Set(SETTING);
const dist = {};
for (const s of BOUNDARY) dist[threshold(s, "p", all)] = (dist[threshold(s, "p", all)] || 0) + 1;
write(D, "threshold (items)", "components");
for (const k of Object.keys(dist).sort()) write(D, k, dist[k]);
write(D, "components that open the entire inventory",
  BOUNDARY.filter((s) => item([...new Set(s.opens)]) === TOTAL).length);
```

```
asset inventory: 43 items / 7 layers — kernel-memory 5, process-memory 9, disk-data 11, configuration 6, key-material 3, network-endpoint 4, log 5

1. boundary per component (inside: items reached when the boundary falls)
component                         boundary                  inside   outside  remote  physical
processor                         privilege-mode                25        18       2         2
memory-management-unit            process-address-space          9        34       2         2
persistent-storage                filesystem-permission         22        21       2         0
firmware                          boot-chain                    43         0       3         1
trusted-platform-module           key-storage                    3        40       3         3
network-interface                 listening-port                 4        39       1         1
peripheral-interface              device-memory-access          14        29       -         1
out-of-band-management-interface  management-network            43         0       2         1

2. weakest link (with no hardening setting enabled)
path        weakest component              lowest  threshold<=1   opened
remote      network-interface                   1             1        4
physical    persistent-storage                  0             5       43

3. as settings are enabled in sequence (physical access)
open setting                          lowest  threshold<=1   opened
(none)                                     0             5       43
+ secure-boot                              0             4       43
+ disk-encryption                          1             3       43
+ device-address-isolation                 1             2       43
+ management-network-segmentation          1             1        4

4. threshold distribution (all settings enabled, physical access)
threshold (items)                          components
1                                                  1
2                                                  3
3                                                  4
components that open the entire inventory          2
```
The numbers belong to the **measurement** class; their inputs are HO1–HO6.

#### The Boundary Below Establishes the One Above

The `inside` column of the first table splits the components into two groups. When **firmware**
or the **out-of-band management interface** falls, all 43 items open; their `outside` columns are
zero. When the memory management unit falls, 9 items open; when the trusted platform module
falls, 3.

The difference does not come from whether the component matters; it comes from its **order**.
**Firmware** is the first code that runs when the machine powers on, and it loads every layer
that runs after it. This chain is called the **boot chain**: firmware starts the boot loader, the
loader starts the operating system kernel, and the kernel starts the user processes. The code
that enforces the privilege-mode boundary and the code that checks filesystem permissions both
sit in the upper links of this chain. If the first link of the chain is altered, none of the
boundaries above it are established — because the thing that establishes the boundary has itself
been altered.

**Secure boot** exists for exactly this reason: each link verifies the next one's signature
before loading it. The model's `secure-boot` setting raises firmware's physical threshold from 1
to 3; bypassing the signature requires the signature check to be disabled, the trusted key set to
be replaced, and the change to persist.

The **trusted platform module** runs the opposite way. Its boundary is narrow — just 3 items, the
key material. But its threshold is 3 for both remote and physical access, because the key never
leaves the module; the module uses the key, it does not hand it over. **A narrow, deep boundary
serves a different purpose than a wide, shallow one:** the first protects few items almost
certainly, the second protects many items weakly. This difference is invisible unless the two
numbers are written separately.

#### What Changes When the Threshold Is Two

The processor's privilege-mode boundary and the memory management unit's process-address-space
boundary carry the same threshold in the model: 2. The number says two separate conditions must
be wrong **at the same time**. For privilege mode these are the existence of an exploitable bug
in kernel code and an open call path that reaches that bug; without either one, no crossing
occurs. For process address space these are a process being able to write outside its own
boundary and the location it writes to falling within another process's map.

Raising the threshold to two makes crossing harder — but only if the two items are
**independent**. If the same configuration item closes both, or the same privileged account can
change both, the two items behave as one and the real threshold is 1. The model does not see this
dependency: the `threshold` function counts items, it does not ask about the link between them.

**This is the first place the boundary falls short.** The threshold table gives an upper bound,
not a lower one; writing that a boundary is "two items" says at most two items are required, not
that two items must independently be wrong. The number is therefore never used alone; two
questions accompany it. Are the counted items managed separately? Can a single authority change
both at once? If the answer to the second question is yes, the number drops to one and the
boundary no longer counts as a boundary.

The same limitation applies to the third column too. The layer model counts `process-memory` and
`key-material` as separate items, but a key sits in process memory while it is in use; if the
memory management unit's boundary falls at that moment, the items opened are not 9 but 9 plus
whatever key currently sits in memory. Dividing the inventory into layers makes counting
possible; it hides **time**.

#### Physical Access Drives the Threshold to Zero

The second table reads the same inventory two ways. Over remote access the weakest boundary is
the network interface: threshold 1, items opened 4. Over physical access the weakest boundary is
persistent storage, and **its threshold is 0**.

Zero's meaning is exact: no condition needs to be wrong. When an unencrypted storage device is
removed and mounted on another machine, filesystem permissions are never even asked, because what
enforces the permissions is not the disk itself but the operating system that mounts it. The
permission bits sit on the disk as **data**; without a kernel that honors them, nothing is
enforced. Disk encryption raises this threshold to 2: both the key and the secret that unlocks
the key are required.

The dash in the peripheral interface's `remote` column is the other side of the same distinction.
A device's direct memory access cannot be exercised remotely; but if a device can be physically
attached to the machine, the threshold is 1, and kernel memory plus process memory — 14 items —
open. When device address isolation is enabled, the address space a device can see is restricted
and the threshold rises to 3.

#### The Weakest Link Is Counted

The third table gives this lesson's decision-carrying number. The four hardening settings are
enabled in sequence, and the **items opened** are read on each row: 43, 43, 43, 43, 4.

Even after three settings are enabled, the entire inventory over physical access is still open.
The reason is in the last column: the `threshold<=1` count drops from 5 to 4, 3, 2, but it
**never reaches zero**, and one of the remaining loose boundaries is, every time, the
out-of-band management interface, which opens 43 items. Enabling secure boot and disk encryption
never touches that component. The items reached drop to 4 only once the fourth setting —
management network segmentation — is enabled.

**This is the numerical counterpart of layering not closing paths on its own.** Three settings
strengthened three separate boundaries, and all three did real work, but the measured result did
not change; because what is measured is not the average of the boundaries but **the items opened
by the weakest one**. A hardening sequence should start from the loose boundary that opens the
most items — the model's sequence did the opposite, and its cost was gaining nothing across three
whole steps.

The fourth table gives the remaining reading. With all settings enabled, the threshold
distribution is 1, 3, and 4 components: one component's threshold is still 1. That component is
the network interface, and the items it opens number 4 — a narrow boundary. Also, **two** of the
eight components each open the entire inventory on their own; these two components are the items
that must be counted first on every machine in the measurement network.

#### Summary

- A component's security value is three numbers: the boundary's name and side, the number of
  items that must be simultaneously wrong to cross it, and the number of items reached when it
  falls.
- Across the 43-item inventory, the items opened by the eight components range from 3 to 43;
  firmware and the out-of-band management interface each open the entire inventory alone, because
  both sit beneath the boot chain or the management path.
- Over remote access the lowest threshold is 1 and opens 4 items; over physical access the lowest
  threshold is 0 and opens 43 items. On unencrypted storage, the filesystem permission is not a
  boundary but data sitting on the disk.
- A narrow, deep boundary (trusted platform module: 3 items, threshold 3) and a wide, shallow
  boundary (persistent storage: 22 items, threshold 0) can only be told apart in the same table
  if the two numbers are written separately.
- The threshold number assumes the items are independent. If the same authority or the same
  configuration item can change both, a two-item boundary is in fact a one-item boundary; the
  table gives an upper bound, not a lower one.
- Even after three of four settings are enabled, the items opened stay at 43; the number drops to
  4 only once the loose boundary that opens the most items is closed. What is measured is not the
  average of the boundaries but the weakest one.

#### Next Step

The boundaries hardware draws have been counted, and two of them were seen to open the entire
inventory. But hardware only **makes possible** most of the boundaries in the first table; the
code that decides which request passes through does not live in hardware. The processor offers
privilege mode, but the operating system chooses which process runs in it; the disk stores the
filesystem permission, but the operating system is again what honors that permission.

The next lesson descends into this decision layer and asks a single question: running on the
same hardware, how many items apart do different operating system families answer the same
access request.

### Operating System Families

Source: https://academia.sh/en/courses/it-fundamentals/operating-system-families

Operating system families told apart by their security model rather than their product name: 14 system requests giving a different result across three families in 4, 11, and 12 items, crossing the admin boundary costing 1, 2, and 3 items, the requests reached once the boundary falls being 13, 9, and 5, and the requests unreachable even as admin rising to 1, 1, and 9.

The previous lesson counted the boundaries hardware draws and left one distinction open: a
hardware boundary only **makes possible**. The processor offers privilege mode but does not
decide which code runs in it; persistent storage stores the permission bits, and whether to
honor them is the job of the software that mounts the disk. The layer that makes the decision is
the operating system.

When this layer is invoked, product names are usually listed off. A product name carries no
decision: names change, versions borrow one another's features, and two installations under the
same name can behave completely differently. What carries the decision is the family's
**security model** — the response it gives to the same request, and how many items it costs to
change that response.

#### Three Families, Three Models

In this lesson, families are referred to by their model.

**The family with discrete user and group permissions.** Every object has an owner and a group;
access is determined by the bits given to the owner–group–others classes. Privilege is
concentrated in a single supreme identity: whoever holds that identity can do everything.

**The family with access control lists.** An entry is kept per object, per subject; system
privileges are also **split** — changing the clock, taking a full-disk-read backup, touching the
log are items that can be granted separately. Being an admin does not automatically grant all of
these.

**The family based on a signed app store.** Executable code comes only from a signed source, and
every application runs in its own isolated area. Some requests, even if the user is an admin,
are **never granted by any means** in the model.

What the three definitions share is that all of them can run on the same hardware. The eight
boundaries from the previous lesson sit in the same place across all three families; what
changes is which request passes under which condition, on top of those boundaries. This is why
families can only be compared over **the same request set**: a comparison made with different
request lists measures the difference between the lists, not between the families.

#### The Same Request Set Across Three Families

The example is the field teams' laptop in the fictional **regional measurement network**. The team
and the request set are fictional; the matrix below is a **model** — there is no real
distribution, product, or vendor.

**HO7:** the request set is 14 items and is asked the same way across the three families.
**HO8:** the result takes four values — `grant` is given directly, `admin` is given by crossing
the admin boundary, `separate` is given with its own privilege item without becoming full admin,
`deny` is never given in that family, by any means. **HO9:** crossing the admin boundary requires
1 item in the discrete-permission family, 2 in the access-list family, 3 in the signed-store
family. **HO10:** every `separate`-result request additionally requires its own item. **HO11:**
the scenario has two stages — first a standard account is compromised, then the admin boundary
falls.

```js
// it/family.mjs — the same set of system requests across three operating system families
// (model): not a real distribution, product, or vendor. No randomness.

// Families are named by their security model. cross: items needed to cross the admin
// boundary; extraItem: items needed on top, for each "separate"-result request.
export const FAMILY = [
  { code: "discrete-permission", cross: 1, extraItem: 0 }, // family with discrete user and group permissions
  { code: "access-list", cross: 2, extraItem: 1 },          // family with access control lists
  { code: "signed-store", cross: 3, extraItem: 1 },         // family based on a signed app store
];

// Result: grant (directly), admin (by crossing the admin boundary), separate (with its own
// privilege item, without full admin), deny (never granted in this family).
export const REQUEST = `
  read-own-document               grant  grant     grant
  other-users-document              deny   deny      deny
  write-system-directory           admin  admin      deny
  install-software-from-store      admin  admin     grant
  run-unsigned-software            grant  grant      deny
  load-driver                      admin  admin      deny
  add-startup-item                 grant  grant     admin
  open-privileged-port             admin  admin      deny
  read-another-process-memory      admin  admin      deny
  change-system-clock              admin  separate    deny
  delete-log                       admin  separate    deny
  mount-disk-volume                admin  separate   admin
  camera-and-microphone            grant  grant     admin
  full-disk-read-backup            admin  separate    deny`
  .trim().split("\n").map((r) => {
    const [name, ...s] = r.trim().split(/\s+/);
    return { name, s: Object.fromEntries(FAMILY.map((f, i) => [f.code, s[i]])) };
  });

export const RESULT = ["grant", "admin", "separate", "deny"];
export const count = (f, c) => REQUEST.filter((i) => i.s[f.code] === c).length;

// Two-stage scenario: a standard account is compromised first, then the admin boundary falls.
export function stage(f) {
  const grant = count(f, "grant");
  const admin = count(f, "admin");
  const separate = count(f, "separate");
  return { grant, boundaryFalls: grant + admin, all: grant + admin + separate, cross: f.cross,
    items: f.cross + separate * f.extraItem, deny: count(f, "deny") };
}
```

```js
// it/familymeasure.mjs — how many items the same request set diverges across three families, and what opens once the boundary falls.
import { FAMILY, REQUEST, RESULT, count, stage } from "./family.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const K = FAMILY.map((f) => f.code);

console.log(`${REQUEST.length} system requests x ${FAMILY.length} families; result: ` +
  "grant directly, admin by crossing the admin boundary, separate with its own item, deny never");

const A = [-36, 21, 16, 14];
console.log("\n1. the same request set across three families");
write(A, "request", ...K);
for (const i of REQUEST) write(A, i.name, ...K.map((k) => i.s[k]));

const B = [-36, 21, 16, 14];
console.log("\n2. count per family");
write(B, "measure", ...K);
for (const c of RESULT) write(B, `${c}-result requests`, ...FAMILY.map((f) => count(f, c)));
const R = FAMILY.map(stage);
for (const [name, f] of [["crossing the admin boundary: items", (r) => r.cross],
  ["reaching everything: items", (r) => r.items],
  ["reached with a standard account", (r) => r.grant],
  ["reached once the boundary falls", (r) => r.boundaryFalls],
  ["reached if every item is taken", (r) => r.all],
  ["unreachable even as admin", (r) => r.deny]]) write(B, name, ...R.map(f));

const C = [-36, 21];
console.log("\n3. pairwise comparison (requests with a different result)");
write(C, "pair", "different requests");
for (let x = 0; x < K.length; x++)
  for (let y = x + 1; y < K.length; y++)
    write(C, `${K[x]} - ${K[y]}`,
      REQUEST.filter((i) => i.s[K[x]] !== i.s[K[y]]).length);
write(C, "same across all three", REQUEST.filter((i) => new Set(K.map((k) => i.s[k])).size === 1).length);
write(C, "different across all three", REQUEST.filter((i) => new Set(K.map((k) => i.s[k])).size === 3).length);
```

```
14 system requests x 3 families; result: grant directly, admin by crossing the admin boundary, separate with its own item, deny never

1. the same request set across three families
request                               discrete-permission     access-list  signed-store
read-own-document                                   grant           grant         grant
other-users-document                                 deny            deny          deny
write-system-directory                              admin           admin          deny
install-software-from-store                         admin           admin         grant
run-unsigned-software                               grant           grant          deny
load-driver                                         admin           admin          deny
add-startup-item                                    grant           grant         admin
open-privileged-port                                admin           admin          deny
read-another-process-memory                         admin           admin          deny
change-system-clock                                 admin        separate          deny
delete-log                                          admin        separate          deny
mount-disk-volume                                   admin        separate         admin
camera-and-microphone                               grant           grant         admin
full-disk-read-backup                               admin        separate          deny

2. count per family
measure                               discrete-permission     access-list  signed-store
grant-result requests                                   4               4             2
admin-result requests                                   9               5             3
separate-result requests                                0               4             0
deny-result requests                                    1               1             9
crossing the admin boundary: items                      1               2             3
reaching everything: items                              1               6             3
reached with a standard account                         4               4             2
reached once the boundary falls                        13               9             5
reached if every item is taken                         13              13             5
unreachable even as admin                               1               1             9

3. pairwise comparison (requests with a different result)
pair                                   different requests
discrete-permission - access-list                       4
discrete-permission - signed-store                     11
access-list - signed-store                             12
same across all three                                   2
different across all three                              3
```
The numbers belong to the **measurement** class; their inputs are HO7–HO11.

#### How Many Items Is the Admin Boundary

The fifth and sixth rows of the second table give this lesson's measure. Crossing the admin
boundary is **1** item in the discrete-permission family, **2** in the access-list family, **3**
in the signed-store family. But the row below is different: the items required to reach **every
request** are 1, **6**, and 3.

The difference comes from the splitting of privilege. In the discrete-permission family, crossing
to the supreme identity is one item, and that item grants everything; the cost of crossing the
boundary and the cost of reaching everything are the same number. In the access-list family,
becoming admin does not additionally grant four requests; each of them requires its own privilege
item, and the total rises to 6. **Split privilege does not raise the height of the boundary — it
lengthens the distance beyond it.**

Seeing this distinction requires the two numbers to be written separately. The single question
"is becoming admin hard" gives the sequence 1, 2, 3 and makes the three families look nearly
equal; once the second number is added, the sequence becomes 1, 6, 3, and the ranking changes.

#### What Opens When the Boundary Falls

The third column group reads the scenario's second stage. When a standard account is
compromised, the requests reached directly are 4, 4, and 2 across the three families — nearly the
same. When the admin boundary falls, the numbers diverge: **13, 9, and 5**.

In the discrete-permission family, 13 of 14 requests open. A single item grants nearly the entire
inventory; this is the software-side counterpart of the pattern seen in the firmware row in the
previous lesson. In the access-list family, the requests that open at the same time are 9, and
the remaining 4 requests each need one more separate item. In the signed-store family, even when
the boundary falls, the requests opened stay at 5, because the remaining 9 requests open with no
item at all, in this model.

The last row writes this directly: the number of requests unreachable even as admin is **1, 1,
and 9**. This is the sharpest difference among the three families, and it comes from a single
design decision — whether the highest point of privilege grants everything or not.

The scenario's second stage is an account's authority turning into more than the system permits;
this step is called **privilege escalation**. What is measured here is not how the escalation is
carried out but **the number of requests that open once it is assumed to have happened**; the
step itself is a model, and this lesson contains no working example of it. Attempting escalation
on a system, without written authorization and a defined scope, is both illegal and
unprofessional.

#### The Item the Model Does Not See

The `deny` value in the table is absolute in the model: that request opens with no item at all.
In a real installation it is not absolute. In the signed-store family, the signing key itself,
enterprise enrollment profiles, developer mode, and permission to install outside the store are
each an item; if one of them is enabled, part of the `deny` results turns into `admin`, and the
9 in the last row drops.

This is not a flaw in the model but a limit, and it determines how the model should be used.
**The table measures not a family but a family's default configuration.** Applying the number to
a machine starts with two questions: how many items on this machine are enabled that turn `deny`
results into `admin`, and who can change those items. If the answer to the second question is
"the admin," the signed-store family's 9 is not really 9.

The same limitation works in the other direction too. The "requests unreachable even as admin"
number, written as 1 in the discrete-permission family, rises in an installation with a mandatory
access control layer added; that layer is another way of splitting privilege. **The family name
gives an upper bound; the installation gives the real number.** This is why the next lesson
measures the installation itself, not the family.

#### The Same Family, Two Installations

All of the measurement network's field laptops belong to the same family, but not all of them give
the same number. If one uses a standard user account and another does its daily work with the
admin account, the scenario's first stage starts directly at the second stage on the latter: the
compromised account is already on the far side of the boundary, and the item required to cross
the admin boundary is not 1 but **0**.

This is the software-side counterpart of the zero threshold from the previous lesson. Zero says
not that the boundary is weak but that it was never established. In this case, the choice of
family changes nothing: even in the strictest model, doing daily work with the admin account
moves the table's `admin` column into the `grant` column, and the 4, 11, 12 gap among the three
families is largely erased.

#### The Cost: Legitimate Work That Cannot Be Done

The same row, read in reverse, gives the cost of the defense. Among the 9 requests unreachable in
the signed-store family are loading a driver, taking a full-disk-read backup, and reading another
process's memory. All three of these are legitimate administrative work: in the measurement network,
a meter-reading device's driver must be loaded, a field laptop's full backup must be taken, and a
process's memory must be read while investigating a crash.

The model does not count these tasks as "impossible" — it counts them as **done somewhere else**:
the driver is signed and enters the store, the backup is taken with a permission granted per
application, the crash record is collected by the operating system's own mechanism. The cost is
not that the work disappears but that **each of it is tied to its own approved path** — and the
number of those paths is 9. In the discrete-permission family this cost is 1; against that, the
requests opened once the boundary falls number 13.

The third table shows where this trade-off concentrates. The first two families differ from each
other in only **4** requests, and all four of those concern the splitting of privilege. The
signed-store family differs from the others in **11** and **12** requests. The request that gives
the same result across all three families is only **2**: reading one's own document and reading
another user's document. The common ground is nothing more than the distinction between a user's
own data and someone else's; everything beyond that depends on the family.

The trade-off does not read in only one direction. In the measurement network, the field laptop and
the branch server look at the same table from different places. Code run on the laptop mostly
comes from outside, and the user does not maintain the system; there, a high `deny` count is a
gain, not a cost. On the branch server, however, loading drivers, taking full backups, and
inspecting memory are the job itself; the same number 9 there means nine separate exception
paths, and each exception path is its own item. **The same number is a closed path on one machine
and a newly opened configuration item on the other.** The choice of family is therefore made per
machine class, not per organization.

#### Summary

- An operating system family is told apart by its security model, not its product name: the
  response it gives to the same request, and how many items it costs to change that response.
- 14 requests give a different result across the three families in 4, 11, and 12 items; the
  request that gives the same result in all three is only 2.
- Crossing the admin boundary is 1, 2, and 3 items; reaching every request is 1, 6, and 3 items.
  Split privilege does not raise the height of the boundary — it lengthens the distance beyond it.
- The requests reached once the boundary falls are 13, 9, and 5. If privilege's highest point
  grants everything, a single item opens 13 of 14 requests.
- The cost is the number of requests unreachable even as admin: 1, 1, and 9. Most of the nine
  requests closed in the signed-store family are legitimate administrative work, and each is tied
  to its own approved path.
- The table measures not a family but the family's default configuration. If daily work is done
  with the admin account, the item required to cross the admin boundary drops to 0, and the
  difference among the families is erased.

#### Next Step

All three families bring their model with an installation; the results in the table are that
installation's default values. But a default value is not a choice: it is what remains when the
person doing the installation selects nothing.

The next lesson looks at that moment. When a clean install finishes, how many items remain open,
how many close during initial hardening, and how many can only be closed if the machine is
reinstalled.

### Installation and Configuration

Source: https://academia.sh/en/courses/it-fundamentals/installation-and-configuration

Measuring the clean install itself: 17 of 20 configuration items staying risky by default, baseline hardening closing 8 of them, 4 closing only by reinstalling, and 1 never closing; when hardening happens before connecting to the network, the open items in the window drop from 17 to 9, and to 5 once later items are closed too.

The previous lesson compared three operating system families over the same request set and left
a warning in its final section: the results in the table belong to the family's **default
configuration**. A default is not a choice; it is what remains when the person doing the install
selects nothing.

This lesson measures that state. When a clean install finishes, how many items remain risky, how
many of them can be closed right away, how many close only if the machine is reinstalled, and how
many never close.

#### Installation Is Not a Moment, It Is a Set of Decisions

Installation is usually spoken of as a single event: the machine was installed. Seen from a
measurement standpoint, installation is a **set of decisions**, and not all of those decisions
can be undone at the same point in time.

Some items are settled only during installation. Encrypting a volume later means moving the
data; changing the partition layout later means rebuilding the filesystem; the key that
generates the machine's identity is generated at first boot, and if a weak source was used at
that moment, fixing it later means changing the identity. For these items, "we will do it later"
is not an option — it means **reinstalling**.

Some items close during the **baseline hardening** right after installation: default accounts,
default passwords, services left open. Some can be closed even while the machine is in use. One
never closes.

Counting these four moments separately turns the sentence "the install was hardened" into a
number.

#### Twenty Items, Four Moments

The example is a newly installed branch server in the fictional **regional measurement network**.
Both the server and the item list are fictional; the table below is a **model** — there is no
real distribution, install tool, or vendor.

**HO12:** the configuration item set is 20, and each item carries a closing time. **HO13:** the
closing time takes four values — `install` only at install time, `initial` during baseline
hardening, `later` while the machine is in use, `never` at no time. **HO14:** the state a clean
install leaves is either risky or safe. **HO15:** `opens` is the number of items reached if the
item stays open; items are not summed because they can touch the same assets, the **highest** is
written instead. **HO16:** the window is the set of risky items not yet closed while the machine
is connected to the network.

```js
// it/install.mjs — configuration items a clean install leaves behind (model): not a real
// distribution, install tool, or vendor. No randomness.

// time: when the item can be closed — install (only at install time; changing it later
// means reinstalling), initial (during the baseline hardening right after install), later
// (whenever wanted), never (cannot be closed, only managed). risky: whether the state a
// clean install leaves is risky. opens: items reached if the item stays open.
export const ITEM = `
  disk-encryption               install  risky   11
  separate-data-partition       install  risky    6
  boot-signature-record         install  risky    9
  machine-key-generation        install  risky    3
  install-source-verification   install  safe    14
  firmware-admin-password       initial  risky   12
  default-admin-account         initial  risky    8
  default-password              initial  risky    8
  remote-management-service     initial  risky    7
  unnecessary-service-set       initial  risky    5
  firewall-default               initial  risky    6
  automatic-login                initial  risky    4
  guest-account                  initial  safe     3
  log-collection                 initial  risky    4
  time-synchronization           initial  safe     2
  screen-lock-timeout            later    risky    2
  software-source-restriction    later    risky    5
  backup-schedule                 later    risky    9
  driver-signature-requirement    later    risky    6
  hardware-error-channel           never    risky    1`
  .trim().split("\n").map((r) => {
    const [name, time, safe, opens] = r.trim().split(/\s+/);
    return { name, time, risky: safe === "risky", opens: Number(opens) };
  });

export const TIME = ["install", "initial", "later", "never"];
export const risky = ITEM.filter((k) => k.risky);
export const countTime = (t, all = ITEM) => all.filter((k) => k.time === t).length;

// Window: risky items still open, given the phases already closed.
export function openWindow(closed) {
  const open = risky.filter((k) => !closed.includes(k.time));
  return { count: open.length, worst: Math.max(...open.map((k) => k.opens)),
    worstName: open.reduce((a, b) => (b.opens > a.opens ? b : a)).name };
}
```

```js
// it/installmeasure.mjs — how many items a clean install leaves open, when each closes, and what the sequence changes.
import { ITEM, TIME, risky, countTime, openWindow } from "./install.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

console.log(`${ITEM.length} configuration items; risky items a clean install leaves behind: ` +
  `${risky.length}; closing time: ${TIME.join(", ")}`);

const A = [-30, 11, 12, 8];
console.log("\n1. closing time per item (opens: items reached if the item stays open)");
write(A, "item", "time", "default", "opens");
for (const k of ITEM) write(A, k.name, k.time, k.risky ? "risky" : "safe", k.opens);

const B = [-30, 11, 12, 8];
console.log("\n2. distribution by closing time");
write(B, "time", "items", "risky", "worst");
for (const t of TIME) {
  const r = risky.filter((k) => k.time === t);
  write(B, t, countTime(t), r.length, r.length ? Math.max(...r.map((k) => k.opens)) : 0);
}
write(B, "total", ITEM.length, risky.length, Math.max(...risky.map((k) => k.opens)));

const C = [-33, -29, 12, 9];
console.log("\n3. two install sequences (window: risky items open while connected)");
write(C, "sequence", "worst item in the window", "open items", "opens");
for (const [name, closed] of [["install -> connect -> harden", []],
  ["install -> harden -> connect", ["initial"]],
  ["  + later items closed too", ["initial", "later"]]]) {
  const s = openWindow(closed);
  write(C, name, s.worstName, s.count, s.worst);
}

const D = [-36, 12];
console.log("\n4. what remains after hardening finishes");
write(D, "measure", "items");
write(D, "closed during initial hardening", risky.filter((k) => k.time === "initial").length);
write(D, "closable later", risky.filter((k) => k.time === "later").length);
write(D, "closable only by reinstalling", risky.filter((k) => k.time === "install").length);
write(D, "never closes", risky.filter((k) => k.time === "never").length);
write(D, "worst opened by install-time items",
  Math.max(...risky.filter((k) => k.time === "install").map((k) => k.opens)));
```

```
20 configuration items; risky items a clean install leaves behind: 17; closing time: install, initial, later, never

1. closing time per item (opens: items reached if the item stays open)
item                                 time     default   opens
disk-encryption                   install       risky      11
separate-data-partition           install       risky       6
boot-signature-record             install       risky       9
machine-key-generation            install       risky       3
install-source-verification       install        safe      14
firmware-admin-password           initial       risky      12
default-admin-account             initial       risky       8
default-password                  initial       risky       8
remote-management-service         initial       risky       7
unnecessary-service-set           initial       risky       5
firewall-default                  initial       risky       6
automatic-login                   initial       risky       4
guest-account                     initial        safe       3
log-collection                    initial       risky       4
time-synchronization              initial        safe       2
screen-lock-timeout                 later       risky       2
software-source-restriction         later       risky       5
backup-schedule                     later       risky       9
driver-signature-requirement        later       risky       6
hardware-error-channel              never       risky       1

2. distribution by closing time
time                                items       risky   worst
install                                 5           4      11
initial                                10           8      12
later                                   4           4       9
never                                   1           1       1
total                                  20          17      12

3. two install sequences (window: risky items open while connected)
sequence                         worst item in the window       open items    opens
install -> connect -> harden     firmware-admin-password                17       12
install -> harden -> connect     disk-encryption                         9       11
  + later items closed too       disk-encryption                         5       11

4. what remains after hardening finishes
measure                                    items
closed during initial hardening                8
closable later                                 4
closable only by reinstalling                  4
never closes                                   1
worst opened by install-time items            11
```
The numbers belong to the **measurement** class; their inputs are HO12–HO16.

#### What a Clean Install Leaves Behind

The header line gives the first number: **17** of 20 items come out of a clean install in a
risky state. Three are safe — install source verification, the guest account being closed, and
time synchronization being on.

This ratio explains why hardening is a separate job. The install tool does not make a security
decision; it leaves behind a working system. A working system's defaults are chosen in the
direction that makes the install easier to **count as successful**: services on, accounts ready,
remote access usable. Each of these choices is defensible on its own; together they add up to 17
items.

The three items that come out safe are not random either. Install source verification is the
install tool's own job, and if it is not verified the entire installed system is called into
question — this is why the number it opens, 14, is the highest on the list, and why it is on by
default for the same reason. Time synchronization is required for records to be comparable. The
guest account coming closed is the reversal of an old default: an item's default changes
according to how much trouble that item has caused in the past.

The second table gives the distribution. Ten items can be closed during baseline hardening, and
8 of these are risky. Five items are settled only at install time, and four of them are risky.
Four items close while the machine is in use. One item never closes.

The rows' `worst` column also gives a ranking. The worst item in baseline hardening opens 12
assets, and this is the highest risky value on the list; the worst item at install time is 11,
and the worst among the later items is 9. So the riskiest item that opens the most assets sits in
the cheapest-to-close class. This ranking determines the hardening sequence: the items with the
highest `opens` value are closed first, because the gap between their cost and their payoff to
close is the largest.

#### The Item Missed at Install Time

The third row of the fourth table gives this lesson's most expensive number: **4 items close
only by reinstalling**, and the worst item count they open is 11.

What these four items share is that they correspond to a structure that cannot be added to the
system later. Disk encryption is not a setting; it is how the volume was written. The separate
data partition is not a preference; it is where the filesystem begins and ends. The boot
signature record is the trusted key set of the boot chain counted in the previous lesson, and
once the chain is built without that record, no layer above it can go back and add it. The key
that generates the machine's identity is generated at first boot.

The practical consequence of the number is this: for these four items, **the checklist cannot be
run after installation**. An audit performed after installation finds the remaining 13 risky
items and writes the other four as "missing" — but the cost of closing those four is not a
setting change, it is reinstalling the machine. Considered across the measurement network's forty
branch servers, this means reinstalling forty machines, not changing four settings.

#### The Window: Connecting to the Network Before Hardening

The third table measures the cost of the sequence. If the machine is installed and **connected to
the network first**, then hardened, 17 risky items are open in the window, and the worst item
opens 12 assets. If hardening happens before connecting to the network, the open items drop to 9
and the worst item falls to 11. If the items that can be closed later are also closed before
connecting, the open items drop to **5**.

The difference is achieved without adding a single new control. The items closed are the same
across all three sequences; the only thing that changes is **when** they are closed. This is a
different kind of gain than the threshold measurements in the previous lessons: the boundary was
not strengthened — the time during which the boundary was not yet established was shortened.

The window never drops to zero in any sequence. Even in the best sequence, 5 risky items are
still open, and the worst of them opens 11 assets; these are the items settled at install time.
The only way to close the window is to close the items **during installation itself** — that is,
to stop treating hardening as a step after installation and move it into the installation
itself.

#### The Distinction Between an Install Profile and a Checklist

Reading the third table has a direct consequence. **12** of the risky items — the 4 belonging to
install time plus the 8 belonging to baseline hardening — can be settled before the machine is
powered on for the first time. When these are written not as a checklist entry but as part of the
installation itself, the window never opens at all.

The distinction is this. A **checklist** asks about items one by one on an already installed
machine and reports what is missing; each query happens at a point in time, and the time elapsed
up to that point is the window. An **install profile** carries the same items as decisions
applied during installation; when the machine is installed, the items are already closed, and
the window is zero.

The scope of the two approaches also differs. A checklist can ask about all 20 items but can fix
only 12 of them — the items closable in baseline hardening plus the ongoing items, that is, 8
plus 4. An install
profile closes 12 items and leaves behind the ongoing items and the item that cannot be closed.
**The two do not substitute for each other:** the profile closes the window, the checklist
catches items that open over time. If an install has only one of the two, the number measured is
incomplete.

This distinction has a cost too. Writing an install profile requires finding, for each of the 12
items, which option the install tool uses to set that item; this is a one-time job done per
machine class. A checklist, by contrast, runs on every pass and rediscovers the same gaps on
every new machine — the number that measures the gap between finding and closing is the fix
repeated forty times on forty machines.

#### The Same Item, Two Machine Classes

The `risky` marker in the table is not independent of the machine class. The measurement network
has two classes, and the same item falls in a different place in each.

On the branch server, `remote-management-service` is a requirement; it cannot be closed, only
narrowed — in this case the item leaves the `initial` class and turns into a continuously managed
item. On the field laptop, the service is not needed at all and is genuinely closed. Conversely,
`disk-encryption` is the most important install item on the laptop, because the device can be
physically lost; on a server in a locked branch room, the same item's 11 assets sit behind
another boundary too.

This is why the measurement is done per machine class, and two numbers are kept separate: the
assets the item **itself** opens, and **which other boundaries** those assets sit behind. When a
single organization-wide list is applied to both classes at once, it produces unnecessary work in
one class and incomplete closing in the other.

#### The Item That Never Closes

The last row of the fourth table sets one item apart: `hardware-error-channel`. This item cannot
be closed, only managed. A defect present in the hardware is not a configuration option; what
corresponds to it are the mitigations that come with firmware and operating system updates. How
updates are scheduled is a separate subject and is not measured in this course; what is counted
here is only **that an item exists that cannot be closed**.

An item falling into this class changes the measurement. If it stands in a checklist as a row
waiting for a "closed" mark, it shows up as missing on every audit and is eventually ignored.
Written as a separate class instead, it asks the right question: since this item does not close,
which other boundary does the 1 asset it opens sit behind. The number being small matters here —
if the item that cannot be closed opens a small surface, layered defense does its job; if it
opens a large surface, the machine's role must change.

#### Summary

- A clean install makes no security decision: 17 of 20 configuration items stay risky by
  default, and only 3 are safe.
- Items split into four by closing time: 8 risky items close during baseline hardening, 4 close
  while the machine is in use, 4 close only by reinstalling, and 1 never closes.
- The four items settled at install time cannot be fixed later with a setting change; a gap an
  audit finds means reinstalling the machines involved.
- Sequence alone yields a measurable gain: when hardening happens before connecting to the
  network, the open items in the window drop from 17 to 9, and to 5 once the later items are
  closed too.
- The window never zeroes out in any sequence; the remaining 5 items belong to install time. The
  only way to close the window is to move hardening into the installation itself.
- The item that cannot be closed is written as its own class; kept as a row that constantly shows
  up missing on a checklist, it gets ignored.

#### Next Step

When installation finishes, a permission scheme has already been established on the machine:
accounts, groups, and who has access to what on which object. This scheme was the source of the
difference between families in the previous lesson, and several of the 17 items counted in this
lesson set it directly.

The next lesson descends into that scheme and performs the topic's most detailed measurement:
the same access request set is run through two separate permission models, how many requests
give a different result is counted, and which request each model **cannot express** is written
down.

### File Permissions and Ownership

Source: https://academia.sh/en/courses/it-fundamentals/file-permissions-and-ownership

Running the same 70 access requests through two permission models: the permission-bits model's loose expression producing 6 excess grants, its strict expression 6 excess denials, the two expressions differing from each other in 12 requests, the access control list expressing the policy in full yet 6 requests flipping when the entry order changes, and ownership sitting outside both models.

The previous lesson counted the 17 risky items a clean install leaves and noted that several of
them directly set the permission scheme. When installation finishes, the machine already has a
permission scheme: accounts, groups, and who has access to what on which object.

This lesson descends into that scheme. The mechanics of permission bits — three classes, three
access types, octal notation, the exclusivity of class selection — were established in the
Introduction to Linux course and are not repeated here. The question here is different: **when
the same policy is expressed in two separate permission models, how many requests give a
different result, and which request can each model not express at all.**

#### What Is Measured Is Not Mechanics but Expressive Power

A permission model does two jobs at once. The first is responding to an access request; this is
mechanical, and it is learned. The second is **being able to carry** the policy the organization
has in mind; this is the model's expressive power, and it is limited.

When the two jobs are conflated, the measurement is lost. "The permissions were set correctly" is
the mechanical job. The sentence that carries a decision is this: can this policy be expressed
**exactly** in this model, and if not, how many requests does the nearest expression cost, and
which way does that cost fall — toward permission granted in excess, or toward legitimate work
blocked by a denial made in excess.

#### Policy, Two Models, and 70 Requests

The example is five files on the branch server of the fictional **regional measurement network**.
The subjects, files, and policy are fictional; the evaluation below is a **model** — there is no
real system or product.

**HO17:** seven subjects, five objects, and two operations produce 70 requests; `intent` is the
result the policy wants. **HO18:** the permission-bits model carries three classes, and class
selection is exclusive. **HO19:** for each object, the group with **the least deviation** is
chosen; the model is given its best chance. **HO20:** two expression strategies are tried —
`loose` gives the others class the union of the outside needs; `strict` leaves the others class
empty. **HO21:** in the access control list model, the first matching entry decides; the list is
evaluated both in the order it was written and in reverse. **HO22:** deviation is counted in two
directions — excess grant and excess deny are separate columns.

```js
// it/permission.mjs — the same access request set run through two permission models (model):
// fictional measurement network files, no real system or product. No randomness.

export const SUBJECT = { "field-technician": "field", "branch-manager": "branch",
  "collector-service": "service", "billing-service": "service", "data-analyst": "analytics",
  "auditor": "audit", "guest-device": "guest" };
export const GROUP = [...new Set(Object.values(SUBJECT))];
export const OPERATION = ["r", "w"];

// intent: the access the policy wants. acl: the list the admin wrote (deny entries first).
export const OBJECT = [
  { name: "raw-read", owner: "collector-service",
    intent: { "collector-service": "rw", "field-technician": "r", "branch-manager": "r",
      "data-analyst": "r", "auditor": "r" },
    acl: [["deny", "billing-service", "rw"], ["allow", "service", "rw"], ["allow", "field", "r"],
      ["allow", "branch", "r"], ["allow", "analytics", "r"], ["allow", "audit", "r"]] },
  { name: "verified-read", owner: "data-analyst",
    intent: { "data-analyst": "rw", "billing-service": "r", "branch-manager": "r",
      "auditor": "r" },
    acl: [["deny", "collector-service", "rw"], ["allow", "service", "r"], ["allow", "analytics", "rw"],
      ["allow", "branch", "r"], ["allow", "audit", "r"]] },
  { name: "invoice-draft", owner: "billing-service",
    intent: { "billing-service": "rw", "branch-manager": "r", "auditor": "r" },
    acl: [["deny", "collector-service", "rw"], ["allow", "service", "rw"], ["allow", "branch", "r"],
      ["allow", "audit", "r"]] },
  { name: "key-file", owner: "collector-service",
    intent: { "collector-service": "r" },
    acl: [["deny", "billing-service", "rw"], ["allow", "service", "r"]] },
  { name: "audit-log", owner: "auditor",
    intent: { "auditor": "r", "collector-service": "w", "billing-service": "w" },
    acl: [["allow", "service", "w"], ["allow", "audit", "r"]] },
];

export const REQUEST = Object.keys(SUBJECT).flatMap((o) =>
  OBJECT.flatMap((n) => OPERATION.map((i) => ({ o, n, i }))));
export const intent = (n, o, i) => (n.intent[o] || "").includes(i);

// Permission-bits model: three classes, class choice is exclusive (established in M03/K01).
const combine = (n, who) => who.filter((o) => o !== n.owner)
  .map((o) => n.intent[o] || "").join("");
export function bits(n, group, loose) {
  const member = Object.keys(SUBJECT).filter((o) => SUBJECT[o] === group);
  const outside = Object.keys(SUBJECT).filter((o) => SUBJECT[o] !== group && o !== n.owner);
  return { group, owner: n.intent[n.owner] || "", g: combine(n, member),
    others: loose ? combine(n, outside) : "" };
}
export const bitsResult = (b, n, o, i) =>
  (o === n.owner ? b.owner : SUBJECT[o] === b.group ? b.g : b.others).includes(i);

// Best group choice: the group with the least deviation. Deviation = excess grant + excess deny.
export function best(n, loose) {
  return GROUP.map((g) => bits(n, g, loose))
    .map((b) => ({ b, ...deviation((o, i) => bitsResult(b, n, o, i), [n]) }))
    .reduce((a, x) => (x.excessGrant + x.excessDeny < a.excessGrant + a.excessDeny ? x : a));
}

// Access control list model: the first matching entry decides.
export const aclResult = (n, o, i, reversed) => {
  const l = reversed ? [...n.acl].reverse() : n.acl;
  const g = l.find(([, who, ops]) => (who === o || who === SUBJECT[o]) && ops.includes(i));
  return g ? g[0] === "allow" : false;
};

export function deviation(f, objects = OBJECT) {
  const k = REQUEST.filter((x) => objects.includes(x.n));
  return { excessGrant: k.filter((x) => f(x.o, x.i, x.n) && !intent(x.n, x.o, x.i)).length,
    excessDeny: k.filter((x) => !f(x.o, x.i, x.n) && intent(x.n, x.o, x.i)).length };
}
```

```js
// it/permissionmeasure.mjs — the same request set in two permission models: how many requests differ, how many the model cannot express.
import { SUBJECT, OBJECT, REQUEST, intent, bitsResult, best, aclResult, deviation } from "./permission.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));

const B = { loose: {}, strict: {} };
for (const n of OBJECT) for (const s of ["loose", "strict"]) B[s][n.name] = best(n, s === "loose").b;
const bitsFn = (s) => (o, i, n) => bitsResult(B[s][n.name], n, o, i);
const MODEL = [["permission-bits (loose)", bitsFn("loose")], ["permission-bits (strict)", bitsFn("strict")],
  ["access-list (deny-first)", (o, i, n) => aclResult(n, o, i, false)],
  ["access-list (allow-first)", (o, i, n) => aclResult(n, o, i, true)]];

const granted = REQUEST.filter((x) => intent(x.n, x.o, x.i)).length;
console.log(`${Object.keys(SUBJECT).length} subjects x ${OBJECT.length} objects x 2 operations = ` +
  `${REQUEST.length} requests; granted in intent ${granted}, denied ${REQUEST.length - granted}`);

const A = [-28, 14, 13, 16];
console.log("\n1. how far each model deviates from intent");
write(A, "model", "excess grant", "excess deny", "same as intent");
for (const [name, f] of MODEL) {
  const s = deviation(f);
  write(A, name, s.excessGrant, s.excessDeny, REQUEST.length - s.excessGrant - s.excessDeny);
}

const C = [-20, -14, -14, 14, 13];
console.log("\n2. leftover per object in the permission-bits model (with the best group choice)");
write(C, "object", "loose group", "strict group", "excess grant", "excess deny");
for (const n of OBJECT) {
  const g = best(n, true);
  const s = best(n, false);
  write(C, n.name, g.b.group, s.b.group, g.excessGrant, s.excessDeny);
}

const D = [-56, 11];
console.log("\n3. difference between models (requests with a different result)");
write(D, "pair", "different");
for (let x = 0; x < MODEL.length; x++)
  for (let y = x + 1; y < MODEL.length; y++)
    write(D, `${MODEL[x][0]} / ${MODEL[y][0]}`,
      REQUEST.filter((r) => MODEL[x][1](r.o, r.i, r.n) !== MODEL[y][1](r.o, r.i, r.n)).length);

const E = [-24, 14, 17];
console.log("\n4. where the extra access goes");
write(E, "subject", "loose bits", "allow-first acl");
for (const o of Object.keys(SUBJECT)) {
  const f = (g) => REQUEST.filter((x) => x.o === o && g(x.o, x.i, x.n) && !intent(x.n, x.o, x.i)).length;
  const a = f(bitsFn("loose"));
  const b = f((s, i, n) => aclResult(n, s, i, true));
  if (a || b) write(E, o, a, b);
}
```

```
7 subjects x 5 objects x 2 operations = 70 requests; granted in intent 19, denied 51

1. how far each model deviates from intent
model                         excess grant  excess deny  same as intent
permission-bits (loose)                  6            0              64
permission-bits (strict)                 0            6              64
access-list (deny-first)                 0            0              70
access-list (allow-first)                6            0              64

2. leftover per object in the permission-bits model (with the best group choice)
object              loose group   strict group    excess grant  excess deny
raw-read            service       field                      1            3
verified-read       field         branch                     2            2
invoice-draft       field         branch                     3            1
key-file            field         field                      0            0
audit-log           service       service                    0            0

3. difference between models (requests with a different result)
pair                                                      different
permission-bits (loose) / permission-bits (strict)               12
permission-bits (loose) / access-list (deny-first)                6
permission-bits (loose) / access-list (allow-first)               8
permission-bits (strict) / access-list (deny-first)               6
permission-bits (strict) / access-list (allow-first)             12
access-list (deny-first) / access-list (allow-first)              6

4. where the extra access goes
subject                     loose bits  allow-first acl
collector-service                    2                3
billing-service                      0                3
data-analyst                         1                0
guest-device                         3                0
```
The numbers belong to the **measurement** class; their inputs are HO17–HO22.

#### What Permission Bits Cannot Express

The first two rows of the first table are this lesson's main finding. The permission-bits model
resolves 64 of 70 requests the same way as intent. The remaining 6 requests are wrong in both
strategies. The direction differs: the loose expression produces **6 excess grants**, the strict
expression **6 excess denials**.

This not being zero is not a setting mistake. The code tries all five groups for each object and
picks the one with the least deviation; the model has been given its best chance. The leftover
comes from the model's **structure**: there are three classes, and a subject can only be reached
through one of those three. If the distinction the policy wants does not fit into three classes —
if readers sit in four separate groups and one subject must be kept out — only an approximate
expression remains.

The second table shows where the leftover accumulates. `key-file` and `audit-log` both give zero
leftover in either strategy; in both, the access set falls within a single owner and a single
group. `invoice-draft` produces 3 excess grants in the loose expression, because the only way to
cover two readers in two different groups is to open the others class, and that class holds three
more subjects.

The fourth table writes down who the cost goes to. One of the subjects that gains excess access
in the loose expression is `guest-device`, and it gains **3 requests**. This subject has no
access to any object under the policy; the model has no place to keep it out, because the
"others" class covers everyone. **What the permission-bits model cannot express is not a
permission — it is an exception.**

#### The Direction of the Approximation Is Chosen, Its Existence Is Not

The first row of the third table compares the two strategies: the loose and strict expressions
differ from each other in **12 requests**. Although the total deviation is 6 in both, the number
of requests where they diverge is double that, because the deviations sit on different requests
and point in opposite directions.

This is the manageable part of an approximate expression. If the model cannot express the policy
exactly, the administrator picks a direction. The loose direction does not stop work but opens
out-of-policy access on 6 requests. The strict direction grants no excess access but denies 6
legitimate requests; in the measurement network, this means the auditor cannot read the invoice
draft and the branch manager cannot access the raw read.

What cannot be chosen is **the deviation itself**. The total deviation is 6 in both directions;
which strategy is chosen only decides where the cost is written. The question to ask when
choosing a permission model is not "which one is safer" but "in how many requests can my policy
not be expressed in this model, and which direction can I write the cost of those requests to."

#### What an Access Control List Brings, and What It Does Not

The third row of the first table is zero: the access control list model, when deny entries are
evaluated first, resolves all 70 of 70 requests the same way as intent. Because an entry can be
written per subject, the three-class limit disappears, and the `guest-device` problem never
arises.

The fourth row, however, shows the cost. The same list produces **6 excess grants** when the
entries are evaluated in reverse order. The last row of the third table writes this directly: the
two orderings differ from each other in 6 requests. These 6 requests belong to subjects who are
the target of a group-level grant but must be kept out by an individual deny entry —
`collector-service` and `billing-service` each gain 3 requests in the fourth table.

The difference here is not a typo — it is that the model's second axis is **order**. In the
permission-bits model, an object's permission state is a single value, and reading it determines
the result; in an access control list, the same policy can be written in different orders, and
two lists carrying the same entries give different results. **As expressive power grows, the
result becomes harder to read:** a list's correctness cannot be seen by looking at its entries —
it can only be understood by running it over the request set.

This is the second place the boundary falls short. Zero deviation rests on the assumption that
the list is written correctly **and** evaluated in the correct order; when either of these two
conditions is lost, the model drops to the same number as the permission-bits model's loose
expression — 6 excess grants.

#### Outside Both: Ownership

There is one item neither model measures. An object's **ownership** sits not inside the
permission decisions but above them: the owner can change both the bits and the list. In the
model, the owner of the `audit-log` file is `auditor`, and the policy grants the auditor only
read — but because it is the owner, it can grant itself write.

This is clear once it is written in the course's own measure. The items required to cross an
object's permission boundary, no matter what the policy on that object says, is **1**: ownership
being compromised. The items reached when the boundary falls are that object's entire set of
permission decisions. This is why ownership is not audited like a permission line — it is counted
separately. In the measurement network's five files, ownership is distributed across four separate
subjects, and this distribution determines how many files' permission scheme opens when a single
subject is compromised.

Counting ownership separately also corrects the measurement's third column. The first two columns
— the deviation count and the deviation's direction — measure the model's expressive power; the
third column measures how long that expression stays valid. In a set of files whose ownership is
concentrated in one subject, the policy is void the moment that subject is compromised, and the
deviation count loses its meaning.

#### Summary

- The permission-bits model cannot express 6 of 70 requests; the loose expression produces 6
  excess grants, the strict expression 6 excess denials. The leftover comes not from the group
  choice but from the three-class structure.
- What the model cannot express is not a permission but an exception: `guest-device`, which has
  no access under the policy, gains 3 requests in the loose expression, because the "others"
  class covers everyone.
- The two strategies differ from each other in 12 requests. The deviation's existence cannot be
  chosen, only its direction: between access opened in excess and legitimate work stopped.
- The access control list model expresses the same policy exactly across all 70 requests, but it
  brings a second axis: 6 requests turn into excess grants when the entries are evaluated in
  reverse order.
- As expressive power grows, correctness becomes unreadable; a list's correctness cannot be seen
  by looking at its entries — it can only be understood by running it over the request set.
- Ownership sits outside both models: the items required to cross the permission boundary are 1,
  no matter what the policy says, and when the boundary falls, that object's entire set of
  permission decisions opens.

#### Next Step

The permission scheme is now measured: which model can express what, and cannot, is written down
with a number. But all of this measurement never asked **who** changes permissions and **how**. A
permission line changing is an event, and the trace that event leaves behind matters as much as
the permission line itself.

The next lesson does the same job in two management forms — through a graphical interface and
through the command line — and counts the trace items each leaves behind: how many records fall,
how many can be reproduced, how many never appear in the audit trail.

### Graphical Interface and Command Line

Source: https://academia.sh/en/courses/it-fundamentals/graphical-interface-and-command-line

Counting the trace the same ten management jobs leave in two management forms: the graphical interface and the command line tying at 11 items appearing in the audit trail, three jobs leaving no trace at all in either form, the reproducible job count needing a script and a repository to rise from 0 to 10, and manual operations across forty machines dropping from 400 to 10.

The previous lesson measured the expressive power of permission models and left one question
open: a permission line **changing** is also an event. Policy changes when ownership is
compromised or when an admin changes a rule; what that change leaves behind is something just as
measurable as the permission line itself.

This lesson performs the same management jobs in two forms — through a graphical interface and
through the command line — and counts the trace items each one leaves behind.

#### A Trace Is a Number

When a management job finishes, what is left behind is written with three separate measures.

**How many records fall.** At the end of the job, how many separate places a trace forms in: the
subsystem's own record, the interface's own log, the shell history, a script file, a repository
commit, a change ticket.

**How many appear in the audit trail.** Not all records are equal. If a record can be deleted by
the person who did the job, it does not count as an audit trail item. Shell history is the
user's own file; the interface's local log rotates and is not retained. **A record that is not
persistent is not a trace — it is a reminder.**

**How many can be reproduced.** If a record carries exactly what the change was, the same change
can be repeated identically on another machine or rolled back. A record saying "a user was added"
is not the same thing as a record carrying the definition of that addition.

#### Ten Jobs, Three Forms

The example is ten management jobs performed on the branch servers of the fictional **regional
measurement network**. Both the jobs and the record scheme are fictional; the table below is a
**model** — there is no real interface, shell, or product.

**HO23:** the management job set is 10, and each job can be done in three forms. **HO24:** the
record type is 6; the `persistent` field says the record cannot be deleted by the person who did
the job and appears in the audit trail, the `reproduces` field says the record is enough to
reproduce the change. **HO25:** the system record and the change ticket are
**form-independent** — if the subsystem doing the job keeps its own record, it keeps it in all
three forms. **HO26:** a form only adds its own extra records. **HO27:** the same job is repeated
across forty machines; in the script-and-repo form, the definition is written once and run forty
times.

```js
// it/trace.mjs — the trace the same management job leaves in two forms (model): fictional
// measurement network, no real interface, shell, or product. No randomness.

// Record types. persistent: cannot be deleted by the user, appears in the audit trail.
// reproduces: the record itself is enough to reproduce the change.
export const TYPE = {
  "system-record": { persistent: true, reproduces: false },
  "change-ticket": { persistent: true, reproduces: false },
  "interface-log": { persistent: false, reproduces: false },
  "shell-history": { persistent: false, reproduces: false },
  "script-file": { persistent: false, reproduces: true },
  "repo-commit": { persistent: true, reproduces: true },
};

// Records each form adds to every job. The system record and the change ticket are form-independent.
export const FORM = { graphical: ["interface-log"], command: ["shell-history"],
  "script-repo": ["shell-history", "script-file", "repo-commit"] };

// system: does the subsystem doing the job keep its own record. ticket: is the job tied to a change ticket.
export const JOB = `
  add-user             yes  yes
  change-permission    no   yes
  stop-service         yes  yes
  firewall-rule        yes  yes
  mount-disk-volume    no   no
  install-software     yes  yes
  set-clock            yes  no
  install-certificate  no   no
  take-backup          yes  no
  review-log           no   no`
  .trim().split("\n").map((r) => {
    const [name, system, ticket] = r.trim().split(/\s+/);
    return { name, system: system === "yes", ticket: ticket === "yes" };
  });

export const records = (job, form) => [...(job.system ? ["system-record"] : []),
  ...(job.ticket ? ["change-ticket"] : []), ...FORM[form]];

export function measure(form) {
  const r = JOB.map((job) => records(job, form));
  const persistent = r.map((x) => x.filter((t) => TYPE[t].persistent).length);
  return { records: r.reduce((a, x) => a + x.length, 0),
    inAudit: persistent.reduce((a, x) => a + x, 0),
    notInAudit: r.reduce((a, x) => a + x.filter((t) => !TYPE[t].persistent).length, 0),
    traceless: persistent.filter((x) => x === 0).length,
    reproducible: r.filter((x) => x.some((t) => TYPE[t].reproduces && TYPE[t].persistent)).length };
}
```

```js
// it/tracemeasure.mjs — how many records fall, how many appear in the audit trail, how many can be reproduced.
import { TYPE, FORM, JOB, records, measure } from "./trace.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const B = Object.keys(FORM);
const MACHINES = 40;

console.log(`${JOB.length} management jobs x ${B.length} forms; record types ${Object.keys(TYPE).length}: ` +
  Object.entries(TYPE).map(([t, v]) => `${t}${v.persistent ? " (persistent)" : ""}`).join(", "));

const A = [-24, -8, -8, 11, 10, 13];
console.log("\n1. record items per job (in parentheses: items appearing in the audit trail)");
write(A, "job", "system", "ticket", ...B);
for (const job of JOB) {
  const h = (b) => { const r = records(job, b);
    return `${r.length} (${r.filter((t) => TYPE[t].persistent).length})`; };
  write(A, job.name, job.system ? "yes" : "no", job.ticket ? "yes" : "no", ...B.map(h));
}

const C = [-40, 11, 10, 13];
console.log("\n2. total per form");
write(C, "measure", ...B);
const R = Object.fromEntries(B.map((b) => [b, measure(b)]));
for (const [name, f] of [["record items dropped", (r) => r.records],
  ["items appearing in the audit trail", (r) => r.inAudit],
  ["items not appearing in the audit trail", (r) => r.notInAudit],
  ["jobs leaving no audit trail", (r) => `${r.traceless}/${JOB.length}`],
  ["reproducible jobs", (r) => `${r.reproducible}/${JOB.length}`]])
  write(C, name, ...B.map((b) => f(R[b])));

const D = [-40, 11, 10, 13];
console.log(`\n3. the same job repeated across ${MACHINES} machines`);
write(D, "measure", ...B);
for (const [name, f] of [["manual operations", (b) => (b === "script-repo" ? JOB.length : JOB.length * MACHINES)],
  ["place storing the change definition", (b) => (b === "script-repo" ? 1 : 0)],
  ["items appearing in the audit trail", (b) => R[b].inAudit * MACHINES]])
  write(D, name, ...B.map(f));
```

```
10 management jobs x 3 forms; record types 6: system-record (persistent), change-ticket (persistent), interface-log, shell-history, script-file, repo-commit (persistent)

1. record items per job (in parentheses: items appearing in the audit trail)
job                     system  ticket    graphical   command  script-repo
add-user                yes     yes           3 (2)     3 (2)        5 (3)
change-permission       no      yes           2 (1)     2 (1)        4 (2)
stop-service            yes     yes           3 (2)     3 (2)        5 (3)
firewall-rule           yes     yes           3 (2)     3 (2)        5 (3)
mount-disk-volume       no      no            1 (0)     1 (0)        3 (1)
install-software        yes     yes           3 (2)     3 (2)        5 (3)
set-clock               yes     no            2 (1)     2 (1)        4 (2)
install-certificate     no      no            1 (0)     1 (0)        3 (1)
take-backup             yes     no            2 (1)     2 (1)        4 (2)
review-log              no      no            1 (0)     1 (0)        3 (1)

2. total per form
measure                                   graphical   command  script-repo
record items dropped                             21        21           41
items appearing in the audit trail               11        11           21
items not appearing in the audit trail           10        10           20
jobs leaving no audit trail                    3/10      3/10         0/10
reproducible jobs                              0/10      0/10        10/10

3. the same job repeated across 40 machines
measure                                   graphical   command  script-repo
manual operations                               400       400           10
place storing the change definition               0         0            1
items appearing in the audit trail              440       440          840
```
The numbers belong to the **measurement** class; their inputs are HO23–HO27.

#### Two Forms Leave the Same Trace

The first three rows of the second table give an unexpected equality. The graphical interface
and the command line produce **the same numbers** on the same ten jobs: 21 record items, of
which **11** appear in the audit trail and 10 do not. The fourth row is the same too: in both
forms, **3 jobs** leave no audit trail at all.

The reason is in the first table. The items that appear in the audit trail come from only two
types: the subsystem's own record and the change ticket. Both are **form-independent**. Doing a
job through the graphical interface does not turn a job the subsystem does not record into a
recorded one; doing the same job through the command does not either.

The item each form adds — the interface's local log and the shell history — is not persistent.
Both can be deleted by the person who did the job, and neither carries the definition of the
change. This is why neither counts as an audit trail item; they show up as 10 in the columns, and
that 10 contributes nothing to the measure.

**"It was done from the command line, so there is a record" is an assumption, and it is wrong in
the model.** Shell history is a file in the user's home directory: it can be turned off,
deleted, edited. An audit trail item sits in a place that is not under the control of the person
who left the trace.

#### The Difference Is in Reproducibility

The fifth row separates the two forms for the first time — but what it separates is not
graphical from command, it is **scripted from unscripted**. The number of reproducible jobs is
0/10 in the graphical interface, also 0/10 for a manually typed command, and 10/10 in the
script-and-repo form.

What separates a manually typed command from a script is that its definition sits nowhere. The
command runs, leaves its result, and disappears itself. Shell history keeps the command lines,
but a history is not a definition: it does not write down the order, the context, which machine
it ran on, or which attempt failed.

In the script-and-repo form, two items are added, and the two work together. The script file
reproduces the change but is not itself persistent — it sits on disk and can be altered. The
repository commit is both persistent and carries the definition. This is why the measure looks
for the `reproduces` and `persistent` fields together: **reproducibility does not depend on a
definition existing — it depends on the definition sitting in a place that cannot be altered.**

The same row also hides a cost. In the script-and-repo form, the records dropped rise to 41 and
the items not appearing in the audit trail rise to 20. This item not appearing in the audit
trail doubling is not a loss — the items added are already accounted for in the repository commit
— but it raises the number of places to look. In an incident, three separate record sets are
compared, and that comparison is itself a job.

#### Three Jobs Leave No Trace

The **3/10** in the fourth row is the same in both forms and is the measurement's most direct
output. Behind mounting a disk volume, installing a certificate, and reviewing a log there is
neither a subsystem record nor a change ticket; when these three jobs are done, only a
deletable item is left behind.

What these three jobs share is that two of them do not look like a configuration change.
Reviewing a log is not a change — but which records were read by whom is one of the questions
asked in an incident investigation. Installing a certificate looks like copying a file, yet it
changes who the machine trusts.

In the script-and-repo form, this number drops to 0/10, because the repository commit depends on
the job, not the form: even if the subsystem does not keep a record, the change is written down
somewhere. **The fix for a job that leaves no trace is not changing the interface — it is
turning the job into a defined change.**

#### Forty Machines

The third table adds scale. When the ten jobs are repeated across forty branch servers, the
number of manual operations is **400** in the graphical and command forms and **10** in the
script-and-repo form; the definition is written once and run forty times.

The number of places storing the change definition is 0, 0, and 1. Zero means there is **no
definition to compare against** to see whether the forty machines have drifted from one another;
drift can only be found by reading the machines one by one. If a definition exists, drift is
asked as the difference between the definition and the machine.

The items appearing in the audit trail rise to 440, 440, and 840. This number does not improve
as it grows; it is the number of items that must be read. The measure's third column is therefore
not the raw record count but **the number of questions answered per record**: 440 items answer
"who, when"; 400 of the 840 items also answer "exactly what."

#### The Record's Readability Is a Separate Item

The number of items appearing in the audit trail assumes those items can be read together. The
440 records across forty machines turn into a single narrative only under two conditions: the
records sharing a common time axis, and the same job being referred to by the same name across
different machines.

The first condition depends on the `time-synchronization` item from the previous lesson. If that
item is off, the records of two machines cannot be ordered, and the question "which happened
first" goes unanswered; the number of items appearing in the audit trail keeps being written as
440, but the number of questions it answers drops. The second condition concerns naming: if the
same job is recorded under one name on one machine and a different name on another, the items
cannot be aggregated.

This is why the trace measure is written with three numbers, not two: how many items drop, how
many are persistent, and how many of the persistent ones are **comparable**. The first two
numbers depend on the form and the subsystem; the third depends on install-time items. When the
third number is not measured, the audit trail is just a pile of records.

#### The Choice of Form Depends on How Often the Job Repeats

The table does not rank one form above the other. The numbers separate three situations.

**A one-off job with no definition yet.** While hunting for the cause of a problem, which setting
will change is not known in advance; here, the graphical interface's missing trace is not a cost,
because there is no definition to repeat. This kind of job is not repeated across forty machines.

**A job that will be repeated across forty machines.** In this situation, the third table points
in a single direction: the gap between 400 manual operations and 10 is less about the operation
count and more about **how measurable the drift is**. On forty machines with no definition,
drift cannot be found.

**A job that is subject to audit.** What decides here is not the form but whether a subsystem
record exists behind the job. If the subsystem does not keep a record, no form fixes this; the
job must be turned into a defined change.

The common lesson across the three situations is this: **the choice of form is not a habit — it
is a decision determined by how often the job repeats and by the audit requirement.** Once it is
written down how many of the measurement network's ten jobs fall into which class, which job needs
to be scripted stops being a matter of debate.

#### Summary

- A management job's trace is written with three numbers: record items dropped, how many of them
  appear in the audit trail, and how many jobs can be reproduced.
- The graphical interface and the command line give the same numbers on the ten jobs: 21 records,
  11 audit trail items, 3 jobs leave no trace at all. The audit trail comes not from the form but
  from the subsystem keeping its own record and from the change ticket.
- The item each form adds on its own is not persistent; shell history is a file the person who
  did the job can delete, and an audit trail item sits in a place not under the control of the
  person who left it.
- The reproducible job count is 0/10, 0/10, and 10/10. The distinction is not between graphical
  and command but between scripted and unscripted work; the definition must sit somewhere that
  cannot be altered.
- Manual operations across forty machines drop from 400 to 10, and the place storing the change
  definition rises from 0 to 1; drift can only be asked about if a definition exists to compare
  against.
- The number of items appearing in the audit trail assumes the items sit on a common time axis
  and under common names; without these conditions, the item count does not change but the
  number of questions it answers drops.

#### Next Step

Everything measured across this topic has been about how a scheme is built: which boundary
stands where, which item closes when, which policy can be expressed, which job leaves a trace.
All of it holds while the machine works as expected.

When the machine does not work as expected, only symptoms are left, and the records counted in
this lesson are where those symptoms are read. The next lesson looks at that situation and ties
the method to a number: for a set of symptoms, how many possibilities does each hypothesis
eliminate, how many steps does each sequence take to finish, and how many steps does the worst
sequence take.

### Troubleshooting Method

Source: https://academia.sh/en/courses/it-fundamentals/troubleshooting-method

Measuring troubleshooting by its elimination count: over 16 possibilities and 8 tests, the info-priority sequence finishing in an average of 3.50 steps against the list sequence's 5.81, the worst-case step count being 6 against 8, the step count that eliminates nothing coming out to 0 against 22, and two causes that no test can tell apart.

The previous lesson counted the trace management jobs leave and gave the measure of a scheme
that holds while the machine works as expected. When the machine does not work as expected, only
symptoms are left: a service cannot get data, a request comes back with no response, a job is
delayed.

The path from symptom to cause is a method, and a method can be measured. This lesson ties it to
a single number: how many possibilities are eliminated when a hypothesis is tested.

#### A Method Is an Elimination Count

Troubleshooting is usually described as something between guessing and trying. Making it
measurable requires writing down three things.

**The set of possibilities.** Once the symptom is observed, which causes are still possible.
Without writing this set down, the sentence "I'm looking for the cause" carries no number.

**The set of tests.** Each test probes a hypothesis and gives a result. A test's power is how
many possibilities its result eliminates — and this number is read **in the worst case**,
because which result comes back is not known in advance.

**Sequence.** Tests are run in some order. The same test set finishes in a different number of
steps with a different order; sequence is the one thing gained without acquiring a new tool.

#### Sixteen Possibilities, Eight Tests

The example is a symptom in the fictional **regional measurement network**: the branch server is not
sending data to the billing service. Both the symptom and the cause list are fictional; the
evaluation below is a **model** — there is no real incident record.

**HO28:** when the symptom is observed, 16 causes are possible. **HO29:** there are 8 tests; each
test gives a single result for each cause, and **causes that give the same result cannot be told
apart by that test**. **HO30:** when a test is run, the candidate set narrows to the causes that
give the same result as the actual cause. **HO31:** three sequences are tried — `info` picks the
test that eliminates the most in the worst case, `list` runs the tests in the order they were
written, `weak` puts the test that eliminates the least in the worst case first. **HO32:** the run
stops once the candidate set narrows to a single cause, or once no test can separate the set any
further.

```js
// it/diagnose.mjs — possibilities eliminated per hypothesis in a symptom set (model): fictional
// measurement network, no real incident record. No randomness.

// Symptom: the branch server is not sending data to the billing service. 16 possibilities at the start.
export const CAUSE = ["network-cable", "switch-port-state", "address-conflict",
  "name-resolution", "firewall-rule", "routing-table", "service-down", "service-wrong-port",
  "certificate-expiry", "credential", "permission-change", "disk-full", "memory-exhausted",
  "clock-skew", "version-mismatch", "configuration-error"];

// Each test probes a hypothesis. Every character in the string is that cause's result on the
// test; causes with the same character cannot be told apart by that test.
export const TEST = [
  ["network-connectivity", "hheeeeeeeeeeeeee"],
  ["address-and-routing", "eeceeyeeeeeeeeee"],
  ["name-resolution", "eeeheeeeeeeeeeee"],
  ["port-access", "eeeehekkeeeeeeee"],
  ["service-status", "eeeeeedyeeeeeeee"],
  ["identity-and-certificate", "eeeeeeeeskieeeee"],
  ["resource-status", "eeeeeeeeeeedbeee"],
  ["clock-version-config", "eeeeeeeeeeeeetsy"],
].map(([name, s]) => ({ name, s }));

// The largest group a test leaves in a candidate set: the worst-case remaining possibilities.
export const largest = (t, candidates) => Math.max(...[...new Set(candidates.map((c) => t.s[c]))]
  .map((v) => candidates.filter((c) => t.s[c] === v).length));

export const SEQUENCE = {
  // info-priority: the test that eliminates the most in the worst case. Stop if none eliminate.
  info: (candidates, used) => {
    const k = TEST.map((t, i) => i).filter((i) => !used.has(i));
    if (!k.length) return null;
    const e = k.reduce((a, i) => (largest(TEST[i], candidates) < largest(TEST[a], candidates) ? i : a));
    return largest(TEST[e], candidates) === candidates.length ? null : e;
  },
  // list: tests in the order they were written.
  list: (candidates, used) => TEST.map((t, i) => i).find((i) => !used.has(i)) ?? null,
  // weak: the sequence that puts the test eliminating the least in the worst case first.
  weak: (candidates, used) => {
    const k = TEST.map((t, i) => i).filter((i) => !used.has(i));
    if (!k.length) return null;
    return k.reduce((a, i) => (largest(TEST[i], candidates) > largest(TEST[a], candidates) ? i : a));
  },
};

export function run(actual, sequence) {
  let candidates = CAUSE.map((c, i) => i);
  const used = new Set();
  let steps = 0, empty = 0;
  while (candidates.length > 1) {
    const i = SEQUENCE[sequence](candidates, used);
    if (i === null) break;
    used.add(i);
    const before = candidates.length;
    candidates = candidates.filter((c) => TEST[i].s[c] === TEST[i].s[actual]);
    steps++;
    if (candidates.length === before) empty++;
  }
  return { steps, empty, remaining: candidates.length };
}
```

```js
// it/diagnosemeasure.mjs — how many possibilities are eliminated per hypothesis, which sequence finishes in how many steps.
import { CAUSE, TEST, largest, SEQUENCE, run } from "./diagnose.mjs";

const write = (g, ...s) => console.log(s.map((v, i) =>
  (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join(""));
const ALL = CAUSE.map((c, i) => i);
const S = Object.keys(SEQUENCE);
if (TEST.some((t) => t.s.length !== CAUSE.length)) throw new Error("test length mismatch");

console.log(`symptom: branch server not sending data; ${CAUSE.length} possibilities, ` +
  `${TEST.length} tests`);

const A = [-26, 14, 15, 18];
console.log("\n1. each test's power alone (over 16 possibilities)");
write(A, "test", "result types", "worst case", "least eliminated");
for (const t of TEST) {
  const b = largest(t, ALL);
  write(A, t.name, new Set(t.s).size, b, CAUSE.length - b);
}

const B = [-41, 10, 10, 10];
console.log("\n2. three sequences, with each of the 16 causes tried as the actual cause");
const R = Object.fromEntries(S.map((s) => [s, ALL.map((c) => run(c, s))]));
write(B, "measure", ...S);
for (const [name, f] of [["average steps", (r) => (r.reduce((a, x) => a + x.steps, 0) / r.length).toFixed(2)],
  ["worst-case steps", (r) => Math.max(...r.map((x) => x.steps))],
  ["steps eliminating nothing", (r) => r.reduce((a, x) => a + x.empty, 0)],
  ["scenarios narrowed to one cause (of 16)", (r) => r.filter((x) => x.remaining === 1).length]])
  write(B, name, ...S.map((s) => f(R[s])));

const C = [-38, 10, 10, 10];
console.log("\n3. step count for four causes");
write(C, "actual cause", ...S);
for (const c of [0, 7, 10, 15]) write(C, CAUSE[c], ...S.map((s) => run(c, s).steps));

const D = [-46, 10];
console.log("\n4. the method's floor");
const indistinct = ALL.filter((c) => ALL
  .some((d) => d !== c && TEST.every((t) => t.s[c] === t.s[d])));
write(D, "measure", "value");
write(D, "causes no test can tell apart", indistinct.length);
write(D, "smallest set reached with the info sequence", Math.min(...ALL.map((c) => run(c, "info").remaining)));
write(D, "largest set remaining with the info sequence", Math.max(...ALL.map((c) => run(c, "info").remaining)));
console.log(`indistinguishable causes: ${indistinct.map((c) => CAUSE[c]).join(", ")}`);
```

```
symptom: branch server not sending data; 16 possibilities, 8 tests

1. each test's power alone (over 16 possibilities)
test                        result types     worst case  least eliminated
network-connectivity                   2             14                 2
address-and-routing                    3             14                 2
name-resolution                        2             15                 1
port-access                            3             13                 3
service-status                         3             14                 2
identity-and-certificate               4             13                 3
resource-status                        3             14                 2
clock-version-config                   4             13                 3

2. three sequences, with each of the 16 causes tried as the actual cause
measure                                        info      list      weak
average steps                                  3.50      5.81      5.81
worst-case steps                                  6         8         8
steps eliminating nothing                         0        22        12
scenarios narrowed to one cause (of 16)          14        14        14

3. step count for four causes
actual cause                                info      list      weak
network-cable                                  4         8         8
service-wrong-port                             2         5         4
permission-change                              2         6         7
configuration-error                            3         8         8

4. the method's floor
measure                                            value
causes no test can tell apart                          2
smallest set reached with the info sequence            1
largest set remaining with the info sequence           2
indistinguishable causes: network-cable, switch-port-state
```
The numbers belong to the **measurement** class; their inputs are HO28–HO32.

#### Tests Are Uneven, the Split Is Not Half-and-Half

The first table breaks an expectation. None of the eight tests splits the possibility set down
the middle: the worst-case remaining possibilities fall between 13 and 15, meaning even the
strongest test eliminates only **3** of 16 possibilities. The `name-resolution` test eliminates
just **1** possibility in the worst case.

The reason is in the number of result types a test has. If a test gives two results, it splits
the possibilities into at most two; if it gives four results, into at most four. But the split
does not have to be **even**, and in this model it is not: each test separates out a few causes
and leaves the remaining thirteen, fourteen, or fifteen at the same result. In a system, tests are
naturally uneven, because each test probes a specific layer, and causes outside that layer look
identical in the test's eyes.

The practical consequence of this is that **picking the test that eliminates the most in the
worst case is not finding a half-and-half split — it is finding the least bad split.** The
expectation of finishing 16 possibilities in four steps with binary search is not met by this
test set.

#### What Sequence Changes

The second table runs the three sequences with each of the sixteen causes taken as the actual
cause. The info-priority sequence finishes in an average of **3.50** steps; the list sequence
and the weak sequence in **5.81**. The worst-case steps are **6** for the info-priority sequence
and **8** for the others — eight is the full test count, meaning these two sequences run every
single test in the worst case.

The difference does not mean **a forty-five-minute gap in a forty-five-minute incident**; the
cost per step varies from test to test. But the number still gives direction: with the same test
set, the same information, and the same tools, changing only the sequence gains more than a
third of the average steps.

The third table shows this does not hold for every cause. For `service-wrong-port`, the
info-priority sequence takes 2 steps, the weak sequence 4, the list sequence 5. For
`permission-change`, the weak sequence takes 7 steps and the list sequence 6 — the weak sequence
is **worse** than the list sequence. For `network-cable`, both the list and weak sequences run 8
steps, while the info-priority sequence runs 4.

#### A Fixed Sequence Produces More Empty Steps Than an Adapted Bad One

The third row of the second table gives this lesson's most unexpected number. The count of
steps that eliminate nothing is **0** for the info-priority sequence, **22** for the list
sequence, and **12** for the weak sequence.

The weak sequence deliberately puts the test that eliminates the least in the worst case first;
yet it still produces only half as many empty steps as the list sequence. The reason is that the
real difference between the two is not "good choice / bad choice" — it is **adapted / not
adapted**. The weak sequence looks at the current candidate set on every step and does not pick
a test that splits the set not at all if one remains available; the list sequence never looks at
the candidate set and, in order, also runs tests that probe a layer already eliminated.

An empty step is troubleshooting's invisible cost. It runs, takes time, produces a result, and
that result does not change the candidate set — but the result itself looks like information,
because the sentence "that turned out normal too" gives a feeling of progress. **What must be
counted is not the number of tests run but the number of possibilities eliminated.** The list
sequence's 22 empty steps are 22 checks that ran but eliminated nothing.

#### Elimination Count Is Not the Only Measure

The model treats every cause as equally likely and ranks tests only by their eliminating power.
An incident has two more things, and neither is in this table.

The first is **the likelihood of the causes**. Not all sixteen causes occur at the same
frequency; a symptom that appears right after a configuration change is more likely to have
`configuration-error` behind it than the others. The elimination count does not see this: it
picks the test that eliminates the most, not the test that probes the most likely cause. When the
two do not coincide, the choice is a trade-off between the expected step count and the worst-case
step count. Closing the symptom quickly cares about the expected number; bounding the worst case
cares about the worst-case number.

The second is **the cost of the test**. In the model, every step costs the same; in reality, one
test finishes with a command in seconds, another with a field team traveling to the branch in
hours. Choosing between an expensive test that eliminates three possibilities and a cheap test
that eliminates two cannot be made by looking at the elimination count alone. The `network-cable`
and `switch-port-state` distinction needing a new observation is exactly such an item: the cost
of the distinction is a site visit.

Adding these two axes does not change the model — it changes **how it is read**. The 3.50 in the
table holds under the condition that steps cost the same and causes are equally likely; without
writing down these two assumptions, the number cannot be turned into a target. The measurement's
value shows up once it is written down which assumption produced it.

#### The Method's Floor

The fourth table writes down where the method stops. **Two** of the sixteen causes cannot be told
apart by any test: `network-cable` and `switch-port-state`. Both give the same result on every
test, because seen from the server, their symptom is identical.

This is why the number of scenarios all three sequences narrow to a single cause is **14/16**. In
the remaining two scenarios, the method stops at two possibilities, and this is not a
shortcoming — it is **the test set's resolution**. Making the distinction does not require a new
sequence, it requires a new test — in this model, an observation made from outside the server,
from the other end of the connection.

This has a decision to match it. Writing a troubleshooting procedure starts with listing out the
cause pairs the test set cannot separate; for those pairs, either a new observation is added, or
a single fix is defined that can be applied without making the distinction. In the measurement
network, progress can be made without distinguishing `network-cable` from `switch-port-state`:
both causes are addressed in the same field team visit. **Two indistinguishable causes are not a
problem if their fixes are the same.**

#### Summary

- Troubleshooting is measured by the number of possibilities eliminated per hypothesis; this
  number is read in the worst case, because which result comes back is not known in advance.
- Tests are uneven: the strongest of the eight tests eliminates only 3 of 16 possibilities in the
  worst case, the weakest just 1. The expectation of a half-and-half split is not met in real
  test sets.
- The info-priority sequence finishes in an average of 3.50 steps, the list and weak sequences in
  5.81; the worst-case steps are 6 against 8. Sequence is the one thing gained without acquiring
  a new tool.
- The count of steps that eliminate nothing is 0, 22, and 12. The fixed list sequence produces
  twice as many empty steps as the adapted sequence that deliberately makes a weak choice; what
  decides is not the quality of the choice but whether the candidate set is looked at.
- Elimination count is not the only measure: the likelihood of causes and the cost of tests are
  not in the table. The 3.50 holds under the assumption that steps cost the same and causes are
  counted as equally likely.
- Two causes cannot be told apart by any test; all three sequences narrow 14 of 16 scenarios to a
  single cause. The remaining distinction requires not a new sequence but a new observation, and
  if the fixes are the same, the distinction may not be needed at all.

#### Next Step

This topic counted a single machine's boundaries item by item. The eight boundaries hardware
draws and two of them opening the entire inventory; operating system families diverging on the
same request by 4, 11, and 12 items; the 17 risky items a clean install leaves and the 5 that
never close; two permission models hitting their expressive limit on 6 requests; two management
forms leaving the same 11 audit trail items; and now the path from symptom to cause dropping to
3.50 steps. The machine's boundary is no longer an adjective — it is a number.

All of these measurements carried a silent assumption: the machine being counted **runs on its
own hardware**. That is not true of most such machines. There is one more layer beneath the
branch server, and that layer's boundary was never measured: which items are shared between
guest and host, how many items it takes to cross that boundary, and how many items become
reachable when the boundary falls. The next topic, **Virtualization and Redundancy**, begins with
this question.

## Virtualization and Redundancy

Measuring the layer placed beneath the hardware as a boundary: the trusted-base size of type 1 and type 2 hypervisors, the number of items a guest and host share and the path each shared item opens, the secrets a snapshot chain and a clone carry and the vulnerabilities a rollback reopens, the same isolation question compared between a guest system and a container in terms of items required to cross the boundary, and the items a snapshot and a backup do and do not recover in the same loss scenario.

### Hypervisor Types

Source: https://academia.sh/en/courses/it-fundamentals/hypervisor-types

Counting the boundary a hypervisor draws across two designs: the number of layers between guest and hardware, the items each layer adds to the trusted base, the fraction of items reached when a layer falls, and, for the same set of targets, the items required to cross the boundary together with the candidate items that item could have been chosen from.

The previous lesson tied troubleshooting to the number of possibilities eliminated per hypothesis,
and closed the topic with a gap: every boundary measured was the boundary of **a single machine**.
That machine's hardware belonged to it alone, its kernel spoke directly to that hardware, and the
boundary between user and kernel was the only boundary. On the branch servers of the regional
measurement network this is no longer true: a single branch server runs four separate guest
systems, and all four use the same processor, the same memory, and the same disk. **Below** the
boundary the operating system draws there is a second boundary, and the layer that draws it is
called the hypervisor.

This lesson distinguishes hypervisors not by an adjective but by two numbers. The first number is
how many layers stand between the guest and the hardware. The second number is how many items each
layer adds to the **trusted base** — that is, the number of items that must behave correctly for
the guest boundary to hold. These two numbers place a design not against the other, but against its
own set of items.

**VR1 — The measurement object is a branch server of the fictional regional measurement network**;
four guest systems run on it. The item counts are a model, the run and the arithmetic are real.
**VR2 — The hypervisor is referred to only by its two type names: the type 1 hypervisor and the
type 2 hypervisor.** **VR3 — The trusted base is the set of items that must behave correctly for
the guest boundary to hold.** **VR4 — A layer falling means exactly one item in that layer behaving
incorrectly.** **VR5 — The number of items required to cross the boundary is the number of layers
that must be wrong at the same time**; one item per layer is enough. **VR6 — A candidate item is
the total of items that could be wrong in those layers.**

#### Layers and the Trusted Base

A type 1 hypervisor installs directly on top of the hardware; there is no general-purpose operating
system beneath it. A type 2 hypervisor runs as a process on top of an operating system, and guest
systems run inside that process. The difference is not an installation detail but the length of the
layer list beneath the guest.

```js
// layers.mjs — layers between guest and hardware, and the trusted base (MODEL).
// Fictional branch server: four guest systems run on it. Type names are used, not product names.

// Item sets in the system: short key -> [name, item count]
export const ITEM_SETS = {
  gd: ['guest disk image', 4], gm: ['guest memory content', 4], gn: ['guest network interface', 4],
  hy: ['hypervisor config item', 3], hu: ['host user-space file', 7],
  fw: ['firmware setting', 4],
};
// [layer, trusted-base items, on the guest-hardware path?, sets reached if it falls]
export const DESIGN = {
  'type 1 hypervisor': [
    ['management interface', 3, false, ['gd', 'gn', 'hy']],
    ['hypervisor kernel', 6, true, ['gd', 'gm', 'gn', 'hy']],
    ['firmware and boot chain', 4, true, ['gd', 'gm', 'gn', 'hy', 'fw']],
  ],
  'type 2 hypervisor': [
    ['host user space', 7, false, ['gd', 'gn', 'hy', 'hu']],
    ['hypervisor process', 5, true, ['gd', 'gm', 'gn', 'hy', 'hu']],
    ['host kernel', 9, true, ['gd', 'gm', 'gn', 'hy', 'hu', 'fw']],
    ['firmware and boot chain', 4, true, ['gd', 'gm', 'gn', 'hy', 'hu', 'fw']],
  ],
};
export const sum = (keys) => keys.reduce((t, a) => t + ITEM_SETS[a][1], 0);
// Item universe covered by the design: every set the layers can reach
export const universe = (name) => sum([...new Set(DESIGN[name].flatMap(([, , , e]) => e))]);

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = (x, n) => String(x).padStart(n);
  for (const [name, layers] of Object.entries(DESIGN)) {
    const base = layers.reduce((t, [, k]) => t + k, 0);
    const onPath = layers.filter(([, , y]) => y).length;
    const all = universe(name);
    console.log(`\n${name}: trusted base ${base} items, on path ${onPath} layers, universe ${all} items`);
    console.log(`${'layer'.padEnd(28)}${s('items', 7)}${s('on path', 9)}${s('reached if fallen', 20)}`);
    for (const [k, n, y, e] of layers)
      console.log(`${k.padEnd(28)}${s(n, 7)}${s(y ? 'yes' : 'no', 9)}${s(`${sum(e)}/${all}`, 20)}`);
  }
}
```

```
type 1 hypervisor: trusted base 13 items, on path 2 layers, universe 19 items
layer                         items  on path   reached if fallen
management interface              3       no               11/19
hypervisor kernel                 6      yes               15/19
firmware and boot chain           4      yes               19/19

type 2 hypervisor: trusted base 25 items, on path 3 layers, universe 26 items
layer                         items  on path   reached if fallen
host user space                   7       no               18/26
hypervisor process                5      yes               22/26
host kernel                       9      yes               26/26
firmware and boot chain           4      yes               26/26
```
The two designs' layer counts diverge in the expected direction: the type 1 design has **two**
layers between guest and hardware, the type 2 design has **three**. Looking at this number alone,
one might say the type 2 design is protected more deeply; the second column breaks that claim. The
cost of carrying that extra layer is the trusted base climbing from thirteen items to
**twenty-five**. Most of the added items are there not for the guest but for the host's own work:
the seven-item host user space is made of components the guest never needs, from the session
manager to the package manager. The guest boundary still depends on them, because the hypervisor
process sits in the same place as that user's files.

The third column gives the real finding. In the type 1 design, the least damaging layer that can
fall is the management interface: falling, it makes **eleven** of nineteen items reachable; guest
memory content stays out, because the management interface creates and deletes the guest but does
not read the running guest's memory. In the type 2 design, the least damaging fall is the host user
space, and there **eighteen** of twenty-six items become reachable. As a percentage, that is
fifty-eight against sixty-nine. Adding a layer lengthened the path, but **it did not narrow the
surface the first fall opens.**

The sharpest row is the type 2 design's second row. When the hypervisor process falls, the number
of items reached is twenty-two, and that is only four short of the number reached when the host
kernel falls. The reason is structural: guest disk images sit in the host's file system as files
belonging to the identity that runs the hypervisor process. Whoever reaches the process reaches the
four guests' disks as files. **The third layer beneath the guest protects the hardware, not the
guest's disk.**

#### Items Required to Cross the Boundary

Layer count alone is not isolation strength. The measure is how many items must be wrong **at the
same time** to reach a given target. The run below measures the same four targets separately in
both designs and writes two numbers for each target: the items required and the number of
candidates that item could have been chosen from.

```js
// boundary.mjs — the same target set in both designs: items required to cross the boundary and candidate items.
// "Required" = number of layers that must be wrong at the same time. "Candidate" = the item total of those layers.
import { DESIGN, sum, universe } from './layers.mjs';

const TARGETS = [                    // [target, item sets that must be reached]
  ["neighbor guest's disk", ['gd']], ["neighbor guest's memory", ['gm']],
  ['changing a firmware setting', ['fw']], ['direct access to hardware', null],
];
const measure = (name, need) => {
  const layers = DESIGN[name];
  if (need === null) {               // hardware: one item must be wrong in every layer on the path
    const onPath = layers.filter(([, , y]) => y);
    return [onPath.length, onPath.reduce((t, [, n]) => t + n, 0)];
  }
  const qualifying = layers.filter(([, , , e]) => need.every((g) => e.includes(g)));
  return [1, qualifying.reduce((t, [, n]) => t + n, 0)];
};

const NAMES = Object.keys(DESIGN);
const s = (x, n) => String(x).padStart(n);
console.log(`${''.padEnd(29)}${'type 1'.padStart(19)}${'type 2'.padStart(21)}`);
console.log(`${'target'.padEnd(29)}${s('required', 8)}${s('candidate', 11)}${s('required', 10)}${s('candidate', 11)}`);
for (const [h, g] of TARGETS) {
  const [a1, b1] = measure(NAMES[0], g), [a2, b2] = measure(NAMES[1], g);
  console.log(`${h.padEnd(29)}${s(a1, 8)}${s(b1, 11)}${s(a2, 10)}${s(b2, 11)}`);
}
const single = TARGETS.filter(([, g]) => g !== null).length;
console.log(`\n${single} of the targets need 1 item in both designs; the difference is in the candidate count`);
for (const name of NAMES) {
  const k = DESIGN[name];
  const atLeast = Math.min(...k.map(([, , , e]) => sum(e)));
  console.log(`${name.padEnd(27)} base ${s(k.reduce((t, [, n]) => t + n, 0), 2)} items,`
    + ` a single layer falling reaches at least ${atLeast}/${universe(name)} items`);
}
```

```
                                          type 1               type 2
target                       required  candidate  required  candidate
neighbor guest's disk               1         13         1         25
neighbor guest's memory             1         10         1         18
changing a firmware setting         1          4         1         13
direct access to hardware           2         10         3         18

3 of the targets need 1 item in both designs; the difference is in the candidate count
type 1 hypervisor           base 13 items, a single layer falling reaches at least 11/19 items
type 2 hypervisor           base 25 items, a single layer falling reaches at least 18/26 items
```

In three of the four targets, the number of items required is **one** in both designs. Reaching a
neighboring guest's disk takes one item from a single layer; the same is true for the neighbor's
memory. The difference in layer count shows up in none of these three rows, because the layers
stand **one after another** between guest and hardware, not between guest and neighboring guest.
What separates two guests standing side by side is the same hypervisor, and that hypervisor is a
single set of items.

The difference is in the candidate column. For the neighbor's disk there are thirteen candidate
items in the type 1 design and twenty-five in the type 2 design — roughly double. The
firmware-setting row widens the ratio further: in the type 1 design the firmware setting is only
reachable through the boot chain's four items, while in the type 2 design the host kernel can also
write the firmware, so the candidate count climbs from four to thirteen. **Isolation strength is the
number of items required; since that number is one in both, what separates the two designs is not
the strength of the boundary but how many candidates that single item could come from.**

Only in the last row is the required number greater than one: direct access to the hardware demands
two items being wrong at the same time in the type 1 design, three in the type 2 design. This is the
one place the type 2 design gains, and what it gains is the hardware — not the guest's data.

#### Root of the Chain

The layer common to both designs is the firmware and boot chain, and in both tables it is the layer
that reaches everything when it falls: 19/19 in the type 1 design, 26/26 in the type 2 design.
Counting those four items separately is not enough, because there is an order among them: each link
loads the next one. **Secure boot** places a check on that order — each link is verified because it
was signed by the one before it, and the root of the chain is the **trusted platform module** in the
hardware.

```js
// boot.mjs — how signed boot changes the required and candidate items in the boot chain (MODEL).
// The root of the chain is the trusted platform module in the hardware; each link is verified by the one before it.
const CHAIN = ['boot firmware', 'boot loader', 'kernel image', 'hypervisor module'];

// unsigned: index of the first unverified link (null verifies the whole chain)
const measure = (signed, unsigned = null) => {
  if (!signed) return [1, CHAIN.length, 0];          // each link can be swapped on its own
  const cut = unsigned === null ? CHAIN.length : CHAIN.indexOf(unsigned);
  // whatever comes after the cut link is not verified: one item is enough, candidates are those remaining links
  if (cut < CHAIN.length) return [1, CHAIN.length - cut, CHAIN.length - 1];
  return [CHAIN.length, 1, CHAIN.length];            // root key is the sole candidate, links get re-signed
};

const s = (x, n) => String(x).padStart(n);
console.log(`boot chain ${CHAIN.length} links: ${CHAIN.join(' -> ')}`);
console.log(`${'arrangement'.padEnd(34)}${s('required', 8)}${s('candidate', 11)}${s('re-signed', 19)}`);
for (const [ad, signed, unsigned] of [['unsigned boot', false, null], ['signed boot', true, null],
  ['signed boot, last link unsigned', true, 'hypervisor module'],
  ['signed boot, kernel unsigned', true, 'kernel image']]) {
  const [g, a, y] = measure(signed, unsigned);
  console.log(`${ad.padEnd(34)}${s(g, 8)}${s(a, 11)}${s(y, 19)}`);
}
```

```
boot chain 4 links: boot firmware -> boot loader -> kernel image -> hypervisor module
arrangement                       required  candidate          re-signed
unsigned boot                            1          4                  0
signed boot                              4          1                  4
signed boot, last link unsigned          1          1                  3
signed boot, kernel unsigned             1          2                  3
```
In the unsigned arrangement, any one of the four links can be swapped on its own: required is one,
candidate is four. In the signed arrangement, swapping the last link requires changing the entire
chain from it up to the root; required climbs to four, while candidate drops to the single root key
in the hardware. This is the lesson's only numerical improvement — the required number grows and the
candidate number shrinks at the same time.

The last two rows show cost and fragility together. Every update to the signed chain requires
re-signing three or four links; that is a fixed task added to the patch process. The fragility is
sharper: if the chain has **a single unsigned link**, the required item drops from four to one. When
the last link is left unsigned, required is one and candidate is one too — meaning one unverified
hypervisor module erases the entire gain of the four-link chain. When the kernel is left unsigned,
candidate rises to two. **The chain is only as strong as its first unverified link, not its weakest
one.**

#### Where the Cost Is Written

The type 1 design's thirteen-item base does not come for free. On that design the machine runs only
the guest system: the host has no browser, no local tool, no file sharing, and no field team's daily
work. The type 2 design's seven-item host user space is not waste — it is that machine's
**legitimate work**. On field teams' laptops that work cannot be given up; because it can be given
up on a branch server, the type 1 design makes sense there.

The second item of cost is management. In the type 1 design the management interface was counted as
a separate layer, and falling, it reached eleven items. In the type 2 design there is no separate
management layer, because management is done from the host's own session, and that session already
reaches eighteen of twenty-six items. Moving management out into a separate layer narrows the items
reached at the cost of adding one item — this is the first trade-off measured in this course, and it
buys a seven-item narrowing for a cost of three items.

None of these measurements rank one type over the other. On a branch server where the host has no
work of its own, the type 1 design asks to move management out to a separate interface in exchange
for a base smaller by twelve items and a path shorter by one layer. On a field laptop, because that
work cannot be given up, the type 2 design's twenty-five-item base is not a choice but that
machine's definition. All the measure says is which numbers the chosen design comes with.

#### Summary

- The type 1 hypervisor has two layers between guest and hardware, the type 2 hypervisor has three;
  the trusted base is 13 and 25 items respectively.
- The smallest fraction of items reached when a single layer falls is 11/19 in the type 1 design and
  18/26 in the type 2 design: adding a layer did not narrow the surface the first fall opens.
- In the type 2 design, 22/26 items are reached when the hypervisor process falls, because the four
  guests' disk images sit in the host's file system as files belonging to that process's identity.
- In three of the four targets, the number of items required to cross the boundary is one in both
  designs; the difference is in the candidate count (13 against 25, 10 against 18, 4 against 13).
- Only for direct access to hardware does the number rise to two and three; the type 2 design's
  extra layer protects the hardware, not the guest's disk.
- In the four-link boot chain, signed boot raises the required item from 1 to 4 and lowers the
  candidate item from 4 to 1; a single unverified link drops the required item back to 1.

#### Next Step

This lesson's entire measurement counted layers **stacked on top of one another**: guest,
hypervisor, host, hardware. But there is not just a single stack between guest and host; while both
run, they use a set of items **at the same time**. The same scheduler queues both sides' threads,
the same physical memory pages get merged, the disk and network card the guest sees are emulation
code running on the host, and on top of that there are items opened for convenience, like a shared
folder and a clipboard. The next lesson counts the escape risk through these shared items: how many
items the guest and host share, how many paths each shared item opens, and how many paths remain
once those items are closed.

### Guest and Host System

Source: https://academia.sh/en/courses/it-fundamentals/guest-and-host-system

Measuring escape risk by the number of shared items: the eleven items the guest and host share, the path each item opens and its classification by transit, data, or information effect, the paths closed by turning off optional shares along with their cost in work steps and host memory, and the six paths that cannot be closed while the guest is running.

The previous lesson counted layers stacked on top of one another: guest, hypervisor, host,
hardware. In that ordering, each layer carried the one before it. But there is not just a single
stack between guest and host; while both run, they use a set of items **at the same time**. The
same scheduler queues both sides' threads, the same physical memory pages get merged, the disk and
network card the guest sees are emulation code running on the host, and on top of that there is a
shared folder, a clipboard, and an agent channel opened for convenience.

Escape is the guest boundary being crossed once and something on the host side being reached. This
lesson measures escape not as a probability but as **the number of shared items**: how many items
the guest and host share, how many paths each shared item opens, and how many of those paths can be
closed. The escape step itself is not written; the path's name, its effect, and its cost are
written. Trying an escape path on a real system, without written authorization and a defined scope,
is both illegal and unprofessional.

**VR7 — A shared item is a component used jointly with the host while the guest runs**; the list on
the fictional branch server is a model, the count is real. **VR8 — A path is an outcome reached on
the host side through a single shared item.** **VR9 — A path's effect is one of three classes:
transit** (running code in the host process), **data** (touching host data), **information**
(inference only). **VR10 — A required item is an item that cannot be closed while the guest is
running.** **VR11 — Cost is written in two units: changed legitimate work steps and additional host
memory.** **VR12 — The branch server has four guests, split into two trust levels.**

#### Shared Items

Shared items are of two kinds. One part exists as long as the guest runs: the guest must obtain
processor time, see a disk, and write to a network card, and that disk and network card are also
emulation code running on the host. The other part is opened for convenience and can be closed with
a single configuration item.

```js
// sharing.mjs — items the guest and host share, and the paths each item opens (MODEL).
// Paths are named on the defense axis; not exploit code, only the path's name and effect are written.
// effect: transit = moving into the host process, data = touching host data, information = inference.

// [item, required?, [[path, effect], ...], [changed legitimate work steps, extra host memory MiB]]
export const SHARING = [
  ['scheduler', true, [["neighboring guest's activity timing", 'information']], [0, 0]],
  ['processor cache', true, [['access pattern from a shared cache line', 'information']], [0, 0]],
  ['clock source', true, [["inferring the host's maintenance window", 'information']], [0, 0]],
  ['disk controller emulation', true, [['transit into the host process via an emulation bug', 'transit']], [0, 0]],
  ['network interface emulation', true, [['transit into the host process via an emulation bug', 'transit'],
    ["guest packet into the host's network stack", 'data']], [0, 0]],
  ['memory page merging', false, [['measuring whether the same page exists', 'information'],
    ['page sharing inferred from write latency', 'information']], [0, 1856]],
  ['display controller emulation', false, [['transit into the host process via an emulation bug', 'transit']], [2, 0]],
  ['shared folder', false, [["writing to the host's file system", 'data'],
    ['a file from the host running inside the guest', 'data']], [2, 0]],
  ['clipboard', false, [["reading the host's clipboard", 'data'], ['passing data from host to guest', 'data']], [1, 0]],
  ['guest agent channel', false, [['running a command in the guest from the host', 'data'],
    ["reading the guest's file system from the host", 'information']], [4, 0]],
  ['USB passthrough', false, [["a plugged-in device reaching the host's driver", 'transit']], [1, 0]],
];
export const EFFECT = ['transit', 'data', 'information']; const WIDTH = { transit: 9, data: 6, information: 13 };
export const pathCount = (list) => list.reduce((t, [, , y]) => t + y.length, 0);
export const effectCount = (list, e) => list.reduce((t, [, , y]) => t + y.filter(([, k]) => k === e).length, 0);

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = (x, n) => String(x).padStart(n);
  const T = pathCount(SHARING);
  console.log(`${SHARING.length} shared items, ${T} paths; average per item `
    + `${(T / SHARING.length).toFixed(2)} paths`);
  console.log(`\n${'item'.padEnd(30)}${'type'.padEnd(14)}${s('paths', 7)}${s('transit', 9)}${s('data', 6)}`
    + `${s('information', 13)}${s('extra steps', 13)}${s('extra memory', 14)}`);
  for (const k of SHARING) {
    const [ad, required, y, [adim, mib]] = k;
    console.log(`${ad.padEnd(30)}${(required ? 'required' : 'optional').padEnd(14)}${s(y.length, 7)}`
      + EFFECT.map((e) => s(effectCount([k], e), WIDTH[e])).join('')
      + `${s(required ? '-' : adim, 13)}${s(required ? '-' : mib, 14)}`);
  }
  for (const [ad, f] of [['required total', true], ['optional total', false]]) {
    const g = SHARING.filter(([, z]) => z === f);
    const [adim, mib] = g.reduce(([a, m], [, , , [x, y]]) => [a + x, m + y], [0, 0]);
    console.log(`${ad.padEnd(44)}${s(pathCount(g), 7)}` + EFFECT.map((e) => s(effectCount(g, e), WIDTH[e])).join('')
      + `${s(f ? '-' : adim, 13)}${s(f ? '-' : mib, 14)}`);
  }
}
```

```
11 shared items, 16 paths; average per item 1.45 paths

item                          type            paths  transit  data  information  extra steps  extra memory
scheduler                     required            1        0     0            1            -             -
processor cache               required            1        0     0            1            -             -
clock source                  required            1        0     0            1            -             -
disk controller emulation     required            1        1     0            0            -             -
network interface emulation   required            2        1     1            0            -             -
memory page merging           optional            2        0     0            2            0          1856
display controller emulation  optional            1        1     0            0            2             0
shared folder                 optional            2        0     2            0            2             0
clipboard                     optional            2        0     2            0            1             0
guest agent channel           optional            2        0     1            1            4             0
USB passthrough               optional            1        1     0            0            1             0
required total                                    6        2     1            3            -             -
optional total                                   10        2     5            3           10          1856
```

Eleven items open sixteen paths; the average per item is 1.45. That average alone is a warning:
closing a shared item does not always close one path — five of the eleven carry **two**, as does
leaving one open.

The effect columns split the list in two. The five required items open six paths, and three of
those are pure information inference: when the neighbor runs, which memory page is shared, the
host's maintenance window. Two are transit, and both come from a piece of emulation code. Since the
guest must see a disk and a network card, those two pieces of emulation code run on the host in
every run; these two transit paths **are not a closable item.**

The six optional items open ten paths and the distribution flips: five of six **data** paths are
here. The clipboard, the shared folder, and the agent channel — all three items
opened to make one person's work easier — carry five of the six host-data paths. The
right two columns write the cost of that: ten work steps and 1,856 MiB of host memory.

#### What Closing Closes, and What It Leaves

The run below closes the optional items one by one — first the one that opens the most paths, and
on a tie the cheaper one — and writes the closed path together with its cost at each step.

```js
// closing.mjs — optional shares are closed one by one; the closed path and its cost are counted together.
import { SHARING, EFFECT, pathCount, effectCount } from './sharing.mjs';

const T = pathCount(SHARING);
const optional = SHARING.filter(([, z]) => !z)
  .sort((a, b) => b[2].length - a[2].length || a[3][0] - b[3][0]);   // most paths first, then the cheaper one
const s = (x, n) => String(x).padStart(n);
console.log(`${'closed item'.padEnd(30)}${s('closed', 8)}${s('total', 7)}${s('remaining', 11)}`
  + `${s('extra steps', 13)}${s('extra memory', 14)}`);
let closed = 0, steps = 0, mem = 0;
for (const [ad, , y, [a, m]] of optional) {
  closed += y.length; steps += a; mem += m;
  console.log(`${ad.padEnd(30)}${s(y.length, 8)}${s(closed, 7)}${s(T - closed, 11)}${s(steps, 13)}${s(mem, 14)}`);
}

const remaining = SHARING.filter(([, z]) => z);
console.log(`\nclosable ${optional.length}/${SHARING.length} items, closed ${closed}/${T} paths `
  + `(${((100 * closed) / T).toFixed(1)}%); cost ${steps} work steps and ${mem} MiB of host memory`);
console.log(`${'effect'.padEnd(13)}${s('initial paths', 15)}${s('remaining paths', 17)}`);
for (const e of EFFECT)
  console.log(`${e.padEnd(13)}${s(effectCount(SHARING, e), 15)}${s(effectCount(remaining, e), 17)}`);
console.log(`while the guest runs, ${remaining.length} items cannot be closed, leaving ${pathCount(remaining)} paths`);

// Layout: four fictional guests on the same host, two trust levels.
const GUESTS = [['aggregator', 'high'], ['validator', 'high'], ['field portal', 'low'], ['test', 'low']];
const pairs = GUESTS.flatMap((a, i) => GUESTS.slice(i + 1).map((b) => [a[1], b[1]]));
const mixed = pairs.filter(([a, b]) => a !== b).length;
console.log(`\nsingle host: ${GUESTS.length} guests, ${pairs.length} neighbor pairs, ${mixed} pairs from different levels`);
console.log(`separate host per level: ${pairs.length - mixed} pairs, 0 from different levels; `
  + `remaining paths still ${pathCount(remaining)}, cost: 1 more machine`);
```

```
closed item                     closed  total  remaining  extra steps  extra memory
memory page merging                  2      2         14            0          1856
clipboard                            2      4         12            1          1856
shared folder                        2      6         10            3          1856
guest agent channel                  2      8          8            7          1856
USB passthrough                      1      9          7            8          1856
display controller emulation         1     10          6           10          1856

closable 6/11 items, closed 10/16 paths (62.5%); cost 10 work steps and 1856 MiB of host memory
effect         initial paths  remaining paths
transit                    4                2
data                       6                1
information                6                3
while the guest runs, 5 items cannot be closed, leaving 6 paths

single host: 4 guests, 6 neighbor pairs, 4 pairs from different levels
separate host per level: 2 pairs, 0 from different levels; remaining paths still 6, cost: 1 more machine
```

When six items are closed, ten of the sixteen paths close and six remain. The first row looks free
at first glance: memory page merging closes two information paths and the changed work step count is
zero. The cost is not in the workflow but in memory — 1,856 MiB of host memory, because merged pages
are now kept separately. **An item whose cost looks like zero usually has its cost in another
column.**

The effect table shows the real gain. Data paths drop from six to **one**; the single remaining data
path is the one carried by the network emulation. So five out of six paths touching host data were
inside the closable convenience items. Transit paths drop from four to two, information paths from
six to three. The total cost is ten work steps: file transfer is done over the network, closing and
reopening procedures are run by hand, and reading field devices moves to the branch client.

The remaining six paths are this lesson's boundary. As long as the guest runs, a scheduler, a
processor cache, a clock source, a disk, and a network interface are shared. These cannot be closed,
because closing them means the guest does not run either. What remains is to ask **how readily**
those six paths operate.

#### Items Required per Path

Up to this point, every path was matched with a single shared item. Read that way, it looks as if
the share's existence is the path's only condition; for most paths that is not true. The run below
gives each path the extra conditions required **on top of** the share and works out how many items
must be true at the same time to cross the boundary.

```js
// conditions.mjs — number of items that must be true at the same time, for each path (MODEL).
// The shared item's existence is one item; the list below gives it EXTRA conditions.
import { SHARING } from './sharing.mjs';

const EXTRA = {
  "neighboring guest's activity timing": [],
  'access pattern from a shared cache line': ['being scheduled onto the same core as the neighbor'],
  "inferring the host's maintenance window": [],
  'transit into the host process via an emulation bug': ['an unpatched emulation bug'],
  "guest packet into the host's network stack": ['the host interface being bridged to the guest network'],
  'measuring whether the same page exists': ['identical page content in the neighbor'],
  'page sharing inferred from write latency': ['identical page content in the neighbor'],
  "writing to the host's file system": ['the mount being writable'],
  'a file from the host running inside the guest': ['execute permission in the guest', 'the file being opened'],
  "reading the host's clipboard": [], 'passing data from host to guest': [],
  'running a command in the guest from the host': ['agent privilege on the host side'],
  "reading the guest's file system from the host": ['agent privilege on the host side'],
  "a plugged-in device reaching the host's driver": ['physical access', 'an unpatched driver bug'],
};
// [required items, path name, from a required item?]
const PATHS = SHARING.flatMap(([, z, y]) => y.map(([ad]) => [1 + EXTRA[ad].length, ad, z]));

const s = (x, n) => String(x).padStart(n);
console.log(`${'required items'.padEnd(16)}${s('paths', 6)}   example`);
for (const g of [...new Set(PATHS.map(([n]) => n))].sort()) {
  const k = PATHS.filter(([n]) => n === g);
  console.log(`${String(g).padEnd(16)}${s(k.length, 6)}   ${k[0][1]}`);
}
const required = PATHS.filter(([, , z]) => z);
console.log(`\nrequired items in the ${required.length} paths that cannot be closed: `
  + [...new Set(required.map(([n]) => n))].sort()
    .map((g) => `${g} -> ${required.filter(([n]) => n === g).length} paths`).join(', '));
const secondary = PATHS.flatMap(([, ad]) => EXTRA[ad]);
const counts = [...new Set(secondary)].map((k) => [k, secondary.filter((x) => x === k).length])
  .sort((a, b) => b[1] - a[1]);
console.log(`most frequent extra condition: ${counts[0][0]} (${counts[0][1]} paths); total ${secondary.length} extra conditions, `
  + `${counts.length} distinct items`);
```

```
required items   paths   example
1                    4   neighboring guest's activity timing
2                   10   access pattern from a shared cache line
3                    2   a file from the host running inside the guest

required items in the 6 paths that cannot be closed: 1 -> 2 paths, 2 -> 4 paths
most frequent extra condition: an unpatched emulation bug (3 paths); total 14 extra conditions, 10 distinct items
```

**Four** of the sixteen paths work with a single item: if the share is open, the path is open, no
other condition is required. Those four are the scheduler, the clock source, and the clipboard's two
directions; none of the three depends on a bug or a placement — each exists merely by the share's
definition. Ten paths need two items, two paths need three items.

Two of the six paths that cannot be closed work with a single item, and both are information paths.
The remaining four want a second item, and the most frequently required second item is **an
unpatched emulation bug** — the same condition in three paths. This ties the measure of crossing the
boundary to patching: on a host where the emulation code is patched, those three paths require two
items and the second is not in place. The patch program itself is not this course's subject, but the
number here writes what it changes: **it closes the second item of three transit paths.**

#### The Six Paths That Cannot Be Closed

What can be done for a path that cannot be closed is changing **where it reaches**. The last two
rows measure this. While four guests run on the same host, six neighbor pairs form, and four of them
are between different trust levels: the guest that aggregates meter readings and the test guest, the
validator and the field portal. The remaining six paths work between these four pairs.

Distributing the guests across two separate hosts by trust level closes none of the paths — each
host still has six paths — but it drops different-level pairs from four to **zero**.
In the remaining two pairs, the neighbors are already systems that access the same data; the
information one neighbor extracts from the other is information it already has within its own
boundary. The cost is one machine.

This measurement gives an ordering, not a preference. First the six optional items are closed: ten
paths go, at a cost of ten work steps and 1,856 MiB. Then the layout is corrected: no path goes, but
the ends of the remaining six paths are tied to the same trust level, at a cost of one machine. There
is no third step; as long as the guest runs, those six paths are open, and the defense's answer there
is not closing but **watching** — which piece of emulation code returns an unexpected error, which
guest shows activity during its neighbor's maintenance window.

#### Summary

- The guest and host share eleven items, and these items open sixteen paths; the average per item is
  1.45 paths.
- The five required items open six paths: two transit, one data, three information. The two transit
  paths come from disk and network emulation and cannot be closed while the guest runs.
- The six optional items open ten paths and carry five of the six data paths; closing them, data
  paths drop from six to one, transit from four to two, information from six to three.
- Cost is written in two columns: ten changed work steps and 1,856 MiB of host memory. Memory page
  merging's work-step cost is zero; its cost is in memory.
- Four of the sixteen paths work with a single item; of the six remaining paths, two need one item
  and four need two, and the most frequently required second item is an unpatched emulation bug.
- Splitting the layout by trust level does not change the path count; it drops the different-level
  neighbor pair count from four to zero, at a cost of one machine.

#### Next Step

This lesson's and the previous lesson's measurement were both about a **running** guest: which layer
it stands beneath, which items it shares with the host. But a virtual machine's real distinguishing
feature does not show up while it runs, but while it is **stopped and copied**. A guest snapshot can
be taken and stored, an image can be cloned thirty times and distributed to thirty machines, a state
from a week ago can be returned to in a few seconds. The next lesson measures these three operations
in boundary items: how many secret items a clone carries to how many copies, how many sound links a
rollback needs as the snapshot chain grows longer, and how many closed vulnerabilities returning to
a week ago reopens.

### Virtual Machine Management

Source: https://academia.sh/en/courses/it-fundamentals/virtual-machine-management

Measuring cloning and snapshotting in terms of boundary items: how many of a golden image's seven secret items are renewed on first boot and how many stay identical across every clone, the step cost of de-duplication, the number of sound links required to return to each point in a seven-point snapshot chain, and the vulnerabilities a rollback reopens along with the audit records it deletes.

The previous two lessons measured a running guest: how many layers stand beneath it, how many items
it shares with the host. But the real feature that separates a virtual machine from a physical one
shows up not while it runs, but while it is **stopped and copied**. A guest is nothing more than a
file: it can be snapshotted and stored, its image can be cloned twenty-four times and distributed to
twenty-four machines, and it can be returned to a week-old state in a few seconds. These three
operations lighten management, and every one of them carries a boundary item.

What is measured is not how easy the operations are, but the items they carry. For cloning the
question is this: how many items do machines produced from one image carry in common, and if one of
those items falls, how many machines are reached. For the snapshot the question is chain length: how
many links need to be sound to return to a given point. For rollback the question is loss: how many
vulnerabilities, closed between the point returned to and today, reopen.

**VR13 — In the fictional branch fleet, 24 guests are cloned from a single golden image**; the item
list is a model, the count is real. **VR14 — A secret item is a hidden value that is identical
across two machines when copied.** **VR15 — De-duplication is the step that regenerates one item on
a clone's first boot**; its cost is written as steps and the number of service restarts. **VR16 —
The snapshot chain is incremental**: each link is written on top of the one before it, and opening a
point requires the base plus every link up to that point. **VR17 — The validator guest's schedule
spans 42 days**; it contains six snapshots and six patch events, and the patch events close 17
vulnerabilities in total. **VR18 — The guest produces 240 audit records a day.** **VR19 — Four
components run on the guest**, and each is consistent only at a given readiness level; the cost of
that readiness level is the interruption seconds given to the guest.

#### The Secret a Clone Carries

A golden image is installed once, hardened, and distributed to branches. Part of the files inside it
are secret values specific to that installation, and cloning copies those too.

```js
// clone.mjs — secret items carried by clones produced from a golden image (MODEL).
// Fictional branch fleet: 24 guests are cloned from a single golden image.
const CLONES = 24;
// [item, renewed on first boot?, extra steps the renewal needs, service restarts]
const SECRET = [
  ['host key', true, 1, 1], ['machine identity', true, 1, 0],
  ['management agent token', false, 1, 1], ['log shipping key', false, 1, 1],
  ['admin session token cache', false, 1, 0],
  ['local admin password hash', false, 1, 0], ['certificate private key', false, 2, 1],
];
const s = (x, n) => String(x).padStart(n);
console.log(`golden image carries ${SECRET.length} secret items; ${CLONES} clones are produced`);
console.log(`${'item'.padEnd(36)}${s('renewed', 12)}${s('machines staying identical', 28)}`);
for (const [ad, y] of SECRET)
  console.log(`${ad.padEnd(36)}${s(y ? 'yes' : 'no', 12)}${s(y ? 1 : CLONES, 28)}`);

const shared = SECRET.filter(([, y]) => !y);
console.log(`\nrenewed ${SECRET.length - shared.length}, shared ${shared.length}; `
  + `shared item-machine pairs ${shared.length * CLONES}`);
console.log(`required items for one shared secret item: 1, machines reached ${CLONES}`);
const [steps, restarts] = shared.reduce(([a, r], [, , x, y]) => [a + x, r + y], [0, 0]);
console.log(`cost of renewing all five shared items: ${steps} first-boot steps, `
  + `${restarts} service restarts, per clone`);
console.log(`fleet-wide ${steps * CLONES} steps; if a single item is left unrenewed, machines reached `
  + `is still ${CLONES}`);
```

```
golden image carries 7 secret items; 24 clones are produced
item                                     renewed  machines staying identical
host key                                     yes                           1
machine identity                             yes                           1
management agent token                        no                          24
log shipping key                              no                          24
admin session token cache                     no                          24
local admin password hash                     no                          24
certificate private key                       no                          24

renewed 2, shared 5; shared item-machine pairs 120
required items for one shared secret item: 1, machines reached 24
cost of renewing all five shared items: 6 first-boot steps, 3 service restarts, per clone
fleet-wide 144 steps; if a single item is left unrenewed, machines reached is still 24
```

Two of the seven items are renewed. These two are renewed because, unrenewed, the system breaks
visibly: if two machines' machine identity is the same, network management cannot tell them apart;
if the host key is the same, connecting clients raise a warning. The remaining five are not renewed
because, unrenewed, **nothing breaks**. Twenty-four machines carry the same management agent token,
the same log shipping key, and the same certificate private key, and the fleet runs without issue.

The shared item-machine pair count is one hundred twenty. This number is not damage, it is surface.
The real number lies beneath it: for a secret item, the item required to cross the boundary is
**one**, and the machines reached when that one item falls is **twenty-four**. A single private key
read from one machine's disk is the same key at twenty-four branches. Cloning does not open a path;
**it raises the number of machines at the end of an existing path to twenty-four.**

The cost column is small this time. De-duplicating all five items requires six first-boot steps and
three service restarts per clone, one hundred forty-four steps fleet-wide — a one-time, scriptable
job. The last line is the harshest spot in the measurement: **if a single item is left unrenewed,
the machines reached are still twenty-four.** De-duplication cannot be done partially; if four are
renewed and one is forgotten, the number gained is zero.

#### The Snapshot Chain

A snapshot is not a copy but a difference: each link holds the change written on top of the one
before it. This makes taking a snapshot cheap and ties reading it to the length of the chain.

```js
// chain.mjs — snapshot chain and the vulnerabilities a rollback brings back (MODEL).
// Fictional validator guest's 42-day schedule: six snapshots, six patch events.
const TODAY = 42, DAILY = 240;                  // 240 audit records a day
const POINTS = [['base', 0], ['snap-1', 3], ['snap-2', 10], ['snap-3', 17],
  ['snap-4', 24], ['snap-5', 31], ['snap-6', 38]];
const PATCH = [[5, 3], [12, 1], [19, 4], [26, 2], [33, 5], [40, 2]];  // [day, vulnerabilities closed]
const BASE_GIB = 40, LINK_GIB = 1.9;

const reopened = (day) => PATCH.filter(([g]) => g > day).reduce((t, [, n]) => t + n, 0);
const s = (x, n) => String(x).padStart(n);
console.log(`today is day ${TODAY}; ${POINTS.length - 1} snapshots, ${PATCH.length} patch events, `
  + `vulnerabilities closed ${reopened(0)}`);
console.log(`\n${'point restored'.padEnd(16)}${s('day', 5)}${s('links required', 16)}`
  + `${s('vulnerabilities reopened', 26)}${s('audit records lost', 23)}`);
POINTS.forEach(([ad, day], i) => {
  console.log(`${ad.padEnd(16)}${s(day, 5)}${s(i + 1, 16)}${s(reopened(day), 26)}`
    + `${s((TODAY - day) * DAILY, 23)}`);
});

const links = POINTS.length - 1;
console.log(`\nchain: base ${BASE_GIB} GiB + ${links} links x ${LINK_GIB} GiB = `
  + `${(BASE_GIB + links * LINK_GIB).toFixed(1)} GiB; opening the newest point reads ${POINTS.length} links`);
const CORRUPT = 3;                              // snap-3's delta is corrupt
const [ad, day] = POINTS[CORRUPT - 1];
console.log(`link ${CORRUPT} corrupt -> newest point restorable ${ad} (day ${day}), `
  + `vulnerabilities reopened ${reopened(day)}, records lost ${(TODAY - day) * DAILY}`);
console.log(`the ${POINTS.length - CORRUPT} points after the corrupt link cannot be opened; `
  + `the chain requires ${POINTS.length} sound links`);
```

```
today is day 42; 6 snapshots, 6 patch events, vulnerabilities closed 17

point restored    day  links required  vulnerabilities reopened     audit records lost
base                0               1                        17                  10080
snap-1              3               2                        17                   9360
snap-2             10               3                        14                   7680
snap-3             17               4                        13                   6000
snap-4             24               5                         9                   4320
snap-5             31               6                         7                   2640
snap-6             38               7                         2                    960

chain: base 40 GiB + 6 links x 1.9 GiB = 51.4 GiB; opening the newest point reads 7 links
link 3 corrupt -> newest point restorable snap-2 (day 10), vulnerabilities reopened 14, records lost 7680
the 4 points after the corrupt link cannot be opened; the chain requires 7 sound links
```
The third column writes the chain's price: returning to the newest point requires all seven of the
seven links to be sound. This number is good for isolation, bad for resilience — wanting the item
required to cross a boundary to be high is desirable, wanting the links required to open a recovery
point to be high is not. **As the snapshot chain grows longer, rollback does not get easier — it
becomes more tied to a single point.**

The fourth column is rollback's real cost. In forty-two days, six patch events closed seventeen
vulnerabilities. Returning to the base reopens all seventeen; returning to the day-38 point reopens
only two. The difference is not a choice but a scheduling problem: the patch was applied on day 40,
the closest snapshot was taken on day 38. **The cost of a rollback is not the age of the point
restored, but how many patch events remain between that point and today.**

The fifth column counts a quieter item. Rollback does not only bring back the vulnerability, it also
deletes the audit records produced between the point restored and today. Returning to the base takes
away 10,080 records, returning to day 38 takes away 960. Returning to a clean state after an incident
means also deleting the records that carry that incident's trace — which is why copying the records
out of the guest before a rollback is a separate step.

The last two rows show the chain's fragility. When the third link is corrupted, all four of the four
points after it become impossible to open. The newest point restorable drops thirty-two days back,
to day 10; the vulnerabilities reopened climb from two to fourteen, the records lost from 960 to
7,680. There were seven recovery points, and a single corrupt link took away four of them at once.
**The number of points in a chain is not the number of independent recovery points.**

#### The Moment a Snapshot Is Consistent

The chain table counted every point as a single "state." That hides a question that was never
asked: at the moment the snapshot was taken, the guest **was running**, and the host captured the
disk's state at that instant. What is on disk and what the components hold in memory are not the
same instant.

```js
// consistency.mjs — the moment a snapshot is consistent vs. the moment the components are consistent (MODEL).
// A host-level snapshot captures the disk's state at that instant; a pending in-memory write is not on disk.
// [component, readiness level required to be consistent, manual recovery steps on restore]
const COMPONENTS = [['file system metadata', 1, 1], ['file queue', 1, 1],
  ['database pages', 2, 3], ['in-memory application state', 3, 2]];
// [readiness level, name, interruption given to the guest (seconds)]
const LEVEL = [[0, 'no preparation', 0], [1, 'flushing writes inside the guest', 0.4],
  [2, 'pausing the application', 3.2], [3, 'stopping the guest', 95]];

const s = (x, n) => String(x).padStart(n);
console.log(`${'readiness'.padEnd(33)}${s('consistent', 12)}${s('half-done', 11)}${s('interruption s', 16)}`
  + `${s('manual recovery steps', 24)}`);
for (const [d, ad, sn] of LEVEL) {
  const half = COMPONENTS.filter(([, g]) => g > d);
  console.log(`${ad.padEnd(33)}${s(`${COMPONENTS.length - half.length}/${COMPONENTS.length}`, 12)}`
    + `${s(half.length, 11)}${s(sn.toFixed(1), 16)}${s(half.reduce((t, [, , a]) => t + a, 0), 24)}`);
}
const full = LEVEL.find(([d]) => COMPONENTS.every(([, g]) => g <= d));
console.log(`\nall four of the four components are consistent only with "${full[1]}"; interruption ${full[2]} seconds`);
console.log(`an unprepared snapshot leaves ${COMPONENTS.filter(([, g]) => g > 0).length} items half-done; `
  + `the snapshot operation still returns successful`);
```

```
readiness                          consistent  half-done  interruption s   manual recovery steps
no preparation                            0/4          4             0.0                       7
flushing writes inside the guest          2/4          2             0.4                       5
pausing the application                   3/4          1             3.2                       2
stopping the guest                        4/4          0            95.0                       0

all four of the four components are consistent only with "stopping the guest"; interruption 95 seconds
an unprepared snapshot leaves 4 items half-done; the snapshot operation still returns successful
```

The first row is the lesson's most misleading row: interruption is zero, the operation returns
successful, and all four of the four components are half-done. The host does not see what happens
inside the guest; it copies the disk's state at that instant and counts the job as complete.
Flushing writes inside the guest makes two components consistent, at a cost of 0.4 seconds. Database
pages become consistent only by pausing the application, in-memory application state only by
stopping the guest — in exchange for a ninety-five-second interruption.

The right column writes the difference on the restore side: restoring from an unprepared snapshot
requires seven manual recovery steps, restoring from one taken with the application paused requires
two. **The moment a snapshot is consistent, for the host, is the moment the disk was frozen; for the
data layer it is only the moment pending writes reach disk, and the two are not the same moment
unless preparation is done.**

#### The Boundary's Three Columns

This lesson's boundary is the one drawn by management operations, and it fills the three columns as
follows. The boundary's name is the **clone and snapshot boundary**. The number of items required to
cross the boundary is **one**: any one of the five secret items shared across twenty-four machines.
The number of items reached when the boundary falls is **twenty-four machines** — cloning is an
operation that multiplies the number of machines behind a single item.

The snapshot adds a second row to this table, and in that row the number works in the reverse
direction. The sound links required to open a point is seven; this is not an isolation gain but a
resilience debt. Rollback, meanwhile, does not cross the boundary — it **reopens boundaries that were
already closed**: it can go back as far as seventeen vulnerabilities, and this is done without using
any escape path.

What the three measurements share is that all three are operations that **make management easier**.
Cloning sets up twenty-four machines from one image, a snapshot fits a week's work into a single
file, rollback brings a broken system back to running in seconds. The boundary item of all three
comes from the same place: a machine's state becoming a **copyable object**. When that object is
copied, the secret inside it is copied too; when it is stored, the patch level at that instant is
stored too; when it is restored, that level is returned to.

#### Summary

- Two of the golden image's seven secret items are renewed on first boot, five stay identical across
  twenty-four clones; the shared item-machine pair count is 120.
- For a shared secret item, the item required to cross the boundary is one and the machines reached
  is twenty-four; if a single item is left unrenewed, this number is still twenty-four.
- The cost of de-duplicating all five items is six first-boot steps and three service restarts per
  clone, 144 steps fleet-wide.
- In the seven-point incremental chain, opening the newest point requires seven links to be sound;
  when the third link is corrupted, four points become impossible to open at once.
- Rollback undoes the patches between the point restored and today: returning to the base reopens 17
  vulnerabilities and deletes 10,080 audit records, returning to day 38 reopens 2 vulnerabilities and
  deletes 960 records.
- In an unprepared snapshot, all four of the four components are half-done and restoring requires
  seven manual steps; making all four consistent requires stopping the guest and a 95-second
  interruption.

#### Next Step

The three lessons so far asked the same question about a single technology: where is the guest
system's boundary, how many items cross it, and what is reached if it is crossed. There is a second
way to do the same job, and in that way there is no guest system at all: processes separated from one
another run on top of the same kernel. If two ways do the same job, they can be compared with the
same measure. The next lesson does this: for the same eight targets, how many items are required to
cross the boundary in a guest system, how many in a compartment sharing the same kernel; what a shared
kernel means in shared items, and where the difference between the two numbers comes from.

### Comparison with Containers

Source: https://academia.sh/en/courses/it-fundamentals/comparison-with-containers

Measuring the same isolation question across two technologies in terms of items required to cross the boundary: the guest system's five required shared items against the twelve shared by a compartment sharing the same kernel, the number of items that must be wrong at the same time for each of eight targets, the number of targets crossed with a single item, and the concentration of required items in a single item.

The previous three lessons asked the same question about a single technology: where is the guest
system's boundary, how many items cross it, and what is reached if it is crossed. There is a second
way to do the same job, and in that way there is no guest system at all: compartments separated from
one another run on top of the same kernel. Both arrangements are used on the regional measurement
network's branch servers — the validator in a guest system, three copies of the reading aggregator
in compartments sharing the same kernel.

This lesson does not compare the two technologies on speed, size, or ease of operation; those
measurements were made in the Containers course and are not repeated here. The only question here is
isolation strength, and that strength is written in this course's unit of measure: **the number of
items that must be wrong at the same time to cross the boundary.** The two technologies are compared
on the same set of targets with this number.

**VR20 — The comparison is between two arrangements doing the same job**: a guest system and a
compartment sharing the same kernel. **VR21 — The guest system's five required shared items are an
input from the previous lesson**; the compartment's list is counted with the same measure. **VR22 — A
namespace separates the view, not the resource**; this distinction is kept as a separate column in
the item count. **VR23 — The target set is eight items**, and for each target the required items are
written by name; item names are a model, the count is real. **VR24 — Concentration is how many
targets the same item appears in.** **VR25 — The access universe is 52 items**: nine items each for
four workloads, the host's thirteen items, and three config items.

#### Number of Shared Items

Running on the same host, the two arrangements share different numbers of items. In a guest system,
the sharing is as much as the hypervisor opens; in a compartment, the sharing is the kernel itself.

```js
// compartment.mjs — items a guest system vs. a compartment sharing the same kernel shares with the host (MODEL).
// The guest system's five required items are an input from the previous lesson; the compartment is counted with the same measure.
export const GUEST = [['scheduler', false], ['processor cache', false], ['clock source', false],
  ['disk controller emulation', false], ['network interface emulation', false]];
// [item, does its view get separated by a namespace?] — the resource is shared on every row
export const CONTAINER = [['scheduler', false], ['processor cache', false], ['clock source', false],
  ['system call surface', false], ['kernel data structures', false], ['page cache', false],
  ['kernel log buffer', false], ['kernel module set', false],
  ['process ID space', true], ['user ID space', true], ['network stack', true],
  ['file system view', true]];

if (import.meta.url === `file://${process.argv[1]}`) {
  const s = (x, n) => String(x).padStart(n);
  console.log(`${'technology'.padEnd(37)}${s('shared items', 18)}${s('view separated', 18)}`
    + `${s('resource shared', 17)}`);
  for (const [ad, liste] of [['guest system', GUEST], ['compartment sharing the same kernel', CONTAINER]])
    console.log(`${ad.padEnd(37)}${s(liste.length, 18)}`
      + `${s(liste.filter(([, a]) => a).length, 18)}${s(liste.length, 17)}`);
  const notSeparated = CONTAINER.filter(([, a]) => !a).map(([k]) => k);
  console.log(`\nin the compartment, ${notSeparated.length} items with no separated view:`);
  for (let i = 0; i < notSeparated.length; i += 4) console.log(`  ${notSeparated.slice(i, i + 4).join(', ')}`);
  console.log(`ratio: compartment / guest = ${(CONTAINER.length / GUEST.length).toFixed(1)}x shared items`);
}
```

```
technology                                 shared items    view separated  resource shared
guest system                                          5                 0                5
compartment sharing the same kernel                  12                 4               12

in the compartment, 8 items with no separated view:
  scheduler, processor cache, clock source, system call surface
  kernel data structures, page cache, kernel log buffer, kernel module set
ratio: compartment / guest = 2.4x shared items
```
The compartment shares 2.4 times the items. That ratio alone is not a conclusion, it is a baseline:
every shared item is a path candidate, and the candidate count has risen from five to twelve. Two of
the guest system's five items are a piece of emulation code — that is, a limited, single-purpose
piece of software running on the host side. The compartment's twelve items, by contrast, are directly
the kernel itself; there is no emulation layer placed in between.

The second column is more interesting. Only four of the twelve items have their view separated by a
namespace: process ID, user ID, network stack, file system view. The remaining eight do not even
have a separate view — the scheduler, the processor cache, the clock, the system call surface,
kernel data structures, the page cache, the kernel log buffer, and the kernel module set are the same
object for the compartment and for the host. The third column settles the distinction: **the resource
is shared** in all twelve of the twelve items. Even in the four items that have a namespace, what is
separated is the view; the underlying ID space and data structure are singular.

#### Eight Targets, Two Numbers

The number of shared items is not isolation strength; isolation strength is the number of items that
must be wrong at the same time to reach a given target. The run below writes the same eight targets
in both arrangements.

```js
// targets.mjs — the same eight targets in both technologies: items that must be wrong at the same
// time to cross the boundary. Item names are a model, the count is real.
const TARGETS = [
  ["reading the neighbor's files", ['hypervisor bug', "access to the neighbor's image"], ['kernel bug']],
  ["reading the neighbor's memory", ['hypervisor bug', 'memory isolation flaw'],
    ['kernel bug', 'process-tracing privilege']],
  ["writing to the host's file system", ['hypervisor bug', 'host write privilege'], ['a writable mount']],
  ['running code in the host kernel', ['hypervisor bug', 'host kernel bug'], ['kernel bug']],
  ["changing the host's clock", ['hypervisor bug', 'host clock privilege'], ['clock-change privilege']],
  ["exhausting the neighbor's resources", ['an open resource limit'], ['an open resource limit']],
  ["reading the host's secrets", ['hypervisor bug', 'host file access'],
    ['the secret file being mounted', 'read permission']],
  ['reaching an endpoint on the host network', ['an open network path'], ['an open network path']],
];
const NAMES = ['guest system', 'compartment'];
const s = (x, n) => String(x).padStart(n);
console.log(`${'target'.padEnd(42)}${s(NAMES[0], 14)}${s(NAMES[1], 11)}`);
for (const [h, a, b] of TARGETS) console.log(`${h.padEnd(42)}${s(a.length, 14)}${s(b.length, 11)}`);
const required = (i) => TARGETS.reduce((t, h) => t + h[i + 1].length, 0);
const single = (i) => TARGETS.filter((h) => h[i + 1].length === 1).length;
console.log(`${'total required items'.padEnd(42)}${s(required(0), 14)}${s(required(1), 11)}`);
console.log(`${'targets crossed with a single item'.padEnd(42)}${s(single(0), 14)}${s(single(1), 11)}`);

for (let i = 0; i < 2; i++) {                    // concentration: the most frequent item
  const all = TARGETS.flatMap((h) => h[i + 1]);
  const [item, n] = [...new Set(all)].map((k) => [k, all.filter((x) => x === k).length])
    .sort((a, b) => b[1] - a[1])[0];
  const after = TARGETS.filter((h) => h[i + 1].filter((k) => k !== item).length <= 1).length;
  console.log(`\n${NAMES[i]}: most frequent item "${item}" in ${n}/${TARGETS.length} targets`);
  console.log(`with that item in place, targets crossed with a single item ${single(i)} -> ${after}`);
}
```

```
target                                      guest systemcompartment
reading the neighbor's files                           2          1
reading the neighbor's memory                          2          2
writing to the host's file system                      2          1
running code in the host kernel                        2          1
changing the host's clock                              2          1
exhausting the neighbor's resources                    1          1
reading the host's secrets                             2          2
reaching an endpoint on the host network               1          1
total required items                                  14         10
targets crossed with a single item                     2          6

guest system: most frequent item "hypervisor bug" in 6/8 targets
with that item in place, targets crossed with a single item 2 -> 8

compartment: most frequent item "kernel bug" in 3/8 targets
with that item in place, targets crossed with a single item 6 -> 7
```
The expected difference is in the table: six of the eight targets are crossed with a single item in
the compartment, two in the guest system. Total required items are fourteen against ten. On average,
reaching a target requires 1.75 items to be wrong at the same time in the guest system, 1.25 in the
compartment.

The two rows that come out equal show where this difference does **not** come from. Exhausting the
neighbor's resources and reaching an endpoint on the host network both demand a single item in either
arrangement, because these two targets depend not on the isolation boundary but on a configuration
item: if the resource limit was left open and the network path is open, the type of boundary does
not change the outcome. **Choosing an isolation technology does not compensate for a configuration
item being left open.**

The last two sections correct the table itself. The six "two items" in the guest system are not
independent: all six depend on the same second item, **the hypervisor bug**. With that item in
place, the number of targets crossed with a single item rises from two to **eight** — that is, the
guest system's isolation for all eight targets at once rests on the correctness of a single item. The
compartment has no such concentration: the most frequent item appears in three targets, and with that
item in place, the targets crossed with a single item rise from six to seven, not eight.

This is where the two technologies part ways. **The guest system gives a higher but more
concentrated number: six of the eight targets depend on a single item. The compartment gives a
lower but more scattered number: six separate targets depend on five separate items.** In the
first, defense has one clear place to go; in the second, five separate items must be kept correct,
separately.

#### Items Reached When the Boundary Falls

The required item count is the second of three columns; the third is how many items are reached once
the boundary is crossed. This number comes from layout, and it splits not between two technologies
but among three arrangements.

```js
// fall.mjs — items reached when the boundary falls, across three layout arrangements (MODEL).
// Fictional branch server: four workloads, each carrying 6 file and 3 memory items.
const WORKLOADS = 4, FILES = 6, MEMORY = 3, HOST_FS = 9, HOST_SECRET = 4, CONFIG = 3;
const TOTAL = WORKLOADS * (FILES + MEMORY) + HOST_FS + HOST_SECRET + CONFIG;
// [layout, items required to cross the boundary, items reached on first fall, patch items to maintain]
const LAYOUT = [
  ['4 workloads in a single guest system', 2, WORKLOADS * (FILES + MEMORY) + CONFIG, 2],
  ['4 compartments on a single host', 1, TOTAL, 1],
  ['guest system + compartment per workload', 1, FILES + MEMORY, 5],
  ['  same layout, up to host level', 3, WORKLOADS * (FILES + MEMORY) + CONFIG, 5],
];
const s = (x, n) => String(x).padStart(n);
console.log(`universe ${TOTAL} items: ${WORKLOADS} workloads x ${FILES + MEMORY}, host ${HOST_FS + HOST_SECRET}, config ${CONFIG}`);
console.log(`\n${'layout'.padEnd(41)}${s('required', 9)}${s('reached', 10)}${s('patch items', 13)}`);
for (const [ad, g, e, y] of LAYOUT)
  console.log(`${ad.padEnd(41)}${s(g, 9)}${s(`${e}/${TOTAL}`, 10)}${s(y, 13)}`);
const [, , narrowest] = LAYOUT.reduce((a, b) => (b[2] < a[2] ? b : a));
console.log(`\nnarrowest first fall ${narrowest}/${TOTAL} (${((100 * narrowest) / TOTAL).toFixed(1)}%), `
  + `cost ${LAYOUT[2][3] - LAYOUT[1][3]} extra patch items`);
```

```
universe 52 items: 4 workloads x 9, host 13, config 3

layout                                    required   reached  patch items
4 workloads in a single guest system             2     39/52            2
4 compartments on a single host                  1     52/52            1
guest system + compartment per workload          1      9/52            5
  same layout, up to host level                  3     39/52            5

narrowest first fall 9/52 (17.3%), cost 4 extra patch items
```
The first two rows repeat the previous table's result on the access side: when four compartments run
on a single host, the boundary is crossed with one item and all fifty-two of the fifty-two items are
reached; when four workloads run in a guest system, the boundary demands two items and thirty-nine
items are reached. The reason the compartment's reached items equal the entire universe is structural:
the layer that draws the boundary is the kernel, and that kernel is the host itself; whoever crosses
it has also crossed into the host's file system and its secrets.

The third row shows that the two technologies are not opposites — they can be **stacked**. When each
workload runs inside a compartment within its own guest system, the items reached on the first fall
are nine of fifty-two — 17.3% — because escaping that compartment only reaches into its own guest
system. The items required to reach the host level rise to three: the compartment boundary's one item
is added to the guest system's two items. **When layers are stacked, the required items add up; this
is the only arrangement in the course where the required item rises to three.**

The right column writes the cost. Four compartments on a single host require one patch item — a single
kernel. Layout number three requires five patch items: one hypervisor and four separate guest
kernels. Four extra patch items are a continuous, recurring job; that is the price of bringing the
items reached down from 52 to 9.

#### How to Read the Number

This measurement does not say to prefer one technology over the other; it describes two different
defense programs. In the guest system, the program has a single item, and that item is the
hypervisor: keeping it patched, separating the management interface, not running other work on it.
The whole of this program watches one component, and when that component falls, all eight of the
eight targets are crossed with a single item. That is both the strength and the weakness of
concentration.

In the compartment, the program has six items: keeping the kernel patched, leaving the mount
read-only, not granting clock privilege, closing off process-tracing privilege, setting a resource
limit, closing the network path. When all six are kept correct separately, the number of targets
crossed with a single item drops; when one is forgotten, that target is crossed with a single item
and the others do not compensate for it.

The remaining two rows are the same in both: the resource limit and the network path. These two
items are outside the isolation technology, and each is sufficient on its own in either arrangement.

The third table shows that these two programs do not exclude one another. Running the workload
inside a compartment within its own guest system requires carrying out both programs — both the
hypervisor and the kernel will be kept patched, five patch items — but in exchange it brings the
items reached on the first fall down to nine of fifty-two and raises the items required to reach the
host level to three. **This is the only row in the course where the required item rises to three,
and it comes not from choosing a technology but from stacking two technologies.** The next lesson's
measure starts from the same item universe: the question of what is left once the boundary falls is
independent of the boundary's type.

#### Summary

- The guest system shares five required items with the host, the compartment sharing the same kernel
  shares twelve; the ratio is 2.4x.
- Only four of the twelve items in the compartment have a namespace separating the view, and all
  twelve share their resource; the remaining eight do not even have a separate view.
- Across eight targets, the total required items are 14 in the guest system and 10 in the compartment;
  the number of targets crossed with a single item is 2 against 6.
- Two targets — stopping via resource exhaustion and reaching an endpoint on the host network —
  demand a single item in either arrangement, because they depend not on the isolation boundary but
  on a configuration item.
- The guest system's six "two items" are not independent: all six depend on the hypervisor bug, and
  with that item in place, the targets crossed with a single item rise from 2 to 8. In the compartment
  the most frequent item appears in 3 targets, and the number rises from 6 to 7.
- Items reached on the first fall are 39/52, 52/52, and 9/52 across the three layouts; in the
  compartment-per-workload layout, reaching host level requires three items, at a cost of four extra
  patch items.

#### Next Step

Across four lessons the boundary was always measured from the same direction: what happens if
someone crosses it. That is not the only way a boundary falls. A disk fails, a file is deleted by
mistake, a host becomes unbootable — and in these cases no one crosses the boundary, yet the guest is
still lost. Virtualization has two separate answers to this question: the snapshot and the backup.
Both are called "bringing it back," and they do not bring back the same thing. The last lesson runs
both in the same loss scenario and counts the recovered and unrecovered items separately, computes
the point full, incremental, and differential backups can return to when the chain breaks, and closes
the course with an eleven-lesson boundary table.

### Backup and Resilience

Source: https://academia.sh/en/courses/it-fundamentals/backup-and-resilience

Running the snapshot and the backup through the same seven loss scenarios: the snapshot bringing 8/8 items in three scenarios, the backup bringing 6/8 items in seven scenarios, the two together recovering 48 of 56 items, the number of points lost when a file corrupts being 1, 18, and 1 in full, incremental, and differential backups, and storage splitting into 1,120, 91.3, and 758.2 GiB.

Across four lessons, the boundary was always measured from the same direction: what happens if
someone crosses it. That is not the only way a boundary falls. A storage device fails, a guest is
deleted by mistake, a host becomes unbootable — no one crosses the boundary, yet the guest is still
lost. Virtualization has two answers to this question, and both are called "bringing it back": the
**snapshot** and the **backup**.

They do not bring back the same thing. The snapshot sits on the host, on the same storage device the
guest's image sits on, and its retention is short; the backup sits outside the host, its retention is
long, and its scope is limited by a list. This lesson runs both through the same seven scenarios and
counts **the recovered and unrecovered items separately**.

**VR26 — The fictional validator guest carries eight items.** **VR27 — The snapshot sits on the host,
on the guest image's storage device**; retention is 7 days, the capture interval is 1 hour. **VR28 —
The backup sits outside the host**; retention is 90 days, the capture interval is 24 hours. **VR29 —
Two items sit outside the backup's scope**: local secrets and in-memory session state; this scope gap
was measured in the Servers, Networking and Hardening course, and here it is an input. **VR30 — A
method, if it works in a given scenario, brings back every item in its scope.** **VR31 — The backup
chain is a 28-day window**; the sizes are fictional.

#### Two Methods, the Same Loss Scenario

```js
// loss.mjs — snapshot vs. backup in the same loss scenarios: recovered and unrecovered items.
// Fictional validator guest carries 8 items. Scope gaps are an input from the previous course.
const ITEMS = ['operating system root files', 'application binaries', 'configuration',
  'database pages', 'work order queue', 'audit log', 'local secrets', 'in-memory session state'];
const OUT_OF_SCOPE = ['local secrets', 'in-memory session state'];   // outside the backup's scope
const SNAPSHOT = { scope: ITEMS.length, retention: 7, window: 1 };   // on the host, same storage device
const BACKUP = { scope: ITEMS.length - OUT_OF_SCOPE.length, retention: 90, window: 24 };
// [scenario, age of the loss (days), storage device lost, host lost, guest object deleted]
const SCENARIOS = [
  ['guest file system corruption', 0, false, false, false],
  ['misconfiguration (2 hours ago)', 0, false, false, false],
  ['files inside the guest being encrypted', 0, false, false, false],
  ['a record deleted 30 days ago', 30, false, false, false],
  ['loss of the storage device', 0, true, false, false],
  ['loss of the host machine', 0, false, true, false],
  ['guest deleted from the management interface', 0, false, false, true],
];
const snapshotOk = ([, age, b, k, s]) => !b && !k && !s && age <= SNAPSHOT.retention;
const backupOk = ([, age]) => age <= BACKUP.retention;

const s = (x, n) => String(x).padStart(n);
console.log(`${ITEMS.length} items; snapshot scope ${SNAPSHOT.scope}, retention ${SNAPSHOT.retention} days; `
  + `backup scope ${BACKUP.scope}, retention ${BACKUP.retention} days`);
console.log(`\n${'scenario'.padEnd(46)}${s('snapshot', 15)}${s('backup', 9)}${s('best', 10)}`);
let a = 0, y = 0, best = 0;
for (const sen of SCENARIOS) {
  const ka = snapshotOk(sen) ? SNAPSHOT.scope : 0, ky = backupOk(sen) ? BACKUP.scope : 0;
  a += ka; y += ky; best += Math.max(ka, ky);
  console.log(`${sen[0].padEnd(46)}${s(`${ka}/${ITEMS.length}`, 15)}${s(`${ky}/${ITEMS.length}`, 9)}`
    + `${s(`${Math.max(ka, ky)}/${ITEMS.length}`, 10)}`);
}
const T = SCENARIOS.length * ITEMS.length;
console.log(`${'total'.padEnd(46)}${s(`${a}/${T}`, 15)}${s(`${y}/${T}`, 9)}${s(`${best}/${T}`, 10)}`);
console.log(`\nsnapshot works in ${SCENARIOS.filter(snapshotOk).length}/${SCENARIOS.length} scenarios, `
  + `and when it works it brings back ${SNAPSHOT.scope}/${ITEMS.length} items`);
console.log(`backup works in ${SCENARIOS.filter(backupOk).length}/${SCENARIOS.length} scenarios, `
  + `every time with ${OUT_OF_SCOPE.length} items out of scope: ${OUT_OF_SCOPE.join(', ')}`);
console.log(`the two together: ${best}/${T}; unrecoverable ${T - best} items`);
console.log(`data-loss window: snapshot ${SNAPSHOT.window} hour, backup ${BACKUP.window} hours`);
```

```
8 items; snapshot scope 8, retention 7 days; backup scope 6, retention 90 days

scenario                                             snapshot   backup      best
guest file system corruption                              8/8      6/8       8/8
misconfiguration (2 hours ago)                            8/8      6/8       8/8
files inside the guest being encrypted                    8/8      6/8       8/8
a record deleted 30 days ago                              0/8      6/8       6/8
loss of the storage device                                0/8      6/8       6/8
loss of the host machine                                  0/8      6/8       6/8
guest deleted from the management interface               0/8      6/8       6/8
total                                                   24/56    42/56     48/56

snapshot works in 3/7 scenarios, and when it works it brings back 8/8 items
backup works in 7/7 scenarios, every time with 2 items out of scope: local secrets, in-memory session state
the two together: 48/56; unrecoverable 8 items
data-loss window: snapshot 1 hour, backup 24 hours
```

The two columns are incomplete in two different ways. The snapshot is **complete where it works**,
and zero where it does not: it brings back eight of eight items in three scenarios, none in four.
Three of the zeros come from the same cause — the snapshot sits **right next to** the thing that
is lost: when the storage device, the host, or the guest object goes, the snapshots go with it.
The fourth has a different cause; the thirty-day-old record is outside the seven-day retention.

The backup is **incomplete everywhere**: it works in all seven and brings back six items every time.
The two items it misses are written by name — local secrets and in-memory session state — and they
are out of scope regardless of scenario. **The snapshot's shortfall depends on the condition; the
backup's depends on scope.**

The bottom rows give the total: snapshot 24/56, backup 42/56, 48/56 when the better of the two is
picked in every scenario. The two methods do not replace each other, they fill each other's gap —
but the remaining eight items come in no arrangement, and all of them are the out-of-scope items in
the four scenarios where the snapshot does not work. On top of this comes the data-loss window: one
hour for the snapshot, twenty-four hours for the backup.

#### The Point You Can Return to When the Chain Breaks

The backup is not a single file either. Taking a full copy every day is the most expensive
arrangement; incremental and differential arrangements lower the cost, and in exchange tie the
points to one another.

```js
// backupchain.mjs — the point restorable when full, incremental, and differential chains break (MODEL).
// 28-day window; full backup on day 0, one point every day after. Sizes are fictional.
const DAYS = 28, FULL_GIB = 40, DELTA_GIB = 1.9, CORRUPT = 10;

// files required to open point d (day numbers)
const REQUIRED = {
  full: (d) => [d],
  incremental: (d) => Array.from({ length: d + 1 }, (_, i) => i),
  differential: (d) => (d === 0 ? [0] : [0, d]),
};
const SIZE = {
  full: () => DAYS * FULL_GIB,
  incremental: () => FULL_GIB + (DAYS - 1) * DELTA_GIB,
  differential: () => FULL_GIB + DELTA_GIB * ((DAYS - 1) * DAYS) / 2,
};
const lost = (type, corrupt) => Array.from({ length: DAYS }, (_, d) => d)
  .filter((d) => REQUIRED[type](d).includes(corrupt));

const s = (x, n) => String(x).padStart(n);
console.log(`${DAYS} days, full backup on day 0; full ${FULL_GIB} GiB, daily delta ${DELTA_GIB} GiB`);
console.log(`\n${'type'.padEnd(13)}${s('files', 6)}${s('files per point', 20)}`
  + `${s(`day ${CORRUPT} corrupt: lost`, 22)}${s('newest restorable', 21)}${s('storage GiB', 14)}`);
for (const type of Object.keys(REQUIRED)) {
  const n = Array.from({ length: DAYS }, (_, d) => REQUIRED[type](d).length);
  const k = lost(type, CORRUPT);
  const sound = Array.from({ length: DAYS }, (_, d) => d).filter((d) => !k.includes(d));
  console.log(`${type.padEnd(13)}${s(DAYS, 6)}${s(n[0] === n[DAYS - 1] ? n[0] : `${Math.min(...n)}-${Math.max(...n)}`, 20)}`
    + `${s(`${k.length} points`, 22)}${s(`day ${Math.max(...sound)}`, 21)}${s(SIZE[type]().toFixed(1), 14)}`);
}
console.log(`\nif the full backup (day 0) is corrupted, points lost: `
  + Object.keys(REQUIRED).map((t) => `${t} ${lost(t, 0).length}`).join(', '));
console.log(`differential requires ${(SIZE.differential() / SIZE.incremental()).toFixed(1)}x the storage of incremental`
  + ` and, when day ${CORRUPT} corrupts, recovers ${lost('incremental', CORRUPT).length - lost('differential', CORRUPT).length}`
  + ' more points');
```

```
28 days, full backup on day 0; full 40 GiB, daily delta 1.9 GiB

type          files     files per point  day 10 corrupt: lost    newest restorable   storage GiB
full             28                   1              1 points               day 27        1120.0
incremental      28                1-28             18 points                day 9          91.3
differential     28                 1-2              1 points               day 27         758.2

if the full backup (day 0) is corrupted, points lost: full 1, incremental 28, differential 28
differential requires 8.3x the storage of incremental and, when day 10 corrupts, recovers 17 more points
```
All three types have the same number of points: twenty-eight. The difference is how tied the points
are to one another. In the full arrangement, every point is one file, and one file's corruption
takes only that point. In the incremental arrangement, opening day 27 requires all twenty-eight of
the twenty-eight files; when the single file on day 10 corrupts, eighteen points go at once and the
newest point restorable drops to day 9. In the differential arrangement, every point is two files,
and day 10 corrupting takes only day 10.

The storage column writes the trade-off: 1,120, 91.3, and 758.2 GiB. The incremental arrangement is
the cheapest and the most fragile; the differential arrangement recovers seventeen more points in
the same corruption, at 8.3 times the storage. **The backup type looks like a storage decision;
measured, it turns out to be a dependency decision.** All three share a weakness in the last row: if
the full backup itself is corrupted, all twenty-eight of the twenty-eight points go at once in both
the incremental and differential arrangements.

#### Where the Copy Sits

Chain dependency has been measured; **where** the copy sits is a separate number. The same scenarios
now run over a set of copies.

```js
// copies.mjs — copy distribution: how many copies survive in each scenario (MODEL).
// [name, location, online?, deletable with the same management identity?, recovery minutes]
const COPIES = [
  ['host snapshot', 'host', true, true, 8],
  ['backup on the branch server', 'branch', true, true, 45],
  ['backup in the central object store', 'central', true, true, 210],
  ['offline copy at central', 'central', false, false, 720],
];
const SCENARIOS = [
  ['loss of the host machine', (k) => k[1] === 'host'],
  ['loss of the branch facility', (k) => k[1] === 'host' || k[1] === 'branch'],
  ['loss of the central object store', (k) => k[1] === 'central' && k[2]],
  ['compromise of the management identity', (k) => k[3]],
  ['files inside the guest being encrypted', () => false],
];
const remaining = (list, lost) => list.filter((k) => !lost(k));
const s = (x, n) => String(x).padStart(n);
console.log(`${'scenario'.padEnd(40)}${s('remaining', 11)}   fastest recovery`);
let worst = COPIES.length;
for (const [ad, lost] of SCENARIOS) {
  const k = remaining(COPIES, lost);
  const fastest = k.length ? k.reduce((a, b) => (b[4] < a[4] ? b : a)) : null;
  worst = Math.min(worst, k.length);
  console.log(`${ad.padEnd(40)}${s(`${k.length}/${COPIES.length}`, 11)}   `
    + (fastest ? `${fastest[0]} (${fastest[4]} min)` : 'none'));
}
const locations = new Set(COPIES.map((k) => k[1])).size;
console.log(`\n${COPIES.length} copies, ${locations} locations; remaining in the worst scenario ${worst}`);
const online = COPIES.filter((k) => k[2]);
const after = Math.min(...SCENARIOS.map(([, y]) => remaining(online, y).length));
console.log(`once the offline copy is removed, copies ${COPIES.length} -> ${online.length}, `
  + `remaining in the worst scenario ${worst} -> ${after}`);
```

```
scenario                                  remaining   fastest recovery
loss of the host machine                        3/4   backup on the branch server (45 min)
loss of the branch facility                     2/4   backup in the central object store (210 min)
loss of the central object store                3/4   host snapshot (8 min)
compromise of the management identity           1/4   offline copy at central (720 min)
files inside the guest being encrypted          4/4   host snapshot (8 min)

4 copies, 3 locations; remaining in the worst scenario 1
once the offline copy is removed, copies 4 -> 3, remaining in the worst scenario 1 -> 0
```

In three physical scenarios, at least two copies stay standing. The fourth row is different from the
others: compromising the management identity destroys no hardware, yet it still takes three of the
four copies at once — those three are the online copies the same identity can delete, and location
diversity means nothing here.

The last row is the summary of the measure: when the offline copy is removed, the copy count drops
from four to three, but the copies remaining in the worst scenario drop **from one to zero**.
**Resilience is not the number of copies, but the number of items that can destroy those copies at
the same time.**

#### Recovery Testing

All of these numbers rest on one assumption: the backup file is readable. Testing this assumption is
called **recovery testing**; it is the same restore drill from the Servers, Networking and Hardening
course, and it was measured there. With this course's measure, there is only one thing to say: when
recovery testing is not done, the item required to cross the recovery boundary is **one** — one
corrupt file, one out-of-scope item, or one unreadable key — and when that item is in place, the
accessible recovery points are zero.

#### Summary

- In seven loss scenarios, the snapshot works in 3 and brings back 8/8 items when it does; the
  backup works in 7 and brings back 6/8 every time.
- The snapshot's shortfall depends on the condition — it sits right next to the thing that is lost;
  the backup's depends on scope, and two items stay outside it regardless of scenario.
- The two methods together recover 48 of 56 items; the remaining 8 items come in no arrangement. The
  data-loss window is 1 hour for the snapshot, 24 for the backup.
- In the twenty-eight-point chain, one file corrupting takes 1 point in the full arrangement, 18 in
  incremental, 1 in differential; storage is 1,120, 91.3, and 758.2 GiB respectively. If the full
  backup itself is corrupted, all 28 of the 28 points go at once in both incremental and
  differential.
- In the four-copy arrangement, the worst scenario is compromise of the management identity, taking
  3 copies at once; once the offline copy is removed, the copies remaining in the worst scenario
  drop from 1 to 0.
- When recovery testing is not done, the item required to cross the recovery boundary is one, and
  when that item is in place, the accessible recovery points are zero.

#### Course Wrap-Up

| Lesson | Boundary Name | Items Required to Cross | Items Reached if It Falls |
|---|---|---|---|
| Computer Hardware Components | 8 components, 43 items | remote 1–3, physical 0 | 3–43; firmware alone 43 |
| Operating System Families | admin boundary, 14 requests | 1, 2, 3 by family | 13, 9, 5 requests |
| Installation and Configuration | default boundary, 20 items | 1 risky item | 17 items; 5 after hardening |
| File Permissions and Ownership | permission boundary, 70 requests | 1 at ownership | all of the object's permission decisions; 6 deviations |
| Graphical Interface and Command Line | audit trail boundary, 21 records | 1 deletable item | 10 records invisible; 3 jobs untraced |
| Troubleshooting Method | symptom–cause boundary, 16 possibilities | 3.50 steps; 5.81 in the worst order | 14 scenarios to a single cause; 2 inseparable |
| Hypervisor Types | hypervisor boundary, 19 and 26 items | 1 to the neighbor; 2 and 3 to the hardware | 15/19 and 22/26 |
| Guest and Host System | sharing boundary, 11 items | 1 in 4 paths, 2 in 10 paths, 3 in 2 paths | 6 of 16 paths cannot be closed |
| Virtual Machine Management | clone and snapshot boundary, 7 secrets | 1 for a shared secret | 24 machines; 17 vulnerabilities on rollback |
| Comparison with Containers | isolation boundary, 52 items | guest 2, compartment 1, nested 3 | 39/52, 52/52, nested 9/52 |
| Backup and Resilience | recovery boundary, 56 items | 1 for a corrupt file | 8 items unrecoverable; 18 points |

The third column reads the course's rule the same way across all eleven rows: **isolation is not an
adjective, it is the number of items that must be wrong at the same time to cross the boundary; if
that number is one, there is no boundary.** All eleven of the eleven rows have at least one "1." A
risky default item, an ownership record, a deletable audit record, a shared secret, a corrupt backup
file — each is sufficient on its own, and none of them uses an escape path.

The places where the number is greater than one come from three sources: **layer stacked on layer**
(two and three items to the hardware, three in the nested arrangement), **a verification chain**
(signed boot raises the required item from one to four), and **split privilege** (the admin boundary
is one item in one family, three in the other). All three have their cost written down — four extra
patch items, three links re-signed on every update, nine requests unreachable even as an admin. **A
boundary whose cost is not written down has not been measured.**

The fourth column shows where the boundary is not enough: six sharing paths that cannot be closed,
eight items that cannot be recovered, two causes that cannot be separated, and one installation item
that never closes. None of them close with a better configuration.

All of these numbers were measured on a single machine and the guest systems on it. But the branch
server does not stand alone: meter endpoints and field teams' laptops connect to it, and it in turn
connects to the center. Each of these connections is a path that runs **outside** the boundary the
operating system draws; neither permission bits, nor the hypervisor, nor the compartment sees it.
**M24/K03 Network Security** begins with this question: how many paths does each connection between
two machines open, and who counts those paths.

