---
title: 'Leak Response'
source: 'https://academia.sh/en/courses/runtime-security/leak-response'
course: 'Runtime Security and Secrets Management'
language: en
updated: '2026-10-07T04:31:06+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - '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'
---

# 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.

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

```js
// 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

```js
// 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.
