Skip to content
academia.sh

Lesson 08 / 12

Configuration Posture Management

Measuring whether a misconfigured item closes before or after deployment: 13 of the graph's 23 edges are tied to a misconfigured item, 7 of those are in the template, 2 in acceptance, and 4 are only seen at runtime. The template gate lowers the radius from 23 to 8, the acceptance gate closes two more items and does not lower the radius at all, and the runtime scan alone lowers it to 4. In a one-week window, daily scanning with approved remediation leaves the radius open for 39 hours, hourly scanning for 25 hours, and automatic remediation for 2 hours; moving to continuous scanning gains 1 hour at the cost of 12 times the evaluations.

Published Updated

In this lesson

  • Separates misconfigured items by the phase in which they can be seen and counts the items each gate closes before deployment
  • Measures the number of items closed and the drop in radius separately, and names the gate that does not lower the radius at all
  • Calculates how many hours the detection-and-remediation delay leaves the radius open, and writes down the cost of scan frequency
Contents

The previous lesson left a direction: what lowers the radius is not a mechanism that watches after the walk has started, but an item closed before the walk starts. Closing off role chaining lowered the radius from 23 to 18 without waiting on the log at all. This lesson measures that direction all the way through and ties it to a single question: does the misconfigured item close before it is used, or after?

Configuration posture management is the mechanism that continuously reads the resource set and flags any item that drifts from the desired state. How compliance rules are written, how many real violations are caught per rule, and how many are false flags were measured in the Cloud Governance, Security and Cost course; they are not repeated here. The only thing measured here is time: the phase in which the item can be seen, the delay between detection and remediation, and how many hours that delay leaves the radius open.

The graph is the same as the previous lesson’s: the regional measurement network is fictional, a model written in node.

CD7. Thirteen of the graph’s 23 edges are tied to a misconfigured item; if the item is closed, that edge cannot be walked. The remaining 10 edges are legitimate and cannot be closed. CD8. A misconfigured item can be seen in one of three phases: template (before deployment, as infrastructure-as-code text), acceptance (at deployment time, while the desired state is evaluated), runtime (only by a scan that looks at the running resource). CD9. Four runtime items are born within a week; their birth minutes are fixed in the model (fictional). CD10. Given scan interval SS and remediation delay RR, an item closes at minute ⌈d/S⌉⋅S+R\lceil d/S \rceil \cdot S + R; dd is the moment of birth. CD11. The measurement window is one week (10,080 minutes), and the radius is calculated separately for every minute. CD12. Each scan evaluates 13 items; the weekly evaluation count is a fictional resource cost.

The Phase in Which a Misconfigured Item Can Be Seen

// posture.mjs — the phase in which a misconfigured item can be seen and the step each phase closes. IT IS A MODEL.
// The graph is the same as the previous lesson's; here every edge is tied to a misconfigured item.
const split = (s) => s.trim().split("\n").map((l) => l.trim().split("|"));

// Edge: source|target|duration(min)|misconfigured item that makes the edge possible ("-" if the edge is legitimate)
export const EDGE = split(`
  portal-service-account|portal-instance|3|-
  portal-service-account|reader-role|4|-
  portal-instance|portal-instance-identity|2|instance-identity-sessionless
  portal-instance-identity|subscriber-database|6|-
  portal-instance-identity|secret-store|4|portal-identity-secret-read
  secret-store|collection-workload-identity|3|secret-store-long-lived-identity
  collection-workload-identity|raw-meter-bucket|5|-
  collection-workload-identity|measurement-queue|4|-
  collection-workload-identity|monitoring-role|4|role-assumption-to-workload
  monitoring-role|measurement-dashboard|3|-
  portal-instance-identity|billing-batch-role|3|portal-identity-batch-role
  billing-batch-role|billing-archive-bucket|6|-
  billing-batch-role|key-management|4|key-usage-unconditional
  key-management|customer-key|2|key-policy-no-condition
  billing-batch-role|backup-role|3|chained-role-assumption
  backup-role|backup-bucket|7|-
  backup-role|deployment-role|4|deployment-to-backup-role
  deployment-role|template-store|5|template-store-open-write
  deployment-role|configuration-bucket|4|-
  deployment-role|workload-instance|9|deployment-role-instance-launch
  reader-role|report-bucket|4|-
  reader-role|log-bucket|5|log-bucket-open-read
  reader-role|configuration-bucket|6|configuration-bucket-broad-read`)
  .map(([k, h, d, y]) => ({ k, h, d: +d, y }));

