Skip to content
academia.sh

Lesson 10 / 15

Leak Response

Leak response is run as a revocation flow: across the remaining six static secrets, 1,386 minutes pass on average from the leak to revocation and 4,650 minutes in the worst item, and 224,171 calls pass through the secrets in that time. An ownership record gains 340 minutes and a pre-staged replacement gains 465; continuous repository scanning gains nothing because only two of the six leaks land in a repository. Revoking without waiting for propagation saves 150,934 calls but cuts 68,503 legitimate requests — one cut request for every 2.2 calls saved.

Published Updated

In this lesson

  • Breaks the post-leak revocation flow into its stages and runs it: detection, confirmation, finding the owner, generating, propagation
  • Counts, separately for each secret, the minutes from detection to revocation and the calls that pass through the secret in that time
  • Measures the gain of each measure that speeds up the flow together with the legitimate requests it cuts, and singles out the measure whose gain is zero
Contents

The first four lessons of the topic measured the number, the life, and the change cost of secrets, and left the same question unanswered: what happens when a secret leaks. The exposure window was the time it takes for the secret to become invalid on its own; if the leak is detected and revoked, the window closes earlier. This lesson measures that “earlier.”

What is measured is the revocation flow, and the flow is on the defense side. Nothing is written about what can be done with a leaked secret or how such a secret would be found; the only things written are the minutes from the detection of the leak to the invalidation of the old value and the call volume that passes through the secret in those minutes. The measurement runs on the six static secrets left over from the previous lesson.

  • SM23. The model of the previous four lessons continues. The environment is fiction, what is measured is a model written with node, and it carries no real secret value; the model holds only secret paths and flow durations.
  • SM24. The leak channel of each secret is chosen in the model and determines the detection path: a leak that lands in the code repository is detected by continuous scanning, one that lands in a log file by usage anomaly, one at a third party by provider notice, and one in a client bundle by external report.
  • SM25. The flow has five stages: detection, confirmation and impact assessment, finding the owner, generating the new value, propagation of the new value. The propagation time is the minimum overlap of the previous lesson.
  • SM26. “Calls passing through the secret” is the total call volume that the secret authorized during the exposure window; it is an upper bound that shows the scale of the window, not the volume that was abused.
  • SM27. There is no randomness; all numbers are derived from the model tables and the output is the same from run to run.

How Many Minutes from Detection to Revocation

// revocation.mjs — minutes from the leak to revocation and calls passing through the secret in that time. A MODEL.
// The environment is FICTION; THERE IS NO REAL SECRET VALUE, only secret paths and flow durations are modeled.
// How a secret would be used is not written; what is measured is the revocation flow on the defense side.

// Six static secrets left over from the previous lesson. channel: where the leak lands (chosen in the model).
// owner: whether the owner is written in the secret record. external: whether an external provider generates the new value.
// calls: daily calls the secret authorizes. spread: minimum overlap of the previous lesson (min).
export const SECRETS = {
  "infra/cache": { channel: "repository", owner: true, external: false, calls: 240000, spread: 60 },
  "signing/session-signing": { channel: "log", owner: true, external: false, calls: 180000,
    spread: 960 },
  "signing/document-signing": { channel: "repository", owner: true, external: false, calls: 6200,
    spread: 240 },
  "external/payment-gateway": { channel: "third-party", owner: true, external: true, calls: 3400,
    spread: 240 },
  "external/email-gateway": { channel: "client-bundle", owner: false, external: true, calls: 13400,
    spread: 1440 },
  "external/sms-gateway": { channel: "log", owner: false, external: true, calls: 8900,
    spread: 960 },
};
// the detection path depends on the channel: only a leak that lands in the code repository is caught by continuous scanning.
export const DETECTION = { repository: ["continuous scanning", 20], log: ["usage anomaly", 90],
  "third-party": ["provider notice", 240], "client-bundle": ["external report", 2880] };
export const CONFIRM = 30;                     // confirmation and impact assessment (min)
export const OWNED = 10, UNOWNED = 180;        // finding the owner (min)
export const PRODUCTION = 45, EXTERNAL_PRODUCTION = 120;  // generating the new value (min)
export const NAMES = Object.keys(SECRETS);
export const stages = (s, o = {}) => {
  const k = SECRETS[s];
  return { detection: o.scan && k.channel === "repository" ? 20 : DETECTION[k.channel][1],
    confirmation: CONFIRM,
    owner: k.owner || o.record ? OWNED : UNOWNED,
    production: o.staged ? 5 : k.external ? EXTERNAL_PRODUCTION : PRODUCTION,
    spread: o.noWait ? 0 : k.spread };
};
export const total = (s, o) => Object.values(stages(s, o)).reduce((t, x) => t + x, 0);
export const callsUntil = (s, o) => Math.round(SECRETS[s].calls * total(s, o) / 1440);
export const gen = (B) => (h) => h.map((v, j) => j ? String(v).padStart(B[j])
  : String(v).padEnd(B[0])).join("");

