---
title: 'Security Architecture'
source: 'https://academia.sh/en/courses/architecture-governance/security-architecture'
course: 'Quality Attributes and Governance'
language: en
updated: '2026-08-23T07:01:05+00:00'
license: 'CC BY-SA 4.0'
---

# Security Architecture

Counting trust boundaries as a graph: how many boundaries there are, which data crosses which boundary, checks per boundary and unchecked crossings, how many paths removing a boundary opens, and what a rule catches, misses, and false-alarms on in a known set of security incidents.

In the previous lesson security was a single score — the average of three leaves' share
relative to their threshold — and it did not say what changed when it rose. A security decision
lands not on a number but on a place: the point where data crosses from one zone into another.

How authentication and authorization get implemented was measured in the Authentication and
Authorization course, secret value management in the Server Security and Going to Production
course; neither is repeated here. The question here sits at the architectural level: how many
trust boundaries there are, which data crosses which boundary, how many checks stand at each
boundary, which crossings pass through no check at all. And the course's question continues — can
this graph be turned into an executable rule.

## The Trust Boundary Graph

The block below **models** the fictional regional library network's trust zones, the boundaries
between them, the checks each boundary carries, the checks each data type requires, and the
crossings observed in one quarter. An unchecked crossing is one where at least one of the
required checks is missing at the boundary it crosses.

```js
// boundary.mjs — models the trust boundary graph and counts checks per boundary
import { mkdirSync, writeFileSync } from "node:fs";

// QA10: the fictional regional library network's seven trust zones and the eight boundaries
// between them. Each boundary carries the checks applied when crossing it. Model.
const BOUNDARY = [
  ["S1", "member device", "central service", ["identity", "encryption"]],
  ["S2", "branch edge unit", "branch network", ["identity"]],
  ["S3", "branch network", "central service", ["encryption", "logging"]],
  ["S4", "central service", "data layer", ["authorization", "encryption"]],
  ["S5", "central service", "municipal identity", ["identity", "encryption", "logging"]],
  ["S6", "central service", "external catalog", ["encryption"]],
  ["S7", "branch network", "data layer", []],
  ["S8", "member device", "external catalog", []],
];

// QA11: seven data types and the checks each one requires when it crosses a boundary. A
// catalog record is open data and requires no check. Model.
const REQUIRED = {
  "member identity": ["identity", "encryption"],
  "loan history": ["identity", "authorization", "masking"],
  "catalog record": [],
  "fee record": ["identity", "authorization", "logging"],
  "staff authorization": ["identity", "authorization", "logging"],
  "audit record": ["authorization", "logging"],
  "secret value": ["encryption", "logging"],
};

// QA12: eighteen data crossings observed in one quarter [data type, source zone, target zone]. Model.
const FLOW = [
  ["member identity", "member device", "central service"],
  ["member identity", "central service", "municipal identity"],
  ["member identity", "branch edge unit", "branch network"],
  ["loan history", "data layer", "central service"],
  ["loan history", "central service", "member device"],
  ["loan history", "data layer", "branch network"],
  ["loan history", "branch network", "branch edge unit"],
  ["catalog record", "external catalog", "central service"],
  ["catalog record", "external catalog", "member device"],
  ["catalog record", "central service", "data layer"],
  ["catalog record", "branch network", "central service"],
  ["fee record", "central service", "data layer"],
  ["fee record", "branch edge unit", "branch network"],
  ["staff authorization", "municipal identity", "central service"],
  ["staff authorization", "central service", "branch network"],
  ["audit record", "central service", "data layer"],
  ["secret value", "central service", "external catalog"],
  ["secret value", "central service", "data layer"],
];

const ZONE = [...new Set(BOUNDARY.flatMap(([, a, b]) => [a, b]))];
const findBoundary = (a, b) => BOUNDARY.find(([, x, y]) => (x === a && y === b) || (x === b && y === a));
const missing = (type, s) => REQUIRED[type].filter((c) => !s[3].includes(c));

mkdirSync("graph", { recursive: true });
writeFileSync("graph/graph.mjs",
  `export const BOUNDARY = ${JSON.stringify(BOUNDARY)};\n` +
  `export const REQUIRED = ${JSON.stringify(REQUIRED)};\n` +
  `export const FLOW = ${JSON.stringify(FLOW)};\n`);

console.log(`${ZONE.length} trust zones, ${BOUNDARY.length} boundaries, ${FLOW.length} data crossings, ` +
  `${Object.keys(REQUIRED).length} data types`);
console.log(`\n${"boundary".padEnd(10)}${"zone pair".padEnd(38)}${"checks".padStart(8)}` +
  `${"crossings".padStart(10)}${"crossings missing a check".padStart(27)}`);
for (const s of BOUNDARY) {
  const crossing = FLOW.filter(([, a, b]) => findBoundary(a, b) === s);
  console.log(`${s[0].padEnd(10)}${`${s[1]} - ${s[2]}`.padEnd(38)}${String(s[3].length).padStart(8)}` +
    `${String(crossing.length).padStart(10)}` +
    `${String(crossing.filter(([v]) => missing(v, s).length > 0).length).padStart(27)}`);
}

const flawed = FLOW.map((a) => [a, findBoundary(a[1], a[2])])
  .map(([a, s]) => [a, s, missing(a[0], s)]).filter(([, , e]) => e.length > 0);
console.log(`\ncrossing missing a check: ${flawed.length}/${FLOW.length} crossings miss at least one required check`);
console.log(`${"boundary".padEnd(10)}${"data type".padEnd(21)}${"direction".padEnd(39)}missing check`);
for (const [a, s, e] of flawed)
  console.log(`${s[0].padEnd(10)}${a[0].padEnd(21)}${`${a[1]} -> ${a[2]}`.padEnd(39)}${e.join(", ")}`);

const noCheckAtAll = flawed.filter(([, s]) => s[3].length === 0);
console.log(`\nboundary with no check at all: ${BOUNDARY.filter((s) => s[3].length === 0).map((s) => s[0]).join(", ")}; ` +
  `data crossings needing a check through these boundaries ${noCheckAtAll.length}`);
const perType = Object.keys(REQUIRED).map((v) =>
  [v, flawed.filter(([a]) => a[0] === v).length]).filter(([, n]) => n > 0);
console.log(`crossings missing a check, per data type: ` +
  perType.map(([v, n]) => `${v} ${n}`).join(", "));
```