// The phase in which a misconfigured item can be seen. MODEL input.
// template:   before deployment, read as infrastructure-as-code text
// acceptance: at deployment time, read while the desired state is evaluated
// runtime:    only found by a scan that looks at the running resource
export const PHASE = Object.fromEntries(split(`
  portal-identity-secret-read|template          role-assumption-to-workload|template
  portal-identity-batch-role|template      key-policy-no-condition|template
  chained-role-assumption|template               deployment-to-backup-role|template
  deployment-role-instance-launch|template   key-usage-unconditional|acceptance
  template-store-open-write|acceptance           instance-identity-sessionless|runtime
  secret-store-long-lived-identity|runtime             configuration-bucket-broad-read|runtime
  log-bucket-open-read|runtime`
  .replace(/\s{2,}/g, "\n")));

export const START = "portal-service-account";
export const TOTAL = 28;   // total items in the graph; five are unreachable by any walk

// Radius given a set of open misconfigured items: only legitimate or open edges are walked.
export function radius(open) {
  const arrival = { [START]: 0 };
  for (;;) {
    let best = null;
    for (const e of EDGE) {
      if (!(e.k in arrival) || e.h in arrival) continue;
      if (e.y !== "-" && !open.has(e.y)) continue;
      const t = arrival[e.k] + e.d;
      if (best === null || t < best.t) best = { e, t };
    }
    if (best === null) return { radius: Object.keys(arrival).length, arrival };
    arrival[best.e.h] = best.t;
  }
}

const ITEMS = [...new Set(EDGE.map((e) => e.y).filter((y) => y !== "-"))];
const byPhase = (...p) => new Set(ITEMS.filter((y) => p.includes(PHASE[y])));

const gen = (W) => (row) => row.map((v, j) => j ? String(v).padStart(W[j]) : String(v).padEnd(W[0]))
  .join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const count = (p) => ITEMS.filter((y) => PHASE[y] === p).length;
  console.log(`${ITEMS.length} of ${EDGE.length} edges are tied to a misconfigured item, ` +
    `${EDGE.length - ITEMS.length} are legitimate`);
  console.log(`phase distribution: template ${count("template")}, acceptance ${count("acceptance")}, ` +
    `runtime ${count("runtime")}`);

  const row = gen([34, 14, 12, 10, 20]);
  console.log("\n" + row(["gate", "items closed", "items open", "radius", "closes when"]));
  const stage = [
    ["no gate (all open)", byPhase("template", "acceptance", "runtime"), "-"],
    ["runtime scan only", byPhase("template", "acceptance"), "after deployment"],
    ["template gate", byPhase("acceptance", "runtime"), "before deployment"],
    ["template + acceptance gate", byPhase("runtime"), "before deployment"],
    ["all three", new Set(), "before and after"],
  ];
  for (const [name, open, when] of stage) {
    const r = radius(open);
    console.log(row([name, ITEMS.length - open.size, open.size, `${r.radius}/${TOTAL}`, when]));
  }

  // How far the radius would drop if each misconfigured item were closed alone.
  const full = radius(new Set(ITEMS)).radius;
  const singleton = ITEMS.map((y) => {
    const a = new Set(ITEMS); a.delete(y);
    return { y, drop: full - radius(a).radius, phase: PHASE[y] };
  }).sort((a, b) => b.drop - a.drop);
  const row2 = gen([36, 10, 10]);
  console.log("\n" + row2(["item closed alone", "phase", "radius"]));
  for (const t of singleton.slice(0, 5)) console.log(row2([t.y, t.phase, `${full} -> ${full - t.drop}`]));
  console.log(`the remaining ${singleton.length - 5} items each lower the radius by one item ` +
    `or not at all`);
}
13 of 23 edges are tied to a misconfigured item, 10 are legitimate
phase distribution: template 7, acceptance 2, runtime 4

gate                                items closed  items open    radius         closes when
no gate (all open)                             0          13     23/28                   -
runtime scan only                              4           9      4/28    after deployment
template gate                                  7           6      8/28   before deployment
template + acceptance gate                     9           4      8/28   before deployment
all three                                     13           0      4/28    before and after

item closed alone                        phase    radius
instance-identity-sessionless          runtime   23 -> 6
portal-identity-batch-role            template  23 -> 14
portal-identity-secret-read           template  23 -> 17
secret-store-long-lived-identity       runtime  23 -> 18
chained-role-assumption               template  23 -> 18
the remaining 8 items each lower the radius by one item or not at all

The top table forces two columns to be read separately: items closed and radius do not move in the same direction. The template gate closes seven of the thirteen items — more than half — before deployment, and the radius drops from 23 to 8. The acceptance gate closes two more items — up to nine — and the radius stays at 8. The two additionally closed items were already protecting items reached through another edge even while closed; the path to key management had already been cut when the billing role closed. This is the configuration-side counterpart of the previous lesson’s log-entry line: counting items is not counting radius.