if (import.meta.url === `file://${process.argv[1]}`) {
  const row = gen([18, 22, 8, 9, 8, 10, 11, 8]);
  console.log(`revocation flow — ${NAMES.length} static secrets (fictional production environment); durations in minutes\n`);
  console.log(row(["secret", "detection path", "detect", "confirm", "owner", "generate",
    "propagate", "total"]));
  for (const s of NAMES) {
    const a = stages(s);
    console.log(row([s.split("/")[1], DETECTION[SECRETS[s].channel][0], a.detection,
      a.confirmation, a.owner, a.production, a.spread, total(s)]));
  }
  const T = NAMES.reduce((t, s) => t + total(s), 0), C = NAMES.reduce((t, s) => t + callsUntil(s), 0);
  console.log(row(["total", "", "", "", "", "", "", T]));

  console.log(`\n${gen([18, 12, 12, 14, 16])(["secret", "total min", "= hours",
    "daily calls", "until revoked"])}`);
  for (const s of [...NAMES].sort((x, y) => callsUntil(y) - callsUntil(x)))
    console.log(gen([18, 12, 12, 14, 16])([s.split("/")[1], total(s),
      (total(s) / 60).toFixed(1), SECRETS[s].calls, callsUntil(s)]));
  console.log(gen([18, 12, 12, 14, 16])(["total", T, (T / 60).toFixed(1), "", C]));
  console.log(`\naverage per secret ${Math.round(T / NAMES.length)} min ` +
    `(${(T / NAMES.length / 60).toFixed(1)} hours); longest ${Math.max(...NAMES.map((s) => total(s)))} min`);
  console.log(`if the same leak were in a short-lived credential the revocation flow would never run, ` +
    `the window would stay at 60 min: ${Math.round(Math.max(...NAMES.map((s) => total(s))) / 60)} times`);
}
revocation flow — 6 static secrets (fictional production environment); durations in minutes

secret                    detection path  detect  confirm   owner  generate  propagate   total
cache                continuous scanning      20       30      10        45         60     165
session-signing            usage anomaly      90       30      10        45        960    1135
document-signing     continuous scanning      20       30      10        45        240     345
payment-gateway          provider notice     240       30      10       120        240     640
email-gateway            external report    2880       30     180       120       1440    4650
sms-gateway                usage anomaly      90       30     180       120        960    1380
total                                                                                     8315

secret               total min     = hours   daily calls   until revoked
session-signing           1135        18.9        180000          141875
email-gateway             4650        77.5         13400           43271
cache                      165         2.8        240000           27500
sms-gateway               1380        23.0          8900            8529
payment-gateway            640        10.7          3400            1511
document-signing           345         5.8          6200            1485
total                     8315       138.6                        224171

average per secret 1386 min (23.1 hours); longest 4650 min
if the same leak were in a short-lived credential the revocation flow would never run, the window would stay at 60 min: 78 times

The upper table shows where the flow is spent. The average per secret is one thousand three hundred eighty-six minutes, that is twenty-three hours; but between the rows, from one hundred sixty-five to four thousand six hundred fifty minutes, there is a twenty-eight-fold difference.

Looking at the columns, the whole of the difference comes from two stages. Confirmation is thirty minutes in every row, finding the owner is ten or one hundred eighty, generating is forty-five or one hundred twenty. The detection column, though, goes from twenty to two thousand eight hundred eighty — a one-hundred-forty-four-fold range — and the propagation column from sixty to one thousand four hundred forty. What determines the length of the flow is not the speed of the response team but how the leak is detected and how slowly the new value propagates. Both are fixed before the leak.

The lower table converts the same rows into call volume and breaks the ordering. In duration, the worst item is the email gateway key, at four thousand six hundred fifty minutes; but the most calls pass through the session signing key by the time of revocation: one hundred forty-one thousand eight hundred seventy-five. That key authorizes one hundred eighty thousand calls a day, and one thousand one hundred thirty-five minutes is a long time at that volume. At the opposite end is the document signing key: in three hundred forty-five minutes only one thousand four hundred eighty-five calls. The same flow, the same procedure — and the result differs ninety-six-fold between two items, because the traffic behind them differs.

The last line ties back to the second lesson of this topic. If the same leak were in a short-lived credential, no step of this flow would run: it would not need to be detected, its owner would not need to be found, a new one would not need to be generated; the token would become invalid on its own in sixty minutes. The difference from the worst item is seventy-eight-fold. The revocation flow is the price paid for secrets whose life cannot be shortened.