```
7 trust zones, 8 boundaries, 18 data crossings, 7 data types

boundary  zone pair                               checks crossings  crossings missing a check
S1        member device - central service              2         2                          1
S2        branch edge unit - branch network            1         3                          3
S3        branch network - central service             2         2                          1
S4        central service - data layer                 2         5                          4
S5        central service - municipal identity         3         2                          1
S6        central service - external catalog           1         2                          1
S7        branch network - data layer                  0         1                          1
S8        member device - external catalog             0         1                          0

crossing missing a check: 12/18 crossings miss at least one required check
boundary  data type            direction                              missing check
S2        member identity      branch edge unit -> branch network     encryption
S4        loan history         data layer -> central service          identity, masking
S1        loan history         central service -> member device       authorization, masking
S7        loan history         data layer -> branch network           identity, authorization, masking
S2        loan history         branch network -> branch edge unit     authorization, masking
S4        fee record           central service -> data layer          identity, logging
S2        fee record           branch edge unit -> branch network     authorization, logging
S5        staff authorization  municipal identity -> central service  authorization
S3        staff authorization  central service -> branch network      identity, authorization
S4        audit record         central service -> data layer          logging
S6        secret value         central service -> external catalog    logging
S4        secret value         central service -> data layer          logging

boundary with no check at all: S7, S8; data crossings needing a check through these boundaries 1
crossings missing a check, per data type: member identity 1, loan history 4, fee record 2, staff authorization 2, audit record 1, secret value 2
```

