Skip to content
academia.sh

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 outside sits 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; outside cells 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.

Start typing to search.

↑↓ ↵ Esc navigate · open · close