There is a sharper number in the opposite direction. The runtime scan alone closes four items and the radius drops to 4 — the same result as opening all three gates together. The reason is in the table below: the item that alone lowers the radius from 23 to 6 is the instance-bound identity being issued without per-session authentication, and that item is in the runtime class. The template text is written correctly; an instance launched from an old image carries the old behavior, and only a scan that looks at the running instance can see it.

What the Gate Closes and What It Cannot

The bottom table also gives the ranking. Four of the thirteen items genuinely move the radius; the remaining eight lower it by one item or not at all. The item at the top of the ranking is in the runtime class, the next three are in the template class. The value of closing before deployment is here, and it is measured in time: an item closed in the template phase never comes into existence, so the time it stays open is zero. An item in the runtime class, however, is born, lives for a while, and only then closes. The next section counts that time.

The Window Opened by Remediation Delay

// window.mjs — how many hours the detection-and-remediation delay leaves the radius open. IT IS A MODEL.
import { PHASE, TOTAL, radius } from "./posture.mjs";

const WEEK = 7 * 24 * 60;                       // 10,080-minute window
const RUNTIME = Object.keys(PHASE).filter((y) => PHASE[y] === "runtime");
// The minute the misconfigured item is born (fictional): the moment the drift appears.
const BORN = { "configuration-bucket-broad-read": 900, "instance-identity-sessionless": 2600,
  "secret-store-long-lived-identity": 4300, "log-bucket-open-read": 7100 };
const FIXED = Object.keys(PHASE).filter((y) => PHASE[y] !== "runtime");

// When an item closes, given scan interval S and remediation delay R.
const closes = (born, S, R) => S === Infinity ? Infinity : Math.ceil(born / S) * S + R;

function run({ S, R, gate }) {
  const closeAt = Object.fromEntries(RUNTIME.map((y) => [y, closes(BORN[y], S, R)]));
  const cache = new Map();
  let total = 0, peak = 0, openMin = 0, heavyMin = 0;
  const baseline = radius(gate ? new Set() : new Set(FIXED)).radius;
  for (let t = 0; t < WEEK; t++) {
    const open = new Set(gate ? [] : FIXED);
    for (const y of RUNTIME) if (t >= BORN[y] && t < closeAt[y]) open.add(y);
    const key = [...open].sort().join(",");
    if (!cache.has(key)) cache.set(key, radius(open).radius);
    const r = cache.get(key);
    total += r; peak = Math.max(peak, r);
    if (r > baseline) openMin++;
    if (r >= 10) heavyMin++;
  }
  return { avg: (total / WEEK).toFixed(1), peak, baseline,
    open: (openMin / 60).toFixed(0), heavy: (heavyMin / 60).toFixed(0),
    evaluations: S === Infinity ? 0 : Math.round((WEEK / S) * Object.keys(PHASE).length) };
}

const SETTING = [
  ["no scan, no gate", { S: Infinity, R: 0, gate: false }],
  ["daily scan + approved remediation", { S: 1440, R: 480, gate: false }],
  ["hourly scan + approved remediation", { S: 60, R: 480, gate: false }],
  ["hourly scan + automatic remediation", { S: 60, R: 10, gate: false }],
  ["+ template and acceptance gate", { S: 60, R: 10, gate: true }],
  ["continuous scan + automatic remediation", { S: 5, R: 10, gate: true }],
];

const gen = (W) => (row) => row.map((v, j) => j ? String(v).padStart(W[j]) : String(v).padEnd(W[0]))
  .join("");
const row = gen([41, 9, 9, 9, 12, 13, 15]);

console.log(row(["setting", "baseline", "average", "peak", "open hours", "radius>=10", "evaluations"]));
for (const [name, a] of SETTING) {
  const r = run(a);
  console.log(row([name, `${r.baseline}/${TOTAL}`, r.avg, `${r.peak}/${TOTAL}`, `${r.open} h`,
    `${r.heavy} h`, r.evaluations]));
}

const approved = run({ S: 60, R: 480, gate: false }), auto = run({ S: 60, R: 10, gate: false });
const daily = run({ S: 1440, R: 480, gate: false });
console.log(`\ntaking the scan interval from 1440 -> 60 min brings the open time from ${daily.open} -> ` +
  `${approved.open} hours; taking the remediation delay from 480 -> 10 min brings it ` +
  `${approved.open} -> ${auto.open} hours`);
const gated = run({ S: 60, R: 10, gate: true });
console.log(`adding the gate takes the baseline ${auto.baseline} -> ${gated.baseline} and the peak ` +
  `${auto.peak} -> ${gated.peak}; open hours ${auto.open} -> ${gated.open}`);