12 of eighteen crossings miss at least one required check. The distribution is not even boundary
by boundary: the boundary facing the municipal identity zone carries three checks, the one
between the branch network and the data layer carries none. But the two boundaries without
checks are not the same thing: there is also no check between the member device and the external
catalog, but only the catalog record — open data — crosses there. **An unchecked boundary is not
by itself a flaw; the flaw is data requiring a check crossing an unchecked boundary.**

The busiest boundary is the one with the most missing checks: five crossings run between the
central service and the data layer, four with a missing check. This is not a coincidence — as
more data types cross a boundary, the required set of checks grows, but the boundary carries only
one fixed set.

By data type, loan history leads with four missing checks: it crosses four separate boundaries,
and one of its three required checks, masking, is not defined at any of the eight boundaries.
**One of the required checks does not exist anywhere in the system**, and this only shows up once
the required set and the existing set get placed side by side.

## Removing a Boundary and the Rule's Limit

The graph's second question: what happens if a boundary is removed. Removing one means the two
zones become a single trust zone, and data in either one crosses to the other unconditionally.
The block below computes this for every boundary, then runs the rule against known incidents.

```js
// removal.mjs — counts the paths a removed boundary opens and runs the check against a known set of incidents
import { BOUNDARY, REQUIRED, FLOW } from "./graph/graph.mjs";

// QA13: the zone each data type is born in; reach is the transitive closure that starts at
// the source and follows the flow arrows. Model.
const SOURCE = {
  "member identity": "member device", "loan history": "data layer",
  "catalog record": "external catalog", "fee record": "branch edge unit",
  "staff authorization": "municipal identity", "audit record": "central service",
  "secret value": "central service",
};
const DATA = Object.keys(REQUIRED);

// When a boundary is removed, the two zones become a single trust zone: every data type gets
// an unconditional crossing between the two ends.
const reach = (type, removed) => {
  const reached = new Set([SOURCE[type]]);
  const merged = removed ? [removed[1], removed[2]] : [];
  for (let n = 0; n < BOUNDARY.length + 1; n += 1)
    for (const [v, a, b] of FLOW) {
      if (v === type && reached.has(a)) reached.add(b);
      if (merged.length && reached.has(merged[0])) reached.add(merged[1]);
      if (merged.length && reached.has(merged[1])) reached.add(merged[0]);
    }
  return reached;
};

const baseline = Object.fromEntries(DATA.map((v) => [v, reach(v, null)]));
const baselineTotal = DATA.reduce((s, v) => s + baseline[v].size, 0);
console.log(`baseline reach: ${baselineTotal} (data type, zone) pairs, ` +
  `${DATA.length} data types x ${new Set(BOUNDARY.flatMap((s) => [s[1], s[2]])).size} zones`);
console.log(`\n${"boundary".padEnd(10)}${"zones merged if removed".padEnd(40)}` +
  `${"paths opened".padStart(13)}  data type opened the most`);
const opened = BOUNDARY.map((s) => {
  const added = DATA.map((v) => [v, reach(v, s).size - baseline[v].size]);
  return [s, added.reduce((t, [, n]) => t + n, 0), added.sort((a, b) => b[1] - a[1])[0]];
}).sort((a, b) => b[1] - a[1]);
for (const [s, total, [v, n]] of opened)
  console.log(`${s[0].padEnd(10)}${`${s[1]} + ${s[2]}`.padEnd(40)}${String(total).padStart(13)}` +
    `  ${n > 0 ? `${v} (${n})` : "-"}`);

// Check: every crossing must pass through the checks its data type requires at the boundary it crosses.
const findBoundary = (a, b) => BOUNDARY.find(([, x, y]) => (x === a && y === b) || (x === b && y === a));
const missing = (type, s) => (s ? REQUIRED[type].filter((c) => !s[3].includes(c)) : null);
const warning = FLOW.map((a) => [a, findBoundary(a[1], a[2])])
  .filter(([a, s]) => missing(a[0], s).length > 0);

// QA14: known set of incidents — ten security incidents on record over the last two quarters
// [data type, source, target, note]. Model.
const INCIDENT = [
  ["loan history", "data layer", "branch network", ""],
  ["fee record", "branch edge unit", "branch network", ""],
  ["secret value", "central service", "external catalog", ""],
  ["loan history", "branch network", "branch edge unit", ""],
  ["staff authorization", "central service", "branch network", ""],
  ["fee record", "central service", "data layer", ""],
  ["loan history", "data layer", "data layer", "in-zone backup copy"],
  ["secret value", "central service", "central service", "secret that fell into a log file"],
  ["member identity", "data layer", "external catalog", "path undefined in the graph"],
  ["member identity", "member device", "central service", "boundary with full checks"],
];
// QA15: three of the flagged crossings have their missing check met somewhere other than the boundary.
const METELSEWHERE = [["audit record", "central service", "data layer", "data layer transaction log"],
  ["secret value", "central service", "data layer", "data layer transaction log"],
  ["staff authorization", "municipal identity", "central service", "authorization is signed at the source"]];

const matches = (a, b) => a[0] === b[0] && a[1] === b[1] && a[2] === b[2];
const caught = INCIDENT.filter((i) => warning.some(([a]) => matches(a, i)));
const missed = INCIDENT.filter((i) => !caught.includes(i));
const falseAlarm = warning.filter(([a]) => METELSEWHERE.some((k) => matches(a, k)));
const openGap = warning.length - caught.length - falseAlarm.length;
console.log(`\n${warning.length} warnings, ${INCIDENT.length} known incidents: caught ${caught.length}, ` +
  `missed ${missed.length}, false alarm ${falseAlarm.length}, ` +
  `open gap with no incident yet ${openGap}`);
for (const i of missed) {
  const s = findBoundary(i[1], i[2]);
  console.log(`  missed: ${i[0].padEnd(20)} ${`${i[1]} -> ${i[2]}`.padEnd(36)} ` +
    `${i[3] || (s ? "boundary has no missing check" : "no such boundary")}`);
}
for (const [a] of falseAlarm) {
  const k = METELSEWHERE.find((n) => matches(a, n));
  console.log(`  false alarm: ${a[0].padEnd(20)} check met elsewhere: ${k[3]}`);
}

// QA16: the part that cannot be checked. The graph checks crossings at the boundaries it
// knows about; it cannot check whether a boundary is in the right place, or a path that was
// never reported. In its place: a review every quarter, 3 person-hours per boundary. Model.
const HOURS = 3;
const outsideGraph = missed.filter((i) => !findBoundary(i[1], i[2])).length;
console.log(`\ncannot be checked: whether the boundary is in the right place, and an unreported path; ` +
  `${outsideGraph} of the ${missed.length} missed incidents are a crossing with no path in the graph`);
console.log(`stands in its place: a review for ${BOUNDARY.length} boundaries, ` +
  `${BOUNDARY.length * HOURS} person-hours/quarter; the removal ranking gives the order ` +
  `(top of the list: ${opened[0][0][0]}, ${opened[0][1]} paths)`);
const record = BOUNDARY.length + FLOW.length + DATA.length;
console.log(`check cost: ${record} graph records held by hand (boundary, flow, data type); ` +
  `run ${FLOW.reduce((s, a) => s + REQUIRED[a[0]].length, 0)} check comparisons + ` +
  `${BOUNDARY.length} removal computations; if the graph goes stale, missed grows`);
```

