Lesson 13 / 15
Security Roles
Splitting roles by responsibility item: of 9 item types and 3 duties across 27 cells, 15 have a single owner, 7 are unowned, 3 have two owners, and 2 sit past the boundary; the unowned item forces 12 paths to close from a more costly step, 2 paths never close, 15 closures require two approvals, and writing the boundary drops stopped legitimate requests from 2,321 to 2,135.
Published Updated
In this lesson
- Splits security work into responsibility items rather than roles: builds an item type x duty cell
- Counts unowned and two-owner cells separately and measures each one's path-level counterpart
- Computes how many paths writing the role boundary closes and the drop in legitimate-work cost
Contents
The previous lesson separated three working modes over the same set of paths and counted the information gap that purple work closed. That measurement left one question open: once the paths are known, who cuts which step. “Blue” and “red” are names for a working mode; what does the work in an organization is not a mode, it is a responsibility item with a name written down.
This lesson takes the paths as already known and measures ownership. Security work gathers into five roles: security operations reads the log and runs the incident; security testing looks for the path and confirms it is closed; security architecture sets the design decision and the default; governance carries the policy, the risk, and the third-party contract; systems and network operations actually applies the change. Two numbers are asked: in the same set of paths, how many items sit on no role at all, and how many sit on two roles at once.
Item, Duty, and Owner
Counting roles by name does not give a measure of responsibility; the same name covers different work in two organizations. The measurable unit is the responsibility item: the thing that has to change for a step to be cut.
- FR9. The set of paths comes from the previous lesson’s graph: 20 steps, 30 paths. Each step belongs to an item type; there are 9 item types.
- FR10. Each item type has three duties: track (knowing the item exists and keeping it listed), close (applying the change), sustain (verifying it stays closed). The matrix is 9 x 3 = 27 cells.
- FR11. A cell’s owner can be 0, 1, or 2 roles.
- FR12. A cell marked
outsidesits past the boundary of shared responsibility. Where that boundary runs is not this lesson’s question; it is an input, not an internal gap. - FR13. A path closes when one step on it is cut. A step can be cut only if its item has an internal owner in both its track and close cells: an item on no one’s list never comes up, and even if it does, no one applies it.
- FR14. The defense picks the cuttable step carrying the fewest legitimate requests. A path with no cuttable step stays open.
- FR15. If a close cell has two owners, that closure earns two separate approval steps.
- FR16. Writing the role boundary means assigning one owner to every unowned cell and leaving
a single owner in every two-owner cell;
outsidecells do not change.
// role/item.mjs — the same access graph, this time labeled with a RESPONSIBILITY ITEM (MODEL). // Each step belongs to an item type; each item type has three duties. Numbers are fictional. export const EDGE = ` field-terminal collector 420 field-hardware field-terminal branch-switch 35 inventory-record subscriber-portal validator 900 application-control subscriber-portal identity-directory 760 identity-authority supplier-endpoint management-network 18 third-party-clause supplier-endpoint backup-endpoint 6 third-party-clause branch-switch management-network 24 network-rule branch-switch collector 110 network-rule collector validator 380 application-control collector reading-store 540 data-classification validator reading-store 610 data-classification validator billing 290 application-control identity-directory management-network 44 identity-authority identity-directory work-order 150 identity-authority management-network collector 60 network-rule management-network backup-endpoint 12 backup-route management-network identity-directory 38 identity-authority backup-endpoint reading-store 9 backup-route work-order validator 130 application-control work-order billing 95 patch-matrix` .trim().split("\n").map((r) => { const [a, b, m, k] = r.trim().split(/\s+/); return { a, b, legit: Number(m), item: k }; }); export const ENTRY = ["field-terminal", "subscriber-portal", "supplier-endpoint"]; export const ASSET = ["reading-store", "billing"]; export const DUTY = ["track", "close", "sustain"]; export const ROLE = ["operations", "testing", "architecture", "governance", "systems"]; // Responsibility cell: item type x duty -> list of owning roles. "outside" = the other side of // the shared-responsibility boundary; measured in the previous course, not an internal gap. export const OWNER = { "network-rule": { track: ["systems"], close: ["systems", "architecture"], sustain: ["operations"] }, "identity-authority": { track: ["operations"], close: ["systems"], sustain: ["operations", "governance"] }, "application-control": { track: ["testing"], close: ["architecture"], sustain: [] }, "inventory-record": { track: [], close: ["systems"], sustain: [] }, "data-classification": { track: ["governance"], close: ["architecture", "governance"], sustain: ["operations"] }, "backup-route": { track: [], close: ["systems"], sustain: [] }, "patch-matrix": { track: ["operations"], close: ["systems"], sustain: ["operations"] }, "field-hardware": { track: [], close: ["systems"], sustain: [] }, "third-party-clause": { track: ["governance"], close: ["outside"], sustain: ["outside"] }, }; export function paths() { const out = {}; for (const k of EDGE) (out[k.a] ??= []).push(k.b); const all = []; const walk = (d, trail) => { if (ASSET.includes(d)) { all.push([...trail]); return; } for (const s of out[d] ?? []) if (!trail.includes(s)) walk(s, [...trail, s]); }; for (const g of ENTRY) walk(g, [g]); return all.map((trail) => ({ trail, edges: trail.slice(0, -1).map((_, i) => EDGE.find((k) => k.a === trail[i] && k.b === trail[i + 1])) })); }
Unowned Cells and Overlapping Cells
// role/measure.mjs — responsibility cell count, load per role, and the path-level result. import { EDGE, OWNER, DUTY, ROLE, paths } from "./item.mjs"; const P = paths(), ITEM = Object.keys(OWNER); 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 internal = (s) => s.filter((r) => r !== "outside"); const classify = (s) => (s[0] === "outside" ? "outside" : s.length === 0 ? "NONE" : s.length > 1 ? "BOTH" : "one"); console.log(`responsibility matrix MODEL: ${ITEM.length} item types x ${DUTY.length} duties ` + `= ${ITEM.length * DUTY.length} cells; ${ROLE.length} roles, ${EDGE.length} steps, ` + `${P.length} paths.`); const T = [-22, 12, 25, 23]; console.log("\n1. which cell has an owner (outside = the other side of the shared-responsibility boundary)"); write(T, "item type", ...DUTY); for (const k of ITEM) write(T, k, ...DUTY.map((g) => OWNER[k][g].join("+") || "-")); const count = (c) => ITEM.reduce((a, k) => a + DUTY.filter((g) => classify(OWNER[k][g]) === c).length, 0); console.log(`cell distribution: one owner ${count("one")}, unowned ${count("NONE")}, ` + `two owners ${count("BOTH")}, outside ${count("outside")}.`); // A step is cuttable only if its item has an internal owner in both its track and close cells. const cuttable = (e) => internal(OWNER[e.item].track).length > 0 && internal(OWNER[e.item].close).length > 0; const R = [-20, 10, 10]; console.log("\n2. load per role"); write(R, "role", "cells", "paths"); for (const r of ROLE) { const h = ITEM.reduce((a, k) => a + DUTY.filter((g) => OWNER[k][g].includes(r)).length, 0); const y = P.filter((p) => p.edges.some((e) => cuttable(e) && DUTY.some((g) => OWNER[e.item][g].includes(r)))).length; write(R, r, h, y); } // Writing the role boundary: one owner for every unowned cell, a single owner in every shared cell. const WRITTEN = Object.fromEntries(ITEM.map((k) => [k, Object.fromEntries(DUTY.map((g) => { const s = OWNER[k][g]; return [g, s[0] === "outside" ? s : s.length === 0 ? ["systems"] : [s[0]]]; }))])); const decisions = ITEM.reduce((a, k) => a + DUTY.filter((g) => ["NONE", "BOTH"].includes(classify(OWNER[k][g]))).length, 0); function run(M) { const fit = (e) => internal(M[e.item].track).length > 0 && internal(M[e.item].close).length > 0; const cut = new Set(); let closed = 0, approvals = 0, unverified = 0; for (const y of P) { const u = y.edges.filter(fit).sort((a, b) => a.legit - b.legit); if (u.length === 0) continue; closed++; cut.add(u[0]); if (internal(M[u[0].item].close).length > 1) approvals++; if (internal(M[u[0].item].sustain).length === 0) unverified++; } return { closed, open: P.length - closed, steps: cut.size, unverified, approvals, legit: [...cut].reduce((a, e) => a + e.legit, 0) }; } const CURRENT = run(OWNER), REVISED = run(WRITTEN); const W3 = [-32, 12, 12]; console.log(`\n3. path-level result (writing the role boundary takes ${decisions} cell decisions)`); write(W3, "measure", "current", "written"); for (const [ad, f] of [["closed path", (r) => r.closed], ["open path", (r) => r.open], ["step cut", (r) => r.steps], ["legit request stopped (day)", (r) => r.legit], ["closure needing two approvals", (r) => r.approvals], ["closed but unverified", (r) => r.unverified]]) write(W3, ad, f(CURRENT), f(REVISED)); const shifted = P.filter((y) => { const u = y.edges.filter(cuttable).sort((a, b) => a.legit - b.legit); return u.length > 0 && u[0] !== [...y.edges].sort((a, b) => a.legit - b.legit)[0]; }).length; const unownedItem = ITEM.filter((k) => !internal(OWNER[k].track).length || !internal(OWNER[k].close).length); console.log(`\npath closed from a more costly step because its cheapest step is not cuttable: ${shifted};`); console.log(`item types that cannot be cut: ${unownedItem.join(", ")}.`); const openPaths = P.filter((y) => y.edges.every((e) => !cuttable(e))); for (const y of openPaths) console.log(`path left open: ${y.trail.join(" > ")}`);
responsibility matrix MODEL: 9 item types x 3 duties = 27 cells; 5 roles, 20 steps, 30 paths. 1. which cell has an owner (outside = the other side of the shared-responsibility boundary) item type track close sustain network-rule systems systems+architecture operations identity-authority operations systems operations+governance application-control testing architecture - inventory-record - systems - data-classification governance architecture+governance operations backup-route - systems - patch-matrix operations systems operations field-hardware - systems - third-party-clause governance outside outside cell distribution: one owner 15, unowned 7, two owners 3, outside 2. 2. load per role role cells paths operations 6 26 testing 1 18 architecture 3 25 governance 4 22 systems 7 23 3. path-level result (writing the role boundary takes 10 cell decisions) measure current written closed path 28 30 open path 2 0 step cut 11 12 legit request stopped (day) 2321 2135 closure needing two approvals 15 0 closed but unverified 5 0 path closed from a more costly step because its cheapest step is not cuttable: 12; item types that cannot be cut: inventory-record, backup-route, field-hardware, third-party-clause. path left open: supplier-endpoint > management-network > backup-endpoint > reading-store path left open: supplier-endpoint > backup-endpoint > reading-store
The first table splits the 27 cells into four classes: 15 with a single owner, 7 unowned, 3 with two owners, 2 outside. Of the 25 internal cells, only 15 have exactly one owner; not just a tenth but a full 40% are either undefined or doubly defined.
The seven unowned cells are spread over four item types, and all of them are missing in the same
direction: inventory-record, backup-route, and field-hardware have empty track and
sustain cells while their close cells are filled. This means the organization knows how
to change these three items but has given no one the job of tracking them: who keeps the
inventory record, who knows the backup route exists, who lists the field hardware has no written
answer. Being able to change an item is not the same as managing it — the item that will change
has to be on the list first. application-control also has an empty sustain cell: the control
gets built, but no role is written down to notice if it is removed.
The three two-owner cells are a different kind of gap. The network-rule close cell sits with
systems and architecture, the data-classification close cell with architecture and governance,
the identity-authority sustain cell with operations and governance. These cells are not
empty, but every change to them passes through two separate approvals.
The second table gives the load per role, and its two columns do not track each other. systems
is the busiest role with 7 cells, but it is not part of the security team — it is the hand that
actually cuts the step. testing owns only 1 cell; in exchange, 18 paths’ closure depends
on the item it tracks. Cell count does not measure how much work a role does, nor how many paths
run through it either — the two columns answer different questions. This is also why the same
path is counted under more than one role: closing a path usually needs an owner in all three
duties at once.
The Path-Level Cost of Being Unowned
The third table brings the cell count down to the set of paths. In the current distribution, 28 paths close and 2 stay open. The two open paths are the output’s last lines, and both run from the third-party endpoint through the backup endpoint to the reading store. Every step on them is either past the boundary or on no role’s list — a path stays open only if every step on it is unowned, and that is exactly what has happened here.
The real cost is not in the open paths — it is in how the closed paths close. The output’s third-from-last line writes this: 12 paths cannot close from their cheapest step, because that step’s item is on no role’s list. The defense shifts to the next cuttable step, and that step carries more legitimate work. Stopped legitimate requests are 2,321 a day in the current distribution and 2,135 once the role boundary is written. The 186-request gap between them buys no security at all; it is purely the cost of not being able to choose the step to cut.
The closure needing two approvals row gives the cost of overlap: 15 closures earn two
separate approval steps. Half of the thirty paths close through an item both roles claim, and
each one requires two decisions. Overlap does not leave a path open; it slows closure down and
doubles every change.
The closed but unverified row gives the third item: 5 paths close, but no role verifies
that they stay closed. These paths count as closed in the table. If they reopen at the next
configuration change, no written owner is there to notice — an empty sustain cell means closure
is unmeasured.
The Cost of Writing the Boundary
The written column is the result of ten cell decisions: seven unowned cells each get an owner,
three overlapping cells each keep a single owner. Closed paths rise from 28 to 30, open paths
drop to zero, stopped legitimate requests fall from 2,321 to 2,135, closures needing two
approvals fall from 15 to 0, and unverified paths fall from 5 to 0.
These ten decisions are not a technical effort. No new control is built, no new step is cut, nothing in the graph changes; only ten cells get a name written into them. In exchange, two more paths close and 186 fewer legitimate requests are stopped a day.
The measurement also carries a warning. Every unowned cell sits in the track and sustain columns, none in the close column. Organizations find it easy to name the hand that does the work; naming the hand that keeps the list and confirms the result stays behind is the part they skip. The most commonly skipped cell in a responsibility matrix is not the one where the work happens — it is the one that notices the work is needed and checks the outcome.
Summary
- In the 27-cell responsibility matrix, 15 cells have a single owner; 7 cells are unowned, 3 have two owners, and 2 sit past the shared-responsibility boundary. 40% of the 25 internal cells are either undefined or doubly defined.
- All seven unowned cells sit in the track and sustain columns. The hand that changes the item is written; the hand that keeps it listed and verifies it stays closed is not.
- Cell count per role does not track path count: testing owns a single cell, yet 18 paths’ closure depends on the item it tracks.
- In the current distribution 28 paths close and 2 stay open; 12 of the closed paths cannot close from their cheapest step, so they close from a more costly one and stop 186 more legitimate requests a day.
- 15 closures earn a second approval step, and 5 paths count as closed without their staying closed ever being verified. Writing the boundary is ten cell decisions, and it turns these three numbers into 30, 0, and 0 respectively.
Next Step
Writing the role boundary closed ten cells and turned two more paths closable. Still, two cells in
the matrix stayed outside from start to finish, and this lesson never touched them: the
third-party clause’s close and sustain cells take no internal owner.
The reason for those two cells is not technical. Cutting a step, testing a path, adding an item to the list — all of it depends on someone’s authority to act on that system, and that authority is granted not by a table but by a document. The next lesson builds that authority as a countable item: how many assets sit within the written scope, how many out-of-scope assets sit on the same network, and how many items scope ambiguity leaves open to dispute.
To keep your progress and take notes, Log in
My notes
Log in to take notes.