const continuous = run({ S: 5, R: 10, gate: true });
console.log(`taking the scan interval from 60 -> 5 min brings the open hours from ${gated.open} -> ` +
  `${continuous.open} and takes the weekly evaluations from ${gated.evaluations} -> ` +
  `${continuous.evaluations}: ${(continuous.evaluations / gated.evaluations).toFixed(0)}x ` +
  `the work for ${gated.open - continuous.open} hour(s) gained`);
console.log(`the four runtime items are born at minutes ${Object.values(BORN).join(", ")}; ` +
  `the instance-identity item is born at minute 2600, and while open the radius is ${gated.peak} ` +
  `in the gated setup, ${auto.peak} in the ungated one`);
setting                                   baseline  average     peak  open hours   radius>=10    evaluations
no scan, no gate                              4/28     17.0    23/28       153 h        125 h              0
daily scan + approved remediation             4/28      5.1    17/28        39 h         13 h             91
hourly scan + approved remediation            4/28      4.8    17/28        25 h          9 h           2184
hourly scan + automatic remediation           4/28      4.1    17/28         2 h          1 h           2184
+ template and acceptance gate                4/28      4.0     6/28         2 h          0 h           2184
continuous scan + automatic remediation       4/28      4.0     6/28         1 h          0 h          26208

taking the scan interval from 1440 -> 60 min brings the open time from 39 -> 25 hours; taking the remediation delay from 480 -> 10 min brings it 25 -> 2 hours
adding the gate takes the baseline 4 -> 4 and the peak 17 -> 6; open hours 2 -> 2
taking the scan interval from 60 -> 5 min brings the open hours from 2 -> 1 and takes the weekly evaluations from 2184 -> 26208: 12x the work for 1 hour(s) gained
the four runtime items are born at minutes 900, 2600, 4300, 7100; the instance-identity item is born at minute 2600, and while open the radius is 6 in the gated setup, 17 in the ungated one

With no mechanism at all, the radius stands above the baseline for 153 of the week’s hours, and its average is 17. The drift opens the moment it is born and never closes; the entire measurement window is one open window.

Frequency or Delay

Two numbers set the ranking, and they are not tied to the same knob. Taking the scan interval from daily to hourly pulls the open time from 39 hours to 25 hours: a fourteen-hour gain for twenty-four times the scanning. Taking the remediation delay from eight hours to ten minutes, on the other hand, pulls those 25 hours down to 2 hours: a twenty-three-hour gain, with no added scanning at all. What determines the open window is not how often the scan runs, but when the flagged item actually closes. An eight-hour approval window gives back most of what the hourly scan gained.

This does not make automatic remediation free. It was counted in the earlier course that automatic remediation demands broad write authority on the rented layer and can break legitimate configurations; the contribution here is to write down that the return on that cost is a twenty-three-hour window. Keeping the approval window short and automating remediation get written into the same column.

The last two rows add the gate. The template and acceptance gate does not change the open time at all — 2 hours, unchanged — because the open time is set by the runtime items. What it does change is the peak radius: it drops from 17 to 6. The gate narrows the window’s depth, not its length. Two controls do two separate jobs in the same table, and neither substitutes for the other. The bottom row shows the limit: taking the scan interval from an hour to five minutes raises the weekly evaluations from 2,184 to 26,208 — twelve times the work — for a gain of one hour.

Summary

  • Thirteen of the graph’s 23 edges are tied to a misconfigured item; 7 of the items are in the template phase, 2 are in acceptance, and 4 can only be seen by looking at the running resource.
  • The template gate closes seven items before deployment and lowers the radius from 23 to 8; the acceptance gate closes two more items and does not lower the radius at all — counting items is not counting radius.
  • The single item that lowers the radius the most is the instance-bound identity being issued without per-session authentication (from 23 to 6), and that item is not visible in the template — it can only be seen at runtime.
  • With no mechanism at all, the radius sits above the baseline for 153 of the week’s hours; daily scanning with approved remediation brings this to 39 hours, hourly scanning to 25 hours.
  • What determines the open window is not scan frequency but remediation delay: taking an eight-hour approval window down to ten minutes pulls 25 hours down to 2, with no added scanning required.
  • The template and acceptance gate does not change the open time but lowers the peak radius from 17 to 6; pulling the scan interval from an hour to five minutes gains one hour at the cost of twelve times the evaluations.

Next Step

Up to this point, every misconfigured item lived inside the tenant’s own account and was a configuration that could be closed. One item does not fit this description: the boundary of the partition or guest system the workload runs on. That boundary is not a configuration the tenant wrote, and when it falls, the set of items reached is no longer bounded by the tenant’s account either. The next lesson measures this boundary in a multi-tenant environment: what the radius of a partition escape is for a single tenant, and what it becomes once tenants sharing the same hardware are counted in.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close