Lesson 01 / 16
Hardening Principles
The course sets its unit of measure: the 84 foothold points a default install leaves across four machine classes are counted, clearing unnecessary items closes 40 of them, 44 are only written to a baseline, and the legitimate-work cost of closing them comes out to 41 manual steps.
Published Updated
In this lesson
- Identifies automatically run item types as foothold points and separates them from installed packages
- Counts the closed point and the merely recorded point separately in the default install inventory
- Finds the item that diverges by machine class and calculates the legitimate-work cost of removing it
Contents
The previous course tied identity to a root: who a key belongs to now rests on a named chain. Even if the entire chain is built correctly, that root sits on top of a machine, and if the machine is compromised the chain keeps working in the attacker’s hands.
This course starts from there, and it opens with an assumption different from the previous four courses: code has already run once on the endpoint. What gets counted is no longer whether entry is blocked, but from how many points persistence is possible after entry. Code that has run once is a one-time event if it does not run again by itself when the machine restarts or the user logs on; for it to run again, an item has to be added to the machine’s own automatic-run mechanism. Every item in that mechanism is a foothold point, and it can be counted. This lesson sets the course’s unit of measure.
Foothold Point and Installed Item
An inventory item is a foothold point if nobody has to do anything for it to run. Five types meet this definition: a boot entry, a service definition, a scheduled task, a logon script, and a configuration hook. What they share is not their form but their function — each carries a definition, and whoever can write that definition makes their own chosen thing run the next time the machine boots or the next time a session opens.
An installed package does not meet this definition. A binary sitting on disk does not run by itself; for it to run, either a user has to invoke it or one of the five types above has to name it. The distinction narrows and sharpens the measurement: not the whole inventory, only the automatically run part of it is counted. In this course, how a foothold point is defined — which file gets what written to it — is never written down anywhere; what is counted is that the type exists, and how many there are.
The measurement is set up across the four machine classes of the regional measurement network: field team laptops, branch endpoints, meter collector machines, and central processing servers.
- SH1. The thirty-three-item inventory and the four classes are a model; no real machine is scanned, no real removal is performed.
- SH2. Only the five automatic types count toward the foothold-point tally; installed packages are counted separately.
- SH3. The “required” column is treated as derived from a job description and is fixed in the model. In reality this column is filled in by an inventory pass and changes as roles change.
- SH4. Removal cost is the number of legitimate tasks that fall to a manual step when the item is removed; it was written in by hand.
- SH5. Removal is total: an item either exists or it does not. The distinction between disabling and removing is not modeled; the latter also deletes the definition, the former leaves it in place.
- SH6. The field for who can write the definition carries a single value and shows the broadest privilege.
// harden/inventory.mjs — the item inventory a default install leaves behind IS A MODEL. // No real machine is scanned, no real removal is performed; the numbers derive from this file. export const CLASS = ['field-laptop', 'branch-endpoint', 'collector', 'processing-server']; // Five automatically run item types count as a foothold point; 'package' does not. export const POINT_TYPE = ['boot', 'service', 'task', 'logon', 'hook']; // row: name | type | installed mask | required mask | who can write the definition | removal cost // mask is four characters, in CLASS order; 'k' present, '.' absent. // cost: the number of legitimate tasks that fall to a manual step when the item is removed. export const ITEM = ` boot-manager-entry boot kkkk kkkk administrator 0 recovery-boot-entry boot kkkk .... administrator 2 network-boot boot .kkk ..k. administrator 0 remote-management-service service kkkk .kkk administrator 0 print-spooler service kk.. .k.. administrator 0 discovery-broadcast service kkkk .... administrator 1 remote-desktop service kk.. .k.. administrator 0 metrics-collector-agent service kkkk kkkk administrator 0 log-forwarder-agent service kkkk kkkk administrator 0 patch-agent service kkkk kkkk administrator 0 helper-tool-service service kkkk .... local-administrator 0 cloud-sync-service service k... .... user 2 meter-reading-service service ..k. ..k. administrator 0 job-queue-service service ...k ...k administrator 0 update-check task kkkk kkkk administrator 0 disk-maintenance task kkkk kkkk administrator 0 telemetry-upload task kkkk .... administrator 0 helper-tool-update task kkkk .... local-administrator 1 user-task-folder task kk.. .... user 3 backup-trigger task ..kk ..kk administrator 0 logon-script logon kk.. kk.. administrator 0 user-startup-folder logon kk.. .... user 3 shell-config-file logon kkkk k.k. user 2 package-manager-hook hook kkkk ...k administrator 1 driver-load-hook hook kkkk kk.. administrator 0 auth-plugin hook kkkk .kkk administrator 0 shell-plugin-directory hook kk.. .... user 2 document-editor package kk.. kk.. - 0 archive-tool package kkkk kkkk - 0 script-runtime package kkkk ..kk - 1 database-client package ..kk ..kk - 0 diagnostic-toolkit package kkkk .... - 3 field-measurement-app package k... k... - 0` .trim().split('\n').map((r) => { const [name, type, installed, required, writer, cost] = r.trim().split(/\s+/); return { name, type, installed, required, writer, cost: Number(cost), point: POINT_TYPE.includes(type) }; }); export const isInstalled = (k, i) => k.installed[i] === 'k'; export const isRequired = (k, i) => k.required[i] === 'k'; // Items installed in a class, not required, and therefore removable. export const surplus = (i) => ITEM.filter((k) => isInstalled(k, i) && !isRequired(k, i)); export const points = (i) => ITEM.filter((k) => k.point && isInstalled(k, i)); export const remaining = (i) => points(i).filter((k) => isRequired(k, i)); export const measure = (i) => ({ installed: ITEM.filter((k) => isInstalled(k, i)).length, point: points(i).length, closed: surplus(i).filter((k) => k.point).length, remaining: remaining(i).length, cost: surplus(i).filter((k) => k.point).reduce((a, k) => a + k.cost, 0), });
Counting the Inventory
// harden/count.mjs — foothold points, closed points, and removal cost across four machine classes import { CLASS, POINT_TYPE, ITEM, measure, isInstalled, isRequired, points, remaining, surplus } from './inventory.mjs'; const row = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join('').trimEnd()); const total = (f) => CLASS.reduce((a, _, i) => a + f(i), 0); console.log(`${ITEM.length} inventory items; ${ITEM.filter((k) => k.point).length} of them ` + `automatically run (${POINT_TYPE.join(', ')})`); const A = [-20, 11, 7, 9, 11, 8]; console.log('\n1. count per class'); row(A, 'class', 'installed', 'point', 'closed', 'remaining', 'cost'); for (const [i, s] of CLASS.entries()) { const o = measure(i); row(A, s, o.installed, o.point, o.closed, o.remaining, o.cost); } row(A, 'total', total((i) => measure(i).installed), total((i) => measure(i).point), total((i) => measure(i).closed), total((i) => measure(i).remaining), total((i) => measure(i).cost)); const B = [-8, 7, 9, 11, 8]; console.log('\n2. count per type, sum of four classes'); row(B, 'type', 'point', 'closed', 'remaining', 'cost'); for (const t of POINT_TYPE) row(B, t, total((i) => points(i).filter((k) => k.type === t).length), total((i) => surplus(i).filter((k) => k.point && k.type === t).length), total((i) => remaining(i).filter((k) => k.type === t).length), total((i) => surplus(i).filter((k) => k.point && k.type === t).reduce((a, k) => a + k.cost, 0))); console.log('\n3. point diverging by class (required in one class, surplus in another)'); const diverging = ITEM.filter((k) => k.point && CLASS.some((_, i) => isInstalled(k, i) && isRequired(k, i)) && CLASS.some((_, i) => isInstalled(k, i) && !isRequired(k, i))); for (const k of diverging) row([-30, -9, -6], k.name, k.type, k.required); const nonAdmin = (i) => points(i).filter((k) => k.writer !== 'administrator'); console.log(`\npoints needing a separate decision ${diverging.length} items, ` + `${total((i) => diverging.filter((k) => isInstalled(k, i)).length)} decisions`); console.log(`points definable without an administrator ${total((i) => nonAdmin(i).length)} decisions, ` + `closed ${total((i) => nonAdmin(i).filter((k) => !isRequired(k, i)).length)}, ` + `remaining ${total((i) => nonAdmin(i).filter((k) => isRequired(k, i)).length)}`); console.log(`removable package ${total((i) => surplus(i).filter((k) => !k.point).length)} decisions, ` + `cost ${total((i) => surplus(i).filter((k) => !k.point).reduce((a, k) => a + k.cost, 0))}`); console.log(`total point decisions ${total((i) => measure(i).point)}: ` + `closed ${total((i) => measure(i).closed)}, ` + `written to baseline only ${total((i) => measure(i).remaining)}`);
33 inventory items; 27 of them automatically run (boot, service, task, logon, hook) 1. count per class class installed point closed remaining cost field-laptop 28 23 14 9 15 branch-endpoint 27 23 11 12 15 collector 23 19 7 12 5 processing-server 23 19 8 11 6 total 101 84 40 44 41 2. count per type, sum of four classes type point closed remaining cost boot 11 6 5 8 service 31 12 19 6 task 20 10 10 10 logon 8 4 4 10 hook 14 8 6 7 3. point diverging by class (required in one class, surplus in another) network-boot boot ..k. remote-management-service service .kkk print-spooler service .k.. remote-desktop service .k.. shell-config-file logon k.k. package-manager-hook hook ...k driver-load-hook hook kk.. auth-plugin hook .kkk points needing a separate decision 8 items, 27 decisions points definable without an administrator 19 decisions, closed 17, remaining 2 removable package 6 decisions, cost 14 total point decisions 84: closed 40, written to baseline only 44
The Closed Point and the Recorded Point
The total row gives the course’s two columns at once. Across four classes there are 84 foothold-point decisions; clearing unnecessary items closes 40 of them and leaves 44 open. The only thing that can be done for the 44 open points is to write their definitions to a baseline and compare later deviations against that line. This is not a closure; a recorded point still runs when it is used, it is only noticed when the comparison is made.
Keeping the two columns separate is this course’s rule. The sentence “it was hardened” does not by itself say which column something was written to. A closed point cannot be used again; a recorded point can be used, and it is noticed only when someone reads the record.
The model treats an item as either present or absent (SH5), and that collapses two separate operations into a single number in reality. Disabling a service leaves its definition in place and only flips the run flag off; removing it also deletes the definition. For counting purposes the difference is this: a disabled item does not belong in the closed column, it belongs in the recorded column, because whoever can write the definition can flip the flag back on. When the difference is measured, the number 40 drops in most inventories — items that cannot be removed but can be disabled get mistaken for closed. This lesson’s count assumes removal actually happened; if it did not happen at an endpoint, that item moves to the second column.
The class rows show a reversed distribution. The field laptop carries the most items (28 installed, 23 points) and is also the class cleaned the most: 14 points close, 9 remain. On the collector machine, only 7 of 19 points close, because whatever is installed is nearly always required. So cleanup pays off the most on a machine built for many purposes with no single job, and on a machine with a single job the room to gain is already narrow.
The type table shows where the remaining side concentrates. Of thirty-one service definitions, only 12 close; 19 services remain, and that is the largest piece of the 44 remaining points. The metrics collector, the log forwarder, the patch agent, and the auth plugin cannot be removed, because the machine’s manageability depends on them. Hardening’s limit shows up here: the items that make an endpoint manageable are also the ways to persist on that endpoint.
Who Can Write It Matters More Than How Many There Are
A separate row flags 19 of the 84 points: points definable without administrator privilege. The user task folder, the user startup folder, the shell plugin directory, the shell config file, the cloud sync service, and two helper-tool items that ship with local-administrator privilege belong to this group. Cleanup closes 17 of them, leaving 2.
The smallness of the number does not shrink the importance of the result. A foothold point that requires administrator privilege is an item usable by someone who has already obtained that privilege; a point that does not require administrator privilege is open to code running in an ordinary user session. The difference between the two items is not in the count, it is in which step has to happen before it becomes usable. The two remaining items — the shell config file on the field laptop and on the collector machine — cannot be removed, because in both classes the work itself runs through the shell.
Items That Diverge by Class
The third block separates eight items: each is required in one class, surplus in another. The remote management service is mandatory in three classes and unnecessary on the field laptop. The print spooler is only required at the branch endpoint. Network boot only on the collector machine. The package manager hook only on the processing server.
These eight items produce 27 decisions, and a single corporate “removal list” cannot resolve them. If the list removes all eight items, management access closes in three classes; if it removes none of them, an unnecessary point stays open on the field laptop. The correct approach is to decide per class, and that is exactly the maintenance cost: four lists, 84 decisions, and 8 boundary items that have to be revisited with every role change.
The Cost of Closing
The cost column shows 41 manual steps, and its distribution is also reversed: the two classes with the most points closed pay the highest cost (15 and 15), the two classes with the least closed pay the lowest (5 and 6). The type table shows a sharper ratio: logon scripts run 10 cost per 4 closed points, service definitions run 6 cost per 12 closed points. Closing items users install themselves costs roughly five times as much as closing service definitions — because the user installed that item to get a job done, and once the item closes, the job falls to management.
The cost becomes concrete once items are read by name. The recovery boot entry is surplus in all four classes, and removing it requires portable media for on-site repair — 8 manual steps across the four classes. The user task folder and the startup folder are how users set up their own scheduling; once closed, every request turns into a management task.
The package side is counted separately: 6 removal decisions and 14 manual steps. The diagnostic toolkit is surplus in all four classes, but troubleshooting is done with it; removing it means a reinstall on every incident. This package is not a foothold point — it does not run by itself — but as long as it sits on disk it serves a point that could call it. The same item will be counted again in the application allowlist lesson.
Summary
- A foothold point is an item that requires nobody to do anything for it to run: a boot entry, a service definition, a scheduled task, a logon script, and a configuration hook. An installed package does not belong to this set and is counted separately.
- Of 101 installed-item decisions across four machine classes, 84 are foothold points; clearing unnecessary items closes 40 of them and leaves 44 open. The only thing that can be done for what stays open is to write it to a baseline — that is not a closure, it is making it visible.
- Service definitions are the largest piece of the remaining side: 19 of 31 points cannot be removed, because the items that make an endpoint manageable are also the ways to persist there.
- Of 19 points definable without administrator privilege, 17 close and 2 remain. Who can write a point carries more decision weight than how many there are.
- Eight items diverge by class and produce 27 decisions; a single corporate list resolves them either as unnecessary work in one class or as incomplete closure in another.
- The cost of closing is 41 manual steps and is highest in the classes cleaned the most; the cost per closed point in logon scripts is roughly five times that of service definitions.
Next Step
The remaining 44 points cannot be closed, because the work requires them. But the components carrying these points age where they stand: once a flaw is announced in the binary behind a service definition, that point, because it cannot be removed, stays usable until a patch is applied. The next lesson’s measure is therefore the window: the days between announcement and application, the number of points that stay open in that window, and the product of the two. An emergency patch shortens the window, but how many test steps it skips and how many outages it produces are counted separately. What a patch actually closes is also a separate question: a patch does not remove a foothold point, it only closes one known path to reaching that point.
To keep your progress and take notes, Log in
My notes
Log in to take notes.