What Shortening Which Stage Gains

// speedup.mjs — what shortening which stage of the flow gains, and what it cuts in return
import { NAMES, SECRETS, DETECTION, stages, total, callsUntil, gen } from "./revocation.mjs";

// if the old value is revoked without waiting for the switch to the new one, readers are cut for the switch duration.
// [app, switch (min), daily requests] — from the switch table of the previous lesson.
const READS = {
  "infra/cache": [["portal-api", 60, 96000]],
  "signing/session-signing": [["portal-frontend", 960, 84000], ["portal-api", 60, 96000]],
  "signing/document-signing": [["billing-service", 240, 6200]],
  "external/payment-gateway": [["billing-service", 240, 6200], ["payment-bridge", 0, 3400]],
  "external/email-gateway": [["subscriber-signup", 960, 1400], ["notification", 15, 12000],
    ["report-generator", 1440, 320]],
  "external/sms-gateway": [["subscriber-signup", 960, 1400], ["notification", 15, 12000]],
};
const cut = (o) => o.noWait ? Math.round(NAMES.reduce((t, s) => t + READS[s]
  .reduce((x, [, g, i]) => x + i * g / 1440, 0), 0)) : 0;

const SETTINGS = [["today", {}, "-"], ["+ ownership record", { record: 1 }, "deployment"],
  ["+ pre-staged replacement", { record: 1, staged: 1 }, "deployment"],
  ["+ continuous repository scanning", { record: 1, staged: 1, scan: 1 }, "build"],
  ["+ revoke without waiting for propagation", { record: 1, staged: 1, scan: 1, noWait: 1 },
    "runtime"]];
const row = gen([43, 11, 11, 15, 16]);
console.log(row(["measure", "total min", "gain", "calls passed", "cut requests"]));
const T0 = NAMES.reduce((t, s) => t + total(s), 0);
for (const [name, o] of SETTINGS) {
  const T = NAMES.reduce((t, s) => t + total(s, o), 0);
  console.log(row([name, T, T0 - T, NAMES.reduce((t, s) => t + callsUntil(s, o), 0), cut(o)]));
}
const last = SETTINGS[SETTINGS.length - 1][1], prev = SETTINGS[SETTINGS.length - 2][1];
const dC = NAMES.reduce((t, s) => t + callsUntil(s, prev) - callsUntil(s, last), 0);
console.log(`\nrevoke without waiting for propagation: saves ${dC} calls, cuts ${cut(last)} legitimate requests ` +
  `(${(dC / cut(last)).toFixed(1)} calls saved per 1 request cut)`);
console.log(`continuous repository scanning gains 0: two secrets already have the repository as their channel and detection is 20 min, ` +
  `the leak of the other four never lands in a repository`);

const row2 = gen([18, 16, 9, 11, 34]);
console.log(`\n${row2(["secret", "leak channel", "detect", "calls", "earlier closure"])}`);
const EARLIER = { repository: ["build", "pipeline secret scanning"],
  log: ["code", "masking in the log"], "client-bundle": ["build", "bundle secret scanning"],
  "third-party": ["none", "-"] };
for (const s of NAMES)
  console.log(row2([s.split("/")[1], SECRETS[s].channel, stages(s).detection, callsUntil(s),
    `${EARLIER[SECRETS[s].channel][0]}: ${EARLIER[SECRETS[s].channel][1]}`]));
const closable = NAMES.filter((s) => EARLIER[SECRETS[s].channel][0] !== "none");
console.log(`\nsecrets whose leak channel can be closed earlier: ${closable.length} / ${NAMES.length}; ` +
  `the detection latency of these channels totals ` +
  `${closable.reduce((t, s) => t + stages(s).detection, 0)} min`);
const top = NAMES.reduce((x, s) => stages(s).detection > stages(x).detection ? s : x);
console.log(`largest single share: ${top} — ${stages(top).detection} min detection latency and ` +
  `${Math.round(SECRETS[top].calls * stages(top).detection / 1440)} calls, all of it from the build stage`);
measure                                      total min       gain   calls passed    cut requests
today                                             8315          0         224171               0
+ ownership record                                7975        340         221538               0
+ pre-staged replacement                          7510        805         207648               0
+ continuous repository scanning                  7510        805         207648               0
+ revoke without waiting for propagation          3610       4705          56714           68503

revoke without waiting for propagation: saves 150934 calls, cuts 68503 legitimate requests (2.2 calls saved per 1 request cut)
continuous repository scanning gains 0: two secrets already have the repository as their channel and detection is 20 min, the leak of the other four never lands in a repository