```
baseline reach: 22 (data type, zone) pairs, 7 data types x 7 zones

boundary  zones merged if removed                  paths opened  data type opened the most
S3        branch network + central service                    6  fee record (2)
S7        branch network + data layer                         5  catalog record (1)
S5        central service + municipal identity                4  loan history (1)
S6        central service + external catalog                  4  member identity (1)
S1        member device + central service                     3  staff authorization (1)
S8        member device + external catalog                    3  member identity (1)
S4        central service + data layer                        2  member identity (1)
S2        branch edge unit + branch network                   1  staff authorization (1)

12 warnings, 10 known incidents: caught 6, missed 4, false alarm 3, open gap with no incident yet 3
  missed: loan history         data layer -> data layer             in-zone backup copy
  missed: secret value         central service -> central service   secret that fell into a log file
  missed: member identity      data layer -> external catalog       path undefined in the graph
  missed: member identity      member device -> central service     boundary with full checks
  false alarm: staff authorization  check met elsewhere: authorization is signed at the source
  false alarm: audit record         check met elsewhere: data layer transaction log
  false alarm: secret value         check met elsewhere: data layer transaction log

cannot be checked: whether the boundary is in the right place, and an unreported path; 3 of the 4 missed incidents are a crossing with no path in the graph
stands in its place: a review for 8 boundaries, 24 person-hours/quarter; the removal ranking gives the order (top of the list: S3, 6 paths)
check cost: 33 graph records held by hand (boundary, flow, data type); run 36 check comparisons + 8 removal computations; if the graph goes stale, missed grows
```

Boundaries do not carry equal weight. Removing the boundary between the branch network and the
central service opens six new paths; removing the one between the branch edge unit and the
branch network opens one. The ranking does not track the number of checks at the boundary: a
boundary carrying two checks opens more paths than one carrying none. **A boundary's value is
measured not by the checks it carries but by how the zones it separates connect into the graph.**

The rule produces twelve warnings, and the record holds ten incidents: six get caught, four are
missed, three are false alarms. The remaining three warnings are neither — the missing check
really is missing, it just has not produced an incident yet. **A warning with no incident is not
a false alarm**; a false alarm is a warning where the check is actually met but looks missing.
Without this distinction, half the warnings get written off as noise and the rule gets loosened.

The four missed incidents need to be read separately. Two stayed within a zone — the backup copy
and the log file never crossed a boundary. One passed through a path undefined in the graph. The
fourth is the most instructive: it happened at a boundary with a full set of checks. **The graph
sees whether a check exists, not whether it works correctly.** This is where the part that cannot
be checked comes from: whether a boundary is in the right place, and whether an unreported path
exists, cannot be checked, and three of the missed incidents fall in that scope. In their place, a
quarterly review is set for the eight boundaries, 24 person-hours; the removal analysis gives its
order.

The weight of the cost is not in the run: the rule is thirty-six comparisons plus eight removal
computations, but the thirty-three hand-held graph records must be updated with every
architectural change. If the graph goes stale, the rule fails silently: every incident on a flow
missing from the graph becomes a missed one.

## Summary

- The trust boundary graph has seven zones, eight boundaries, eighteen crossings; twelve of them
  miss at least one required check.
- An unchecked boundary is not by itself a flaw: of the two unchecked boundaries, only one
  carries data that requires a check; the busiest boundary is also the one with the most missing
  checks.
- One required check (masking) is not defined at any of the eight boundaries; this only shows up
  once the required set and the existing set get placed side by side.
- Removing a boundary opens between one and six new paths; the ranking is set not by the check
  count but by how the zones connect into the graph.
- Twelve warnings, ten incidents: 6 caught, 4 missed, 3 false alarms, 3 open gaps; three of the
  missed incidents are a crossing with no path in the graph, one happened at a boundary with a
  full set of checks.

## Next Step

The boundary graph said which zone is separated from which; it did not say how many copies of it
run. Once the system grows, the real question is: **what gets copied**. The next lesson lines up
three scale unit candidates — copying the whole system, copying individual services, copying per
tenant — and for each candidate counts the copied component, the shared remaining resource, and
the singleton that cannot be copied under any of them.