secret                leak channel   detect      calls                   earlier closure
cache                   repository       20      27500   build: pipeline secret scanning
session-signing                log       90     141875          code: masking in the log
document-signing        repository       20       1485   build: pipeline secret scanning
payment-gateway        third-party      240       1511                           none: -
email-gateway        client-bundle     2880      43271     build: bundle secret scanning
sms-gateway                    log       90       8529          code: masking in the log

secrets whose leak channel can be closed earlier: 5 / 6; the detection latency of these channels totals 3100 min
largest single share: external/email-gateway — 2880 min detection latency and 26800 calls, all of it from the build stage

The first three rows give the counterpart of the two items written into the response procedure. Writing the owner into every secret record gains three hundred forty minutes; preparing and keeping ready a replacement value in advance for every secret gains four hundred sixty-five minutes more. Together they bring the eight thousand three hundred fifteen minute flow down to seven thousand five hundred ten — ten percent. Both are cheap and are written before a leak; but the total gain is a tenth of the flow.

The fourth row is the kind of result this course looks for: a measure whose gain is zero. Continuous repository scanning does not shorten the response time by even a minute. The reason is written in the lower table: of the six leaks, only two have the code repository as their channel, and in those two detection is already twenty minutes. The other four never land in a repository. Increasing the scanning power does not make visible a leak in places the scan never looks at; every improvement made without counting the set of items a defense covers produces this row.

The fifth row contains the real decision. Revoking the old value without waiting for the new value to propagate brings the flow down from seven thousand five hundred ten to three thousand six hundred ten, and cuts the calls that pass from two hundred seven thousand to fifty-six thousand — one hundred fifty thousand nine hundred thirty-four calls are saved. In return, sixty-eight thousand five hundred three legitimate requests are cut: one legitimate request for every two point two calls saved. This is the real decision of leak response and it cannot be settled by a procedure item, because the right answer changes with the secret: for the payment gateway key, not waiting for propagation may be reasonable; for the session signing key, it means cutting sixty thousand requests.

The last column of the lower table is the axis of the course. Five of the six leak channels can be closed at an earlier stage, and the detection latency of those five channels totals three thousand one hundred minutes. The largest single share is in the email gateway key: because it is embedded in the client bundle of the field app, the leak is noticed only by external report, two days later, and in those two days twenty-six thousand eight hundred calls pass through the key. There is nothing to be done on the response side for this item; the key entered the bundle at the build stage, and with a secret scan there it would never have entered. The same holds for the two keys that land in the log file, one stage earlier: masking in log writing is a code decision.

The only item left is the payment gateway key. The leak happened at a third party; neither a build scan nor code masking would have seen it. For this item the revocation flow really is the only way — one of six items.

Summary

  • The revocation flow has five stages: detection, confirmation, finding the owner, generating the new value, propagation. Across the remaining six static secrets the total is 8,315 minutes, the average per secret is 1,386 minutes (23.1 hours), and the worst item takes 4,650 minutes.
  • What determines the duration is not the response speed but the detection and propagation columns: confirmation is 30 minutes in every row, while detection varies between 20 and 2,880 and propagation between 60 and 1,440.
  • Until revocation, 224,171 calls pass through the secrets, and the ordering is not the same as the duration ordering: the longest flow is the email gateway (4,650 min) but the most calls pass through session signing (141,875).
  • An ownership record gains 340 minutes and a pre-staged replacement 465 — ten percent of the total. The gain of continuous repository scanning is zero: only two of the six leaks land in a repository and in those two detection is already 20 minutes.
  • Revoking without waiting for propagation saves 150,934 calls and cuts 68,503 legitimate requests: one cut request for every 2.2 calls saved. The right answer changes with the secret and cannot be written into a procedure.
  • Five of the six leak channels could have been closed at an earlier stage (3,100 minutes of detection latency in total); the largest single share is the key embedded in the client bundle — 2,880 minutes and 26,800 calls, all of it from the build stage.

Next Step

This topic measured the life of secrets and shortened it: the access matrix went from seventy-nine items to thirty-one, the exposure window shrank two-thousand-fold on sixteen items, twenty items disappeared entirely with workload identity, and the revocation flow was written minute by minute for the remaining six secrets. Secrets now have an owner, a life, a rotation schedule, and a leak procedure.

Under all of this there is an assumption, and it has not been touched at all. Everything measured in this topic was about a service proving itself: a call that carries the right credential passes, one that does not carry it does not. But what happens when a call does carry the credential. When the field app API calls the billing service, the billing service never asks whether that call is authorized to read billing data; it sees that the call came from somewhere and fulfills it. Even if the secret is managed correctly, if this stays so, the capture of a single service is a door that opens onto the others: every trust relation among the nine services is a path along which one can move from one service to another. The next topic counts those paths — how many service pairs have no authentication, how many of them mutual authentication closes, and at which stage the ones that remain open could have been closed